Im very new to Ruby on Rails 3 and ActiveRecord and seem to have been thrown in at the deep end at work. Im struggling to get to grips with querying data from multiple tables using joins.
A lot of the examples Ive seen either seem to be based on much simpler queries or use < rails 3 syntax.
Given that I know the business_unit_group_id and have the following associations how would I query a list of all related Items and ItemSellingPrices?
class BusinessUnitGroup < ActiveRecord::Base
has_many :business_unit_group_items
end
class BusinessUnitGroupItem < ActiveRecord::Base
belongs_to :business_unit_group
belongs_to :item
belongs_to :item_selling_price
end
class Item < ActiveRecord::Base
has_many :business_unit_group_items
end
class ItemSellingPrice < ActiveRecord::Base
has_many :business_unit_group_items
end
I'm confused as to whether I need to explicity specify any joins in the query since the associations are in place.
Basically, you do not need to specify the joins:
# This gives you all the BusinessUnitGroupItems for that BusinessUnitGroup
BusinessUnitGroup.find(id).business_unit_group_items
# BusinessUnitGroupItem seems to be a rich join table so you might
# be iterested in the items directly:
class BusinessUnitGroup < ActiveRecord::Base
has_many :items through => :business_unit_group_items
# and/or
has_many :item_selling_prices, through => :business_unit_group_items
...
end
# Then this gives you the items and prices for that BusinessUnitGroup:
BusinessUnitGroup.find(id).items
BusinessUnitGroup.find(id).item_selling_prices
# If you want to iterate over all items and their prices within one
# BusinessUnitGroup, try this:
group = BusinessUnitGroup.include(
:business_unit_group_item => [:items, :item_selling_prices]
).find(id)
# which preloads the items and item prices so while iterating,
# no more database queries occur
Related
I have the following models
class Order < ApplicationRecord
has_many :order_details
has_many :products, through: :order_details
end
class OrderDetail < ApplicationRecord
belongs_to :order
belongs_to :product
end
class Product < ApplicationRecord
has_many :order_details
has_many :orders, through: :order_details
end
And I already have product records in my database.
Now, if using syntax: Order.create name: 'HH', product_ids: [1,2]
1 Order record is created, and rails automatically creates 2 more OrderDetail records to connect that Order record with 2 Products.
This syntax is quite handy.
Now, I want to learn more about it from the Rails documentation. But now i still can't find the documentation about it. Can someone help me find documents to learn more?
[Edit] Additional: I'd like to find documentation on the Rails syntax that allows passing a list of ids to automatically create records in the intermediate table, like the Order.create syntax with ```product_ids` `` that I gave above.
The extensive documentation is at https://api.rubyonrails.org/, and many-to-many is here.
The essential part is to analyze the source code of Rails at Module (ActiveRecord::Associations::CollectionAssociation) and at id_writers method:
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
primary_key = reflection.association_primary_key
pk_type = klass.type_for_attribute(primary_key)
ids = Array(ids).compact_blank
ids.map! { |i| pk_type.cast(i) }
# .... code continues
We see that ids parameter (ex.: [1,2]) is first checked to be Array then the compact_blank method removes all falses values, after that, ids are casted to match primary_key type of the model (usually :id). Then code continues to query database with where to get found ids (associations) and saves.
I have a user in my application that can have multiple assessments, plans, and materials. There is already a relationship between these in my database. I would like to show all these in a single tab without querying the database too many times.
I tried to do a method that joins them all in a single table but was unsuccessful. The return was the following error: undefined method 'joins' for #<User:0x007fcec9e91368>
def library
self.joins(:materials, :assessments, :plans)
end
My end goal is to just itterate over all objects returned from the join so they can be displayed rather than having three different variables that need to be queried slowing down my load times. Any idea how this is possible?
class User < ApplicationRecord
has_many :materials, dependent: :destroy
has_many :plans, dependent: :destroy
has_many :assessments, dependent: :destroy
end
class Material < ApplicationRecord
belongs_to :user
end
class Assessment < ApplicationRecord
belongs_to :user
end
class Plan < ApplicationRecord
belongs_to :user
end
If all you want to do is preload associations, use includes:
class User < ApplicationRecord
# ...
scope :with_library, -> { includes(:materials, :assessments, :plans) }
end
Use it like this:
User.with_library.find(1)
User.where(:name => "Trenton").with_library
User.all.with_library
# etc.
Once the associations are preloaded, you could use this for your library method to populate a single array with all the materials, assessments and plans of a particular user:
class User < ApplicationRecord
# ...
def library
[materials, assessments, plans].map(&:to_a).flatten(1)
end
end
Example use case:
users = User.all.with_library
users.first.library
# => [ ... ]
More info: https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Prefer includes over joins unless you have a specific reason to do otherwise. includes will eliminate N+1 queries, while still constructing usable records in the associations: you can then loop through everything just as you would otherwise.
However, in this case, it sounds like you're working from a single User instance: in that case, includes (or joins) can't really help -- there are no N+1 queries to eliminate.
While it's important to avoid running queries per row you're displaying (N+1), the difference between one query and three is negligible. (It'd cost more in overhead to try to squish everything together.) For this usage, it's just unnecessary.
I have a very special cases. I understand maybe db design is not very awesome, but I cannot change that.
class Employer < ApplicationRecord
has_many :contract_employers
has_many :contracts, through: :contract_employers
has_many :crm_contacts, through: :contract_employers
# typical join table, with key: contract_id and employer_id
class ContractEmployer < ApplicationRecord
belongs_to :contract
belongs_to :employer
has_many :crm_contacts
# CrmContact table has key: contract_employer_id
class CrmContact < ApplicationRecord
belongs_to :contract_employer
has_one :employer, through: :contract_employer
Given
employer = Employer.create
I have no issue to run
employer.contracts.create
However, if I try to run
employer.crm_contacts.create
It raise error
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection: Cannot modify association 'Employer#crm_contacts' because the source reflection class 'CrmContact' is associated to 'ContractEmployer' via :has_many.
I checked the rails source code, basically it states very clearly
# Construct attributes for :through pointing to owner and associate. This is used by the
# methods which create and delete records on the association.
#
# We only support indirectly modifying through associations which have a belongs_to source.
# This is the "has_many :tags, through: :taggings" situation, where the join model
# typically has a belongs_to on both side. In other words, associations which could also
# be represented as has_and_belongs_to_many associations.
#
# We do not support creating/deleting records on the association where the source has
# some other type, because this opens up a whole can of worms, and in basically any
# situation it is more natural for the user to just create or modify their join records
# directly as required.
So only typical join table supports model.associations.create? Any suggestion for my user case?
Take my case for example, even rail is willing to do the job. How could employer.crm_contacts.create create middle table record ContractEmployer? Yes, it knows employer.id, but it has no clue what contract.id is, right?
Rails can not create middle table record in this case, but you can.
And I am completely agree with this (comments in rails source code /activerecord/lib/active_record/associations/through_association.rb):
in basically any situation it is more natural for the user to just
create or modify their join records directly as required
I don't see a problem here.
class Employer < ApplicationRecord
# ...
def create_crm_contact
ActiveRecord::Base.transaction do
contract = contracts.create # will create both `contract` and associated `contract_employer`
# find the `contract_employer` that has been just created
contract_employer = contract_employers.find_by(contract_id: contract.id)
contract_employer.crm_contacts.create
end
end
Not new to Ruby on Rails, but never really worked with more complicated ActiveRecord queries.
Say I have a Affiliate model that has_many referred users and referred users has_many purchased_products.
What I want to do is an efficient ActiveRecord way of getting the total sum of the count of purchased_products of all the referred users. How do I go about doing this?
Thanks.
Assuming objects like:
class Affiliate < ActiveRecord::Base
has_many :users
end
class Users < ActiveRecord::Base
#should have purchased_products_count integer column
belongs_to :affiliate
has_many :pruchased_products
end
class PurchasedProducts < ActiveRecord::Base
belongs_to :user, counter_cache: :purchased_products_count
end
products_count = User.first.purchased_products.size # uses counter_cache to get the size
another_products_count = User.first.purchased_products_count # get the value diretly
all_users_products_count = my_affiliate.users.map(&:purchased_products_count).inject(:+) # makes an array of product counts then sums them
I think this might also work
my_affiliate.users.sum('purchased_products_count')
I have 3 models:
class ProductLine < ActiveRecord::Base
has_many :specifications
has_many :specification_categories, :through => :specifications,
end
class Specification < ActiveRecord::Base
belongs_to :product_line
belongs_to :specification_category
end
class SpecificationCategory < ActiveRecord::Base
has_many :specifications
has_many :product_lines, :through => :specifications
end
Basically, we are showing the specifications as a subset of data on the product line page and we would like to do something like (example only, yes I'm aware of N+1):
Controller:
#product_line = ProductLine.find(params[:id])
#specification_categories = #product_line.specification_categories)
View:
#specification_categories.each do |specification_category|
...
specification_category.specifications.each do |specification|
...
end
end
The issue here is getting rails to filter the specifications by ProductLine. I've tried constructing queries to do this but it always generates a separate NEW query when the final association is called. Even though we aren't using the code above now (not a good idea here, since we could potentially run into an N+1 problem), I'd like to know if it's even possible to do a 3 way join with association filtering. Has anyone run across this scenario? Can you please provide an example of how I would accomplish this here?
Prevent the N+1 by altering your line to:
#specification_categories = #product_line.specification_categories).include(:specifications)
OR
Construct your own query using .joins(:association), and do the grouping yourself.