Accesing attributes in the joining table with has_many through - ruby-on-rails

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.

Related

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)

Rails Eager Load Identifiers in has_many Association

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)

Rails active record query with multiple associations

I have tables called users, orders, and delivery_times that are linked using the following relationship.
For table User:
belongs_to :orders
For table orders:
belongs_to :delivery_times
I want to write a query on table users using a condition on table delivery_times as shown:
User.includes(order: :delivery_time).where("delivery_times.start < ?",Time.now)
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "delivery_times"
However I get an error. Can I use the RoR ORM to make this query work using includes, even though I know there is a solution using joins?
You will need a join for this kind of query, since you need the joint knowledge of the delivery_times table and the users table.
What includes actually does is it decides between preload and eager_load automatically and tries to always take the better one. In you case it will do an eager_load; have a look into this article.
For the error you get, I guess it yould result from starting with Users and not User:
User.includes(order: :delivery_time).where("delivery_times.start < ?",Time.now)
Everything else seems correct to me.
The better definition of relations between your models would be this:
So your classes would look like this:
class User < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :user
has_one :delivery_time
end
class DeliveryTime < ActiveRecord::Base
belongs_to :order
end
The query you are making doesn't make any sense? What is the result that you are expecting?
If you want to get order that their delivery time is a specific time you can use scopes:
class Order < ActiveRecord::Base
belongs_to :user
has_one :delivery_time
scope :ready_to_deliver, includes(:delivery_time).where("delivery_time.start < ? ", Time.now)
end
Then you can get orders that are ready to deliver like this:
ready_orders = Order.ready_to_deliver

Rails 4 - Restrict retrieved columns from associated models

I'm new to Rails, and while writing Active Record queries, I notice that all columns of all associated tables are being retrieved. I would like to tell Active Record which fields from which tables ought to be retrieved. How would go about doing that?
My models and their associations are as follows:
class User < ActiveRecord::Base
has_one :profile
has_many :comments
has_many :posts
end
class Profile < ActiveRecord::Base
belongs_to :user
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
end
I'm following the Rails Edge Guides, and when I try to use select("users.id, profiles.first_name, profiles.last_name, comments.comment") to specify the field lists, I get a deprecation warning on the Rails console (and the SQL query that is run is a LEFT OUTER JOIN of all tables involved, but it still includes all columns):
DEPRECATION WARNING: It looks like you are eager loading table(s) (one of: users, posts) that are referenced in a string SQL snippet. For example:
Post.includes(:comments).where("comments.title = 'foo'")
Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string:
Post.includes(:comments).where("comments.title = 'foo'").references(:comments)
If you don't rely on implicit join references you can disable the feature entirely by setting `config.active_record.disable_implicit_join_references = true`. (called from irb_binding at (irb):34)
Check if following work for you
Class User < ActivcRecord::Base
default_scope select("column1, column2, column3")
end
Buried deep inside the Rails Edge Guides for Active Record Query Interface, I found the answer. The trick is to use scopes for the particular association type where you want to restrict the retrieved fields.
Quoted directly from the guide:
4.1.3 Scopes for belongs_to
There may be times when you wish to customize the query used by belongs_to. Such customizations can be achieved via a scope block. For example:
class Order < ActiveRecord::Base
belongs_to :customer, -> { where active: true },
dependent: :destroy
end
You can use any of the standard querying methods inside the scope block.
So, adding a select method to the above scope, with the list of fields you want retrieved will do the trick.

Filtering results of a many to many relationship using data in the join table

I have rails application with two models, StudentProfile and ClassProfile that are associated via a join table (model Enrollment).
In my ClassProfile model, I have:
has_many :enrollments
has_many :student_profiles, through: :enrollments
and in my StudentProfile model, I have:
has_one :enrollment
has_one :class_profile, through: :enrollment
My enrollments table also has a status integer field.
I would like to put a method in the ClassProfile model called "roster" that returns all the student_profiles that have an enrollment status of 1. Right now, I have the following:
def roster
self.student_profiles
end
Needless to say, all this does is return all students in a class, regardless of enrollment status. I feel like this should be simple, but I've seen no examples of how to add filtering on the join table (enrollments). Is this something I can accomplish in the ClassProfile model, or do I need to do something in Enrollment (or elsewhere)?
Update
From looking through the query reference mentioned by #QMFNP, here's the code that worked:
self.student_profiles.includes(:enrollment).where('enrollments.status = ?', 1)
Needed to change :enrollments to :enrollment because it's a has_one association. And the enrollments field that I'm filtering on is status, so changed enrollments.id to enrollments.status.
Thanks!
This can be accomplished by specifying your query conditions on the association like so:
def roster
self.student_profiles.includes(:enrollment).where('enrollments.status = ?', 1)
end
This should return your expected results. More information about querying on Active Record associations can be found here:
http://guides.rubyonrails.org/active_record_querying.html
Hope that helps!

Resources