Rails Eager Load Identifiers in has_many Association - ruby-on-rails

Say I have the following models:
class Category < ActiveRecord::Base
has_and_belongs_to_many :items
end
class Item < ActiveRecord::Base
has_and_belongs_to_many :categories
end
I'm building an endpoint that retrieves all items, where each item should be coupled with the array of category IDs it belongs to. An example result would be:
[
{
name: 'My Item',
category_ids: [1, 2, 3]
},
// more items...
]
With this configuration, I'm able to call item.category_ids on each record which results in the SQL that I want, which looks like SELECT categories.id FROM categories INNER JOIN categories_items ... WHERE categories_items.item_id = $1.
However, that obviously results in an N+1 - and I can't seem to find a way to do this now in an eager load. What I'm looking to do is get a list of items, and just the ID of the categories that they're in, by eager loading the category_ids field of each Item (although, if there's another way to accomplish the same using eager loading I'm okay with that as well)
Edit to explain difference from the duplicate:
I'm looking to see if it's possible to do two this in two separate queries. One that fetches items, and one that fetches category IDs. The items table is relatively wide, I'd prefer not to multiply the number of rows returned by the number of categories each item has - if possible.

Using a join model instead of has_and_belongs_to_many will allow you to query the join table directly instead (without hacking out a query yourself):
class Category < ActiveRecord::Base
has_many :item_categories
has_many :items, through: :item_categories
end
class Item < ActiveRecord::Base
has_many :item_categories
has_many :categories, through: :item_categories
end
class ItemCategory
belongs_to :item
belongs_to :category
end
Note that the table naming scheme for has_many through: is different - instead of items_categories its item_categories. This is so that the constant lookup will work correctly (otherwise rails looks for Items::Category as the join model!) - so you will need a migration to rename the table.
items = Item.all
category_ids = ItemCategory.where(item_id: items.ids).pluck(:category_id)

Related

many-to-many relationship in rails

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.

How do I get the records with exact has_many through number of entries on rails

I have a many to many relationship through a has_many through
class Person < ActiveRecord::Base
has_many :rentals
has_many :books, through rentals
end
class Rentals < ActiveRecord::Base
belongs_to :book
belongs_to :person
end
class Book < ActiveRecord::Base
has_many :rentals
has_many :persons, through rentals
end
How can I get the persons that have only one book?
If the table for Person is called persons, you can build an appropriate SQL query using ActiveRecord's query DSL:
people_with_book_ids = Person.joins(:books)
.select('persons.id')
.group('persons.id')
.having('COUNT(books.id) = 1')
Person.where(id: people_with_book_ids)
Although it's two lines of Rails code, ActiveRecord will combine it into a single call to the database. If you run it in a Rails console, you may see a SQL statement that looks something like:
SELECT "persons".* FROM "persons" WHERE "deals"."id" IN
(SELECT persons.id FROM "persons" INNER JOIN "rentals"
ON "rentals"."person_id" = "persons"."id"
INNER JOIN "books" ON "rentals"."book_id" = "books"."id"
GROUP BY persons.id HAVING count(books.id) > 1)
If this is something you want to do often, Rails offers what is called a counter cache:
The :counter_cache option can be used to make finding the number of belonging objects more efficient.
With this declaration, Rails will keep the cache value up to date, and then return that value in response to the size method.
Effectively this places a new attribute on your Person called books_count that will allow you to quite simply filter by the number of associated books:
Person.where(books_count: 1)

Accesing attributes in the joining table with has_many through

I have a many2many relationship with a has_many through association:
class User < ActiveRecord::Base
has_many :trips_users
has_many :trips, through: :trips_users
end
class Trip < ActiveRecord::Base
has_many :trips_users
has_many :users, through: :trips_users
end
class TripsUser < ActiveRecord::Base
belongs_to :user
belongs_to :trip
end
The joining table trips_user contains a column named 'pending' which id like get when I ask for a list of trips of a user.
So in my controller I need to get all trips a user has, but also adding the 'pending' column.
I was trying
current_user.trips.includes(:trips_users)
that will be done by this select statement:
SELECT trips.* FROM trips INNER JOIN trips_users ON trips.id
= trips_users.trip_id WHERE trips_users.user_id = 3
which is missing the information in the trips_users table that I want.
The desired sql would be:
SELECT trips.*, trips_users.* FROM trips INNER JOIN trips_usersON trips.id =
trips_users.trip_id WHERE trips_users.user_id = 3
This finally worked:
current_user.trips.select('trips_users.*, trips.*')
Overriding the select part of the SQL.
Not very pretty in my opinion thou, I shouldn't be messing with tables and queries but models, specially in such a common case of a m2m association with extra data in the middle.
You'll want to use joins rather than includes for this... See the following Rails Guide:
http://guides.rubyonrails.org/active_record_querying.html#joining-tables
Essentially you'd do something like this:
current_user.trips.joins(:trips_users)
The includes method is used for eager loading, while joins actually performs the table join.
You could also try:
trips_users = current_user.trips_users.includes(:trip)
trips_users.first.pending?
trips_users.first.trip
Which should give you the trips_users records for that user but also eager loading the trips so that accessing them wouldn't hit the database again.

3 or more model association confusion at Rails

It has been almost a week since I'm trying to find a solution to my confusion... Here it is:
I have a Program model.
I have a ProgramCategory model.
I have a ProgramSubcategory model.
Let's make it more clear:
ProgramCategory ======> Shows, Movies,
ProgramSubcategory ===> Featured Shows, Action Movies
Program ==============> Lost, Dexter, Game of Thrones etc...
I want to able to associate each of these models with eachother. I've got what I want to do particularly with many-to-many association. I have a categories_navigation JOIN model/table and all of my other tables are connected to it. By this way, I can access all fields of all of these models.
BUT...
As you know, has_many :through style associations are always plural. There is nothing such as has_one :through or belongs_to through. BUT I want to play with SINGULAR objects, NOT arrays. A Program has ONLY ONE Subcategory and ONLY ONE Category. I'm just using a join table to only make connection between those 3. For example, at the moment I can access program.program_categories[0].title but I want to access it such like program.program_category for example.
How can I have 'has_many :through's abilities but has_one's singular usage convention all together? :|
P.S: My previous question was about this situation too, but I decided to start from scratch and learn about philosophy of associations. If you want so you may check my previous post here: How to access associated model through another model in Rails?
Why a join table where you have a direct relationship? In the end, a program belongs to a subcategory, which in turn belongs to one category. So no join table needed.
class Program < ActiveRecord::Base
belongs_to :subcategory # references the "subcategory_id" in the table
# belongs_to :category, :through => :subcategory
delegate :category, :to => :subcategory
end
class Subcategory < ActiveRecord::Base
has_many :programs
belongs_to :category # references the "category_id" in the table
end
class Category < ActiveRecord::Base
has_many :subcategories
has_many :programs, :through => :subcategories
end
Another point of view is to make categories a tree, so you don't need an additional model for "level-2" categories, you can add as many levels you want. If you use a tree implementation like "closure_tree" you can also get all subcategories (at any level), all supercategories, etc
In that case you skip the Subcategory model, as it is just a category with depth=2
class Program < ActiveRecord::Base
belongs_to :category # references the "category_id" in the table
scope :in_categories, lambda do |cats|
where(:category_id => cats) # accepts one or an array of either integers or Categories
end
end
class Category < ActiveRecord::Base
acts_as_tree
has_many :programs
end
Just an example on how to use a tree to filter by category. Suppose you have a select box, and you select a category from it. You want to retrieve all the object which correspond to any subcategory thereof, not only the category.
class ProgramsController < ApplicationController
def index
#programs = Program.scoped
if params[:category].present?
category = Category.find(params[:category])
#programs = #programs.in_categories(category.descendant_ids + [category.id])
end
end
end
Tree-win!

Category and Product mapping in activerecord

With RoR ORM, how would you map this model.
Categories
-id
Products
-id
CategoriesProducts
-category_id
-product_id
So I want to perform queries like:
category.products.all
product.category.id
In rails, how do you decide which side of the relationship will be used for adding products to a category?
Will it be like:
category.add_product(product)
or
product.add_category(category)
And what if I wanted to fetch all categories with id 234,24,431,214 and products, is there a way I could do this w/o running into a n+1 query issue?
Simplest way to do it, use has_and_belongs_to_many. Make sure that when creating the CategoriesProucts table, you have the migration file create without an id:
create_table :categories_products, :id => false do |t|
Then your cateogry model should look like this:
class Category < ActiveRecord::Base
has_and_belongs_to_many :products
end
And your Product model should look like this:
class Product < ActiveRecord::Base
has_and_belongs_to_many :categories
end
Here's another method. This would also work if you wanted to have some more control for modification later. On your CategoriesProduct model:
class CategoriesProduct < ActiveRecord::Base
belongs_to :category
belongs_to :product
end
On your Categories model:
class Category < ActiveRecord::Base
has_many :categories_products
has_many :products, :through => :categories_products
end
On your product model:
class Product < ActiveRecord::Base
has_many :categories_products
has_many :categories, :through => :categories_products
end
Then you should be able to do:
Category.create(:name => "Cat1")
Category.first.products.create(:name => "Prod1")
Product.first.categories.create(:name => "Cat2")
etc...
This infograph may be helpful in visualizing the concept.
Create a table categories_products, categories, and products, and create modal for Category and Products.
Have the relationship has_and_belongs_to_many in both the modals. This should give allow you to use methods like #category.products (to get all products, for a particular category) and #product.categories (to get all categories for that product).
Also look at these:
http://guides.rubyonrails.org/association_basics.html#the-has_and_belongs_to_many-association
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many
In terms of deciding how to get the data back again it is really up to you and what angle your coming at the data from.
For instance, in a categories_controller it will make more sense to come through categories to products, and vice versa for products.
To get all products for certain categories you can combine this:
Category.where("id IN (?)", [1,2,3,4,5,6]).includes(:products)
(or similar, I've not tested this). This will do one query for Categories, and one for the products.

Resources