How to order by latest nested model created_at in a query - ruby

I've got a question, I don't know if this is possible but suppose these models
class Event < ApplicationRecord
has_many :gifs
end
class Gif < ApplicationRecord
belongs_to :event
end
blank database using rails console you do this
gif_1 = Gif.create
gif_2 = Gif.create
gif_3 = Gif.create
event_1 = Event.new
event_1.gifs = [gif_1, gif_3]
event_1.save
event_2 = Event.new
event_2.gifs = [gif_2]
event_2.save
How would you order events by their last gifs created's created_at attribute.
Here is an example of what I've tried but that doesn't produce the correct result
ordered_events = Event.includes(:gifs).joins(:gifs).order("gifs.created_at DESC")
ordered_events.first.id
=> 2 # I want this to return 1
Now I understand why my attempt probably didn't work. I think it's because it probably only looked at the first Gif to do the ordering.
On top of this I had another thought, and here I have no idea where to begin trying to do this in a query, but what if the Event has 0 Gif, from what I wrote it seems no gifs simply relegates those events to after the ones who do have gifs but this would not work for me.
here's another context in rails console which is more realistic since normally you'd need an event first to store the Gif
event_1 = Event.create
event_2 = Event.create
gif_1 = Gif.create(event_id: event_1.id)
gif_2 = Gif.create(event_id: event_2.id)
event_1 = Event.create
gif_3 = Gif.create(event_id: event_1.id)
Now here what I would like to get back from my query would be something of the sorts of [event_1, event_3, event_2] because since event_3 has no gifs I want to use his created_at to order.
I know how I could do this by hand via some helper function or other but I would really love to be able to this kind of thing in one query directly.

As an example:
Event.joins(:gifs)
.group('events.id')
.order('MAX(gifs.created_at) DESC')
This query takes events, joins them with gifs, groups by event.id (to eliminate event duplicates in case one event has several gifs) and sorts result by latest (maximal) created_at time of event's gifs, in descending order.
Since it uses joins method, which unwraps to INNER JOIN in SQL query, events without gifs won't be returned by this query. To fix it use left_outer_joins:
Event.left_outer_joins(:gifs)
.group('events.id')
.order('MAX(gifs.created_at) DESC')

Related

Ruby: Paginate and sort across a large number of records

When simply displaying large amounts of data (over 100k records) my code works well, and I paginate on the server.
However, when I need to sort this data I'm stuck. I'm only sorting on the page, and NOT sorting on ALL the records related to this one customer.
How can I paginate but also sort across all the records of my customer and NOT simply sort the records returned from the server side pagination?
I'm also using BootStrap Table to display all my data.
Here is my code that gets all the customers:
def get_customers
#data_to_return = []
#currency = current_shop.country_currency
customers = current_shop.customers.limit(records_limit).offset(records_offset)#.order("#{sort_by}" " " "#{sort_order}")
customers.each do |customer|
#data_to_return.push(
state: false,
id: customer.id,
email: customer.email,
accepts_marketing: customer.accepts_marketing,
customer_status: customer.customer_status,
tags: customer.tags)
end
sort_customers
end
And then this is the sort_customers method:
def sort_customers
fixed_data = data_to_return.sort_by {|hsh| hsh[sort_by]}
customer_size = current_shop.customers.length
if sort_order == "ASC"
fixed_data
else
fixed_data.reverse!
end
render json: {"total": customer_size, "rows": fixed_data}
end
In the above code you can see that data_to_return is coming from get_customers and its limited. But I don't want to return ALL the customers for many reasons.
How can I sort across all the records, but only return the paginated subset?
You should actually sort at the model/query level, not at the ruby level.
The difference is basically:
# sort in ruby
relation.sort_by { |item| foo(item) }
# sort in database - composes with pagination
relation.order('column_name ASC/DESC')
In the first case, the relation is implicitly executed, enumerated and converted to array before calling sort_by. If you did pagination (manually or with kaminari), you will get just that page of data.
In the second case, you are actually composing the limit, offset and where (limit and offset are anyways used under the hood by kaminari, where is implicit when you use associations) with a order so your database would execute
SELECT `customers`.`*` FROM `customers`
WHERE ...
OFFSET ...
LIMIT ...
ORDER BY ...
which will return the correct data.
A good option is to define scopes in the model, like
class Customer < ApplicationRecord
scope :sorted_by_email, ->(ascending = true) { order("email #{ascending ? 'ASC' : 'DESC'}") }
end
# in controller
customers = current_shop.customers.
limit(records_limit).
offset(records_offset).
sorted_by_email(false)
You can resolve sorting and paginate issue using Data Tables library, which is client side. It's a Jquery library. Using this you need to load all data into page, then it would work very well.
Below are the references please check.
Data tables jquery libray
Data tables gem for rails
You can try these, they will work very well. You can customise it as well
If the answer is helpful, you can accept it.

ActiveRecord: check whether association exists without loading it?

Suppose I've got ActiveRecord models such that User has_one :photo. In the database, photos has a t.binary column which may hold a lot of data, so I don't want to SELECT that column unless I need to.
I want to do something like:
users.each do |user|
image_tag(user_photo_path) if user.photo.present?
end
However, I don't want to call user.photo.present? because:
Doing so loads the photo association, including SELECT * from photos
Even if it could be made to only SELECT id FROM photos to check existence, it's still an N + 1 query.
What I really want is to load users with a single query which gives each one a property telling me whether it has an associated photo or not.
With ActiveRecord 5, this works:
class User < ActiveRecord::Base
scope :with_photo_id, -> {
left_outer_joins(:photo).select(
"users.*, photos.id AS photo_id"
)
}
end
Then I can call User.with_photo_id and check user.photo_id.present?.
Prior to AR 5, the join would be uglier:
joins(
"LEFT OUTER JOIN photos ON photos.user_id = users.id"
)

Return database results in the same JSON parent and children

I have Team and Players classes and want to return the data in one JSON string which contains Team info but at the same time it displays all the information about the players.
class Team < ActiveRecord::Base
has_many :players
end
class Players < ActiveRecord::Base
belongs_to :team
end
I know how to retrieve the information about team and players but not in the same query. Another problem is I don't how to merge the result JSONs in one JSON.
team = Team.last.to_json
player = team.players.to_json
How can I query the info about Team and Players in the same query. I tried:
#team = Team.includes(:players).where(players: {team_id: Team.last}).last.to_json
and it only returns me information about the team. I want a JSON like:
-id
-name
-players
-player
-player
In case it's impossible, how can I merge into one JSON all the information from the two queries.
You can write a "join" to incorporate the players in the team with the team information. At that point you'll have a structure that has the information needed to create the JSON. See "12 Joining Tables" from the Active Record documentation for more information.
Or, you can make two separate queries, then create a bit more complex JSON hash or array allowing you to output both sets of data into one larger serialized object. For instance:
require 'json'
team = {
'name' => 'bears'
}
players = {
'1' => 'fred',
'2' => 'joe'
}
puts ({
'team' => team,
'players' => players
}).to_json
Here's the output:
{"team":{"name":"bears"},"players":{"1":"fred","2":"joe"}}
Here's the data returned back to the Ruby object:
data = '{"team":{"name":"bears"},"players":{"1":"fred","2":"joe"}}'
JSON[data]
# => {"team"=>{"name"=>"bears"}, "players"=>{"1"=>"fred", "2"=>"joe"}}
Also, since you're using Sinatra, it's not necessary to use Active Record. Sequel is a very good ORM, and is my personal favorite when working with Sinatra. You might find it easier to work with.
Another option to manual serialization is to use ActiveModel::Serializer which allows you to define relationships between objects and gives you finer grained choices of what to include when you serialize, what to filter out and what related objects to preload. An alternative could also be Rabl which also has quite a nice API.
If you're just playing around with a small amount of JSON this might be overkill, but it's a nice practice to be more organized

Rails 3 Query: How to get most viewed products/articles/whatever?

I always wondered how to query and get results that doesn't fit in a model. Similar how it's done using LINQ and projecting into anonymous objects.
So here's the simple schema:
# Product.rb
class Product < ActiveRecord::Base
has_many :product_views
# attributes: id, name, description, created_at, updated_at
end
# ProductView.rb
class ProductView < ActiveRecord::Base
belongs_to :product
# attributes: id, product_id, request_ip, created_at, updated_at
end
Basically I need to get a list of Products (preferably just id and name) along with the count of views it had. Obviously ordered by view count desc.
This is the SQL I want to get:
select
p.id,
p.name,
count(pv.product_id) as views
from
product_views pv
inner join
products p on pv.product_id = p.id
group by
pv.product_id
order by
count(product_id) desc
I tried the following and similar, but I'm getting ProductView objects, and I would like to get just an array or whatever.
ProductView.includes(:product)
.group('product_id')
.select("products.id, products.name, count(product_id)")
This kind of thing are trivial using plain SQL or LINQ, but I find myself stucked with this kind of queries in Rails. Maybe I'm not thinking in the famous 'rails way', maybe I'm missing something obvious.
So how do you do this kind of queries in Rails 3, and specifically this one? Any suggestions to improve the way I'm doing this are welcome.
Thank you
You can use Arel to do what you're looking for:
products = Product.arel_table
product_views = ProductView.arel_table
# expanded for readability:
sql = products.join(product_views)
.on(product_views[:product_id].eq(product[:id]))
.group(product_views[:product_id])
.order('views DESC')
.project(products[:id],
products[:name],
product_views[:id].count.as('views'))
products_with_views = Product.connection.select_all(sql.to_sql) # or select_rows to just get the values
Yes, it is long, but Arel is a very smart way to deal with creating complex queries that can be reused regardless of the database type.
Within a class method in the Product class:
Product.includes(:product_views).all.map { |p| [p.id, p.name, p.product_views.size] }
Then sort it however you want.
I don't know if there's a way to do it using your models. I would probably resort to:
Product.connection.select_rows(sql)
Which will give you an array of arrays. You can use select_all if you'd rather have an array of hashes.
Try this:
#product = Product.find(#product_id)
#product_views = #product.product_views.count
(Source - http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html#M000292)
Hope this helps!

ActiveRecord: Find through multiple instances

Say I have the following in my controller:
#category1
#category2
and I want to find all stores associated with those two categories...
#stores = #category1.stores + #category2.stores
this does work, but unfortunately returns an unaltered Array, rather than a AR::Base Array, and as such, I can't do things like pagination, scope, etc...
It seems to me like there's a built-in way of finding through multiple instance association... isn't there?
##stores = #category1.stores + #category2.stores
#if you want to call API methods you can just add conditions with the category id
#stores = Store.find(:all, :conditions => ['category_id=?', a || b])
With ActiveRecord, whenever you're finding a set of unique model objects, calling find on that model is usually your best bet.
Then all you need to do is constrain the join table with the categories you care about.
#stores = Store.all(:joins => :categories,
:conditions => ['category_stores.category_id in (?)', [#category1.id, #category2.id]])

Resources