Rails 4 - Restrict retrieved columns from associated models - ruby-on-rails

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.

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.

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

Get all associated data for all records of a model type in Rails?

How do I meld together getting all of the associated data for all of the records of a given model?
I have the following models:
User --N:1--> Reservation <--1:N-- Concert
So pseudo-code:
Reservation belongs_to User
Reservation belongs_to Concert
User has_many Reservations
User has_many Concerts through Reservations
Concert has_many Reservations
Concert has_many Users through Reservations
How do I make a single big array of everything?
I can get all my Reservations via Reservation.all
I can get the User for a particular Reservation via Reservation.find(25).user
I can get the Concert for a particular Reservation via Reservation.find(25).concert
But how do I get it for all of them? If I do
Reservation.all.each do |res|
res.user.name+","+res.concert.name+","+res.concert.date # etc.
end
Then it will do two new database queries for each reservation as it loops through. For 10 records, it might not matter, but for thousands, it can be very painful. Add to it other associations (e.g. Concert belongs_to venue, User has_one email, etc.)...
Is there any way to say, "Get all of the reservations and the following attached info" so it loads in a single SQL query?
What you're trying to accomplish is called eager loading, and can be done using includes in ActiveRecord. See below:
N + 1 queries problem
Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the includes method of the Model.find call. With includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.
http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
In your example you could use the following:
Reservation.all.includes(:user, :concert)
It is also a good idea to specify the :inverse_of option for your :belongs_to relations. This optimizes object loading and makes sure that cross-referencing a model will point back to the same object in memory, i.e.:
#user == #user.reservations.first.user # true
More information available here:
If you are using a belongs_to on the join model, it is a good idea to set the :inverse_of option on the belongs_to ...
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
In your example:
# app/models/reservation.rb
belongs_to :user, :inverse_of => :reservations
belongs_to :concert, :inverse_of => :reservations

Include a record by default in any association

I have a Client Model as below:
class Client < ActiveRecord::Base
has_many :custodians,:dependent => :destroy
I have a Custodian Model as below:
class Custodian < ActiveRecord::Base
belongs_to :client
In my custodians table I have record with id = 0 , name = 'N/A' that I want to include in all my collection_selects irrespective of the client_id.
e.g for client_id = 10 I want the following in collection_select
Custodian.where('client_id = 10 or client_id = 0')
I know I can do it in my views but I have too many views so it is not practical. Plus I want a more DRY method on either Custodian model or associations. I tried default_scope on Custodian model but could not get it to work.
Basically I am looking for way to always include custodian with id=0 in each association and collection_select.
You can't do what you want using a has_many and belongs_to approach. To implement a belongs_to relationship, the Custodian record has to have a single client_id field. Your logic requires that the custodian_id=0 record belong to many Client records, so it would have to have many client_id fields, but it can only have one. See the Rails Guides-Active Record Associations-The belongs_to Association (http://guides.rubyonrails.org/association_basics.html)
You can accomplish what you want by using a has_and_belongs_to_many relationship. By making both the Custodian and Client models has_and_belongs_to_many to each other, you will be able to have the custodian_id=0 record belong to many Client records and all the other Custodian records will only belong to one client (even though they could belong to many, your program logic must only allow them to belong to one.) See the has_and_belongs_to_many section of the above Rails Guide. To be clear, here is how your models would look:
class Client < ActiveRecord::Base
has_many_and_belongs_to_many :custodians
end
class Custodian < ActiveRecord::Base
has_many_and_belongs_to_many :client
end
Also, because of your special case on custodian_id=0, you will need to establish the look-up table record for the custodian_id=0 record relationship using an active_record callback (probably before_validation or before_create) when you create a new Client record.
Similarly, you will need to implement your own :dependent => :destroy functionality using the before_destroy callback to preserve the custodian_id=0 record and delete all the other associated Custodian records. You'll also have to destroy the corresponding look-up table entries.
This sounds like a lot of work, but if you absolutely must have the custodian_id=0 record associated with every Client, this is the only way I can see it being done. You may want to evaluate it this is really necessary. There may be other program logic that could allow you to get to similar results without going through this process.
You could use an instance or class method:
#app/models/client.rb
Class Client < ActiveRecord::Base
has_many :custodians,:dependent => :destroy
def inc_zero(id)
where("client_id = ? OR client_id = 0", id)
end
def self.inc_zero_custodians(id)
joins(:custodians).where("client_id = ? OR client_id = 0", id)
end
end
#-> Client.custodians.inc_zero(10)
#-> Client.inc_zero_custodians(10)

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.

Resources