ActiveRecord where statement selecting fields from nested objects - ruby-on-rails

Given
class Foo
has_many :bar
end
class Bar
belongs_to :foo
end
I want:
=> #<ActiveRecord::Relation [#<Foo id: 11, qux: 'hi', bar_id: 1, bar_name: 'blah', bar_something: 'blahblah' >, #<Foo id: 23, qux: 'hi', bar_id: 2, bar_name: 'lorem', bar_something: 'ipsum' >]>
I can do this:
> Foo.where(qux: 'hi').includes(:bar)
=> #<ActiveRecord::Relation [#<Foo id: 11, qux: 'hi', bar_id: 1 >, #<Foo id: 23, qux: 'hi', bar_id: 2 >]>
But it does not load the child records. It seems just to hold on to it in case it's needed.
There must be something more elegant than this?
Foo.where(qux: 'hi').includes(:bar).to_a.map do | f |
f.keys.each { |k| f[ k.to_s ] = f.delete(k) if k.class == :symbol }
Bar.column_names.except('id','foo_id').each do | ba |
ba_name = 'bar_' + ba
f.merge({ba_name => f.bar.send(ba.to_sym)})
end
f
end

includes(:bar) lazy loads the child records, in this case bar. It's one way to avoid n+1 queries (so that you don't run one query for each instance of foo). And you do have access to it.
Foo.where(qux: 'hi').each do |foo|
puts foo.bar.inspect
end
If you want to get all foos where their bar.qux = hi, then go the otherway:
Bar.joins(:foo).where(foo: { qux: 'hi' })

Foo.select("foos.id,foos.qux,bar_id,bars.bar_name,bars.something").joins(:bar).where(qux: 'hi')
includes lazy load the association so it basically does not merge both tables. What you are looking can be done through joins which allow you to query on both tables and select all required columns which you want. You can find more help here http://tomdallimore.com/blog/includes-vs-joins-in-rails-when-and-where/

Do you actually need the AR relation to have all those values loaded up-front? The lazy loading is intentional to keep you from beating up the DB unnecessarily...
You can still reference any attribute of bar directly:
Foo.where(qux: 'hi').includes(:bar).each do |foo|
puts foo.bar.name # This will actually load bar from the DB
end
There aren't usually great reasons for overriding this, especially if the dataset could be large-ish.

Related

ActiveAdmin form for nested jsonb field

I am building a new/edit form for an ActiveRecord model in ActiveAdmin. One of the attributes of that model is a jsonb attribute. Its value is expected to be a Hash whose values are themselves Hashes:
{
'foo' => {
'a' => 1,
'b' => 2
},
'bar' => {
'c' => 3,
'd' => 4
}
}
The MVP that I'm going for is to generate a form that looks like this:
Foo
a __________________
b __________________
Bar
c __________________
d __________________
So essentially, I only want to ask the user to input values for each of the inner Hashes. I've been having some trouble doing this with ActiveAdmin. This is my best attempt to render the above form, but it only renders the "Bar" section. :jsonb_field is the name of the jsonb attribute on the model.
f.inputs name: 'JSONB', for: :jsonb_field do |jsb|
jsb.inputs name: 'Foo', for: 'foo' do |x|
x.input :a
x.input :b
end
jsb.inputs name: 'Bar', for: 'bar' do |y|
y.input :c
y.input :d
end
end
I feel like there's a proper way to do this but my research has come up empty. Can anyone help me figure out how to make this happen? Thank you!

Rails order by calculation in associated table

I’m working with the below Rails Models.
Artist.rb
has_many: updates
Update.rb
belongs_to: artist
Updates has a popularity column (int 0-100)
I need to order artists by difference in popularity within the last 30 days. (last row - first row of updates in range)
I’ve made this work in controller by iterating over list of artists, calculate the difference in popularity, and save that value together with the artist id in a new array. Then sort that array by increase value and recreate the list of artists in the correct order. Issue is this causes a timeout error on my application as the iteration happens upon clicking “search”.
Method to calculate difference:
class Update < ApplicationRecord
belongs_to :artist
def self.pop_diff(a)
in_range = joins(:artist).where(artists: {name: a}).where(created_at: 30.days.ago.to_date..Time.now().to_date)
diff = in_range.last.popularity - in_range.first.popularity
return diff
end
end
Creating a new array in controller with correct ordering:
#artists = Artist.all
#ordering = Array.new
#artists.each do |a|
#ordering << {"artist" => a, "diff" => Update.pop_diff(a) }
end
#ordering = #ordering.sort_by { |k| k["diff"]}.reverse!
Does anyone know best practice on dealing with these types of situations?
These are the three paths I can think of:
Tweaking above solution to work more efficiently
Using a virtual column (attr_accessor) and storing the increase there. I’ve never done this before, not sure what’s possible
Build a back-end script that saves increase value in database on a daily base.
It would be most performant to do this in SQL
class Artist < ApplicationRecord
def self.get_popularity_extreme(direction = 'ASC', days_ago = 30)
<<-SQL
SELECT popularity
FROM updates
WHERE updates.created_at BETWEEN (DATEADD(DAY, -#{days_ago.to_i.abs}, NOW()), NOW())
ORDER BY updates.created_at #{direction.presence || 'ASC'}
LIMIT 1
SQL
end
def self.by_popularity_difference
joins(
<<-SQL
LEFT JOIN (
#{get_popularity_extreme}
) earliest_update ON updates.artist_id = artists.id
LEFT JOIN (
#{get_popularity_extreme('DESC')}
) latest_update ON updates.artist_id = artists.id
SQL
).
where('earliest_update.popularity IS NOT NULL').
where('latest_update.popularity IS NOT NULL').
select('artists.*', 'latest_update.popularity - earliest_update.popularity AS popularity_difference').
order('popularity_difference DESC')
end
end
Of course this is not the 'rails way'
The other option I would take would be to add a trigger to Update after_save to also set a column in the parent artist table
class Update < ApplicationRecord
belongs_to :artist
after_save :set_artist_difference
def self.pop_diff(a)
in_range = where(artist_id: a.id).where(created_at: 30.days.ago.to_date..Time.now().to_date).limit(1)
in_range.order(created_at: :desc).first.popularity - in_range.order(:created_at).first.popularity
end
def set_artist_difference
artist.update(difference: self.class.pop_diff(a))
end
end
the downside to this is if not every artist gets an update every day, the number won't be accurate
If you are to continue using your current solution, you should specify the order, explicit return is unnecessary, you shouldn't lookup an artist you already have, and the join isn't needed, (and also it's just wrong because your passing the whole artist, yet filtering it on 'name'):
class Update < ApplicationRecord
belongs_to :artist
def self.pop_diff(a)
in_range = where(artist_id: a.id).where(created_at: 30.days.ago.to_date..Time.now().to_date).limit(1)
in_range.order(created_at: :desc).first.popularity - in_range.order(:created_at).first.popularity
end
end
also instead of sorting the opposite direction then reversing, sort by negative diff:
#artists = Artist.all
#ordering = Array.new
#artists.find_in_batches do |batch|
batch.each do |a|
#ordering << {"artist" => a, "diff" => Update.pop_diff(a) }
end
end
#ordering = #ordering.sort_by { |k| -(k["diff"])}
Well, this approach you took has a problem with slow performance, in part because of the many queries you execute in the DB. Here's a simple way to do that (or very close to):
artists = Artist.all
pops =
artists.
includes(:updates).
where('updates.created_at' => 30.days.ago..Time.zone.now).
pluck(:id, 'updates.popularity').
group_by {|g| g.first}.
flat_map do |id, list|
diffs = list.map(&:second).compact
{
artist: artists.find { |artist| artist.id == id},
pops: diffs.last - diffs.first
}
end
# => [{:artist=>#<Artist id: 1, name: "1", created_at: "2018-07-10 05:44:29", updated_at: "2018-07-10 05:44:29">, :pops=>[10, 11, 1]}, {:artist=>#<Artist id: 2, name: "2", created_at: "2018-07-10 05:44:32", updated_at: "2018-07-10 05:44:32">, :pops=>[]}, {:artist=>#<Artist id: 3, name: "3", created_at: "2018-07-10 05:44:34", updated_at: "2018-07-10 05:44:34">, :pops=>[]}]
Much much more performant! But notice this is still not the most performant way to do the job. Still, it is very quick (although a little bit algebraic - you can improve somewhat) and uses a lot of the ruby and rails tricks to achieve the result you're looking for. Hope it helps! =)

Rails 4: select multiple attributes from a model instance

How do I fetch multiple attributes from a model instance, e.g.
Resource.first.attributes(:foo, :bar, :baz)
# or
Resource.where(foo: 1).fetch(:foo, :bar, :baz)
rather than returning all the attributes and selecting them manually.
You will use the method slice.
Slice a hash to include only the given keys. Returns a hash containing the given keys.
Your code will be.
Resource.first.attributes.slice("foo", "bar", "baz")
# with .where
Resource.where(foo: 1).select("foo, bar, baz").map(&:attributes)
How about pluck:
Resource.where(something: 1).pluck(:foo, :bar, :baz)
Which translates to the following SQL:
SELECT "resources"."foo", "resources"."bar" FROM, "resources"."baz" FROM "resources"
And returns an array of the specified column values for each of the records in the relation:
[["anc", 1, "M2JjZGY"], ["Idk", 2, "ZTc1NjY"]]
http://guides.rubyonrails.org/active_record_querying.html#pluck
Couple of notes:
Multiple value pluck is supported starting from Rails 4, so if you're using Rails 3 it won't work.
pluck is defined on ActiveRelation, not on a single instnce.
If you want the result to be a hash of attribute name => value for each record you can zip the results by doing something like the following:
attrs = [:foo, :bar, :baz]
Resource.where(something: 1).pluck(*attrs).map{ |vals| attrs.zip(vals).to_h }
To Fetch Multiple has_one or belongs_to Relationships, Not Just Static Attributes.
To fetch multiple relationships, such as has_one or belongs_to, you can use slice directly on the instance, use values to obtain just the values and then manipulate them with a map or collect.
For example, to get the category and author of a book, you could do something like this:
book.slice( :category, :author ).values
#=> #<Category id: 1, name: "Science Fiction", ...>, #<Author id: 1, name: "Aldous Huxley", ...>
If you want to show the String values of these, you could use to_s, like:
book.slice( :category, :author ).values.map( &:to_s )
#=> [ "Science Fiction", "Aldous Huxley" ]
And you can further manipulate them using a join, like:
book.slice( :category, :author ).values.map( &:to_s ).join( "➝" )
#=> "Science Fiction ➝ Aldous Huxley"

Rails CollectionProxy randomly inserts in wrong order

I'm seeing some weird behaviour in my models, and was hoping someone could shed some light on the issue.
# user model
class User < ActiveRecord::Base
has_many :events
has_and_belongs_to_many :attended_events
def attend(event)
self.attended_events << event
end
end
# helper method in /spec-dir
def attend_events(host, guest)
host.events.each do |event|
guest.attend(event)
end
end
This, for some reason inserts the event with id 2 before the event with id 1, like so:
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 2, name: "dummy-event", user_id: 1>, #<Event id: 1, name: "dummy-event", user_id: 1>
But, when I do something seemlingly random - like for instance change the attend_event method like so:
def attend_event(event)
self.attended_events << event
p self.attended_events # random puts statement
end
It gets inserted in the correct order.
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 1, name: "dummy-event", user_id: 1>, #<Event id: 2, name: "dummy-event", user_id: 1>
What am I not getting here?
Unless you specify an order on the association, associations are unordered when they are retrieved from the database (the generated sql won't have an order clause so the database is free to return things in whatever order it wants)
You can specify an order by doing (rails 4.x upwards)
has_and_belongs_to_many :attended_events, scope: -> {order("something")}
or, on earlier versions
has_and_belongs_to_many :attended_events, :order => "something"
When you've just inserted the object you may see a different object - here you are probably seeing the loaded version of the association, which is just an array (wrapped by the proxy)

Rails order records by column and custom method

For one of my models I'm trying to set a default scope that sorts by year and season. Since year is an integer, it's easy to order by that. My trouble is ordering by season (if the year is the same). Here's just ordering by year:
class League < ActiveRecord::Base
def self.default_scope
order(:year)
end
# The season's that are allowed to be used
# This is also the order I'd like to use
def self.season_collection
{
"Spring" => "Spring",
"Summer" => "Summer",
"Fall" => "Fall"
}
end
end
If I try order(:year, :season) then that will just do it alphabetically. Is there any way to use order (so it's done on the database end)?
You can order them in the database, but it isn't going to be very efficient as you'll need to coerce the value of the season field into a integer, and then use that to order the records. See this answer for an example:
SQL: ORDER BY using a substring within a specific column... possible?
A better way would be to store the season as an integer, not a string, in the database. The easiest way to use this would be ActiveRecord::Enum available in Rails 4.1+. In your model add this:
class League < ActiveRecord::Base
enum season: %w{Spring Summer Autumn Winter}
end
Then you can create records like this:
0> league1 = League.create!(season: 'Summer')
=> #<League id: 1>
1> league2 = League.create!(season: 'Spring')
=> #<League id: 2>
2> league3 = League.create!(season: 'Autumn')
=> #<League id: 3>
3> league3.season
=> "Autumn"
Under the hood ActiveRecord doesn't store the string, but an integer referring to it. You can find the integers as follows:
4> League.seasons
=> {"Spring"=>0, "Summer"=>1, "Autumn"=>2, "Winter"=>3}
To get them in order it's then just a case of ordering the field:
5> League.order(:season)
SELECT * FROM leagues ORDER BY season
=> #<ActiveRecord::Relation [#<League id: 2>, #<League id: 1>, #<League id: 3>]>
If you want to query for a specific season ActiveRecord will automatically map the name to the ID:
6> League.where(season: 'Summer')
SELECT * FROM leagues WHERE season = 1
=> #<ActiveRecord::Relation [#<League id: 1>]>
If you try and set an invalid season, ActiveRecord will let you know:
7> league3.season = 'Tomato'
ArgumentError: 'Tomato' is not a valid season

Resources