ActiveRecord Eager Load model that isn't belongs_to - ruby-on-rails

I'm running into an issue with n+1 queries and I want to eager load a relationship, except I'm having trouble defining the relationship. It's complicated, haha, hear me out.
I have two models.
class Pokemon < ActiveRecord::Base
belongs_to :pokemon_detail, primary_key: "level", foreign_key: "level"
end
class PokemonDetail < ActiveRecord::Base
has_one :pokemons, primary_ley: "level", foreign_key: "level"
end
Let's say, I have a the following record:
<Pokemon id: 1, name: "squirtle", level: 1>
Which would obviously correspond with the following PokemonDetail
<PokemonDetail id: 1, name: "squirtle", level: 1, health: 150>
And that can be easily eager loaded like Pokemon.all.includes(:pokemon_detail), however, I want to eager load the information about one level higher.
<PokemonDetail id: 2, name: "squirtle", level: 2, health: 300>
I currently find the information about one level higher with the following method within the Pokemon model.
def next_level_info
PokemonDetail.where(level: self.level + 1)
end
But this isn't eager loaded. Any ideas?

Refactor schema first, to make it more sense:
pokemons { current_level_value, name }
pokemon_levels {value, pokemon_id (foreign key), health }
Redefine models like this:
class PokemonLevel < ActiveRecord::Base
belongs_to :pokemon
end
class Pokemon < ActiveRecord::Base
has_many :pokemon_levels
has_one :current_pokemon_level, -> { joins(:pokemon).where('pokemons.current_level_value = pokemon_levels.value') },
foreign_key: :pokemon_id, class_name: 'PokemonLevel'
has_one :next_pokemon_level, -> { joins(:pokemon).where('pokemons.current_level_value + 1 = pokemon_levels.value') },
foreign_key: :pokemon_id, class_name: 'PokemonLevel'
end
Simply use it eg:
Pokemon.includes(:current_pokemon_level, :next_pokemon_level).find(123)
I use PokemonLevel instead of PokemonDetail, because it is clearer to me

Related

Optimizing database query in Rails 5

I have 4 models
module Vehicle
has_many :routes
end
module Route
has_many :route_users
belongs_to :vehicle
end
module RouteUser
belongs_to :route
belongs_to :user
end
module User
belongs_to :route_user
end
My goal is to return the most recent driver (a user) through the aggregate.rb; to be specific, I need user id, first_name, last_name.
attributes = {
id: vehicle.id,
... other attributes ...
}
attributes.merge!(
driver: {
id: vehicle.routes.last.route_users.last.user.id,
first_name: vehicle.routes.last.route_users.last.user.first_name,
last_name: vehicle.routes.last.route_users.last.user.last_name
}
) if vehicle.routes.present? && vehicle.routes.last.route_users.present?
As you can see, .merge! loads a bunch of information and dramatically slows down the aggregate.rb return. Is there any way to optimize this return to make it faster? Am I missing something?
You can improve your User model to make the query easier.
class User < ApplicationRecord
has_many :route_users
has_many :routes, through: :route_users
has_many :vehicles, through: :routes
end
And when the query is big from one side, the best way is two invert the logic, and make a query from user, example:
First fetch last_user_drive, and after that, use his fields to merge into attributes
last_user_driver = User.joins(routes: :vehicle).where(vehicle: {id: vehicle.id}).order('routes.created_at').last
...
attributes.merge!(
driver: {
id: last_user_driver.id,
first_name: last_user_driver.first_name,
last_name: last_user_driver.last_name
}
) if last_user_driver.present?

Rails 5: How to allow model create through when polymorphic reference also carries distinct association

I have model with polymorhphic reference to two other models. I've also included distinct references per this article eager load polymorphic so I can still do model-specific queries as part of my .where clause. My queries work so I can search for scores doing Score.where(athlete: {foo}), however, when I try to do a .create, I get an error because the distinct reference alias seems to be blinding Rails of my polymorphic reference during validation.
Given that athletes can compete individually and as part of a team:
class Athlete < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Team < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Score < ApplicationRecord
belongs_to :scoreable, polymorphic: true
belongs_to :athlete, -> { where(scores: {scoreable_type: 'Athlete'}) }, foreign_key: 'scoreable_id'
belongs_to :team, -> { where(scores: {scoreable_type: 'Team'}) }, foreign_key: 'scoreable_id'
def athlete
return unless scoreable_type == "Athlete"
super
end
def team
return unless scoreable_type == "Team"
super
end
end
When I try to do:
Athlete.first.scores.create(score: 5)
...or...
Score.create(score: 5, scoreable_id: Athlete.first.id, scoreable_type: "Athlete")
I get the error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: scores.scoreable_type
Thanks!
#blazpie, using your scoping suggestion worked for me.
"those scoped belongs_to can be easily substituted by scopes in Score: scope :for_teams, -> { where(scorable_type: 'Team') }

Loading Relations

Goal: I would like to include all of a customers medical conditions as an array in the result of a customer.
for:
cust = Customer.includes(:conditions).find(1)
expected result:
#<Customer id: 1, first_name: "John", last_name: "Doe", conditions [...]>
actual result:
#<Customer id: 1, first_name: "John", last_name: "Doe">
code:
I have 2 classes and a 3rd join class (ConditionsCustomer).
class Customer < ApplicationRecord
has_many :conditions_customers
has_many :conditions, through: :conditions_customers
end
#join table. Contains 2 foreign_keys (customer_id, condition_id)
class ConditionsCustomer < ApplicationRecord
belongs_to :customer
belongs_to :condition
end
class Condition < ApplicationRecord
has_many :conditions_customers
has_many :customers, through: :conditions_customers
end
What's interesting is that I see 3 select queries getting fired (customer, join table and medical conditions table) so I know the includes is somewhat working but unfortunately customer returns without the medical conditions.
I've also tried using a join but I get an array of same customer over and over again.
Is there an easy way to do this with ActiveRecord? I would prefer not having to merge the record manually.
Not really possible via active record, as json offers some cool possibilities :
render json: customers,
include: {
conditions: {
only: [:attr1, :attr2], # filter returned fields
methods: [:meth1, :meth2] # if you need model methods
},
another_joined_model: {
except: [:password] # to exclude specific fields
}
}

Rails 5: includes for polymorphic with where conditions

I need to query my models and produce a record similar to this:
[{
"subscriber": {
"email": "user#example.com",
"subscriptions": [{
"confirmed": true,
"subscriptionable": {
"name": "Place XYZ",
"comments": [{
"author": "John",
"body": "Hello."
}]
}
}]
}
}]
My models look like this:
class Subscriber < ApplicationRecord
has_many :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :subscriptionable, polymorphic: true
belongs_to :subscriber
end
class Place < ApplicationRecord
has_many :subscriptions, as: :subscriptionable
has_many :comments, as: :commentable
end
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
So far I'm able to produce the record I want by running this query:
Subscriber.includes(subscriptions: [subscriptionable: [:comments]])
The problem is I that, with the query above, I can't specify any where conditions. For example, this will fail:
Subscriber.includes(subscriptions: [subscriptionable: [:comments]])
.where(subscriptions: { confirmed: true })
> ActiveRecord::EagerLoadPolymorphicError: Cannot eagerly load the polymorphic association :subscriptionable
And the other issue is that I can't just get certain attributes, for example:
Subscriber.includes(subscriptions: [subscriptionable: [:comments]])
.pluck("subscribers.email")
> ActiveRecord::EagerLoadPolymorphicError: Cannot eagerly load the polymorphic association :subscriptionable
Edit
Maybe this will help clarify: what I would like to achieve is something in the lines of this SQL query:
SELECT subscriptions.name as sub_name, subscriptions.email as sub_email,
places.name as place_name, comments.author as com_author, comments.body as com_body, subscribers.token
FROM subscriptions
JOIN subscribers
ON subscriptions.subscriber_id = subscribers.id
JOIN places
ON subscriptionable_id = places.id
JOIN comments
ON places.id = commentable_id
WHERE subscriptions.confirmed
AND subscriptionable_type = 'Place'
AND commentable_type = 'Place'
AND comments.status = 1
AND comments.updated_at >= '#{1.week.ago}'

How to eager load an association in the subclass

I have the following code :
class Parent < ActiveRecord::Base
# attributes name:string, root_id:integer , type:string , .....
has_many :childrens, class_name: 'Children, foreign_key: :root_id, primary_key: :id
end
and another class Children with
class Children < Parent
belongs_to :root, class_name: 'Parent', foreign_key: :root_id, primary_key: :id
end
Parent objects can be appear multiple times in the same table (for some reason...), but i wouldn't duplicate rows and copy all information each time, so i just make this Children subclass which inherit from Parent, and root_id is the reference to the parent, example :
object 1 : { id: 1 , name: "parent object", root_id: nil, type: nil, .... }
object 2 : {id: 2, name: "child", root_id: 1, type: 'Children', ....}
object 3 : {id: 3, name: "child", root_id: 1, type: 'Children', ... }
then i do something like this :
Parent.where("id IN (2, 3)")
here i fetch just 'Children' objects, so i want to eager load their parent, to have access to the name, and also other attributes ...
i tried with this
Parent.where("id IN (2, 3)").includes(:root)
but i get this error :
Association named 'root' was not found on Parent; perhaps you misspelled it?
it seem that :root association from the subclass is not accessible in the Parent class, there is a way to improve performance ?
Simple. The includes method takes on the associations that you have defined for a class. In your case, it takes only :childrens(it should actually be children without 's').
So to make available the :root to the includes method, cut and paste the belongs_to association from the Children(it should be actually be Child) class to the Parent class as below.
class Parent < ActiveRecord::Base
has_many :childrens, class_name: 'Children, foreign_key: :root_id, primary_key: :id
belongs_to :root, class_name: 'Parent', foreign_key: :root_id, primary_key: :id
end
Now, the following should work assuming, you dont have other problems.
Parent.where("id IN (2, 3)").includes(:root)
Tip:
1.9.3p385 :007 > "Child".pluralize
=> "Children"
1.9.3p385 :008 > "Children".singularize
=> "Child"

Resources