The following code works, but runs far too many SQL statements for my liking;
User.includes([:profile,:roles]).select("id, email, created_at, confirmed_at, countries.name").find_in_batches do |users|
users.map{|u| # In here I access profile.country.name}
end
What is meant to do is grab the User information along with some profile information and put it into a csv format.
Inside the .map I am calling profile.country.name this is coming in fine but Rails is running an extra SQL call to find the name, what I want to do is capture this information in my initial lookup.
My issue is that because country is linked to profile and not user I can't add it to my .includes because it doesn't see it as a linkable table. Is there anything I can do to force the includes to join a table linked to profile?
The relevant model information is below;
User:
has_one :profile, dependent: :destroy
Profile:
belongs_to :user
belongs_to :country
Country:
has_many :profiles
You should be able to include country in the original query by nesting it under profile in the .includes call:
User.includes( {:profile => [:country], :roles} )
Related
Consider this example in Rails 6.1 with postgresql:
An organization has users & pictures
An item has_and_belongs_to_many pictures and belongs_to an organization
A user has_and_belongs_to_many items and belongs_to an organization, has relationship has_many :pictures, through: :items
pictures belong_to an organization & has_and_belongs_to_many users
pictures can only be viewed by an admin of an organization and those users have have/belong to them. So for a user doing something as simple as current_user.pictures will show them all the pictures they are allowed to view.
(in this example an admin uploads pictures and shares them with certain users)
This is where my question comes in:
Pictures can also be marked as public, which would allow any user to view them in their list. So essentially I want combine organization.pictures.where(public: true) and current_user.pictures (which uses a join table) into one query and also easily add any sql order or pagination.
I could simply use concatenation current_user.pictures + organization.pictures.where(public: true) but that returns an array and then I'd have to use array methods to sort and paginate, which is undesirable.
What other options do I have? I can't seem to get an .or to work either.
Update: A solution I had figured would 'work' originally does and is what I'm going with for now, to pluck the ids from the two joins tables has_many :pictures, through: :items, but I am still curious of a better way;
In the user model:
def viewable_pictures
pictures_ids = pictures.pluck(:id)
organization.pictures.public_to_daycare.or(Picture.where(id: pictures_ids))
end
In this case, I would think the query from the Picture model instead of the organization object. Something like:
Picture.where(public: true, organization_id: organization.id).or(Picture.where(user_id: current_user.id)
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
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.
I'm using the Google places API to collect any number of locations on a profile. Each location has the profile_id, 4 address fields and a lat and long. I want to make sure that all locations for each profile are unique.
At the moment I'm using the code below to validate the uniqueness of the 4 address fields which is working perfectly however this results in a validation error being returned to the view. I would rather save the profile and locations (with duplicates removed) without returning an error to the user.
What is the best way of going about this? Is there a rails approach or should I create my own before-validation function?
class Profile < ActiveRecord::Base
has_many :locations
accepts_nested_attributes_for :locations, allow_destroy: true
etc etc
end
class Location < ActiveRecord::Base
belongs_to :profile
# Only allow unique locations for each profile
validates_uniqueness_of :profile_id, scope: [:sublocality, :locality, :administrative_area_level_1, :country]
etc etc
end
You can check for error[:profile_id].present? in after_validation callback.
If error related to profile_id is present - remove duplicates and save again
You should be careful if you have other validations related to profile_id and check for presence of specific error message in that case.
I have a rails app that is tracking social data. The users are going to be able to create groups and add pages(ie. facebook fan pages) to their groups by the page's social id. Since users could potentially be adding the page as someone else, I have it set up so that there is only one page per social id. I also have a pivot table called Catgorizations that links of the pages to the groups and users.
My model relationships are set up as follows:
User
has_many :groups
Group
belongs_to :user
has_many :categorizations
has_many :pages, :through => :categorizations
Page
has_many :categorizations
has_many :groups, :through => :categorizations
Categorization
belongs_to :group
belongs_to :page
Now when I create a new page and it saves, it is creating a new categorization. The problem I'm running into is that in the Categorization I need to set the user_id manually. I've tried:
#page.categorizations.user_id
But I get an undefined method user_id error. I may be approaching this from the wrong direction but how would I go about setting the user_id of a categorization through the page object?
Also if it matters, I'm using devise to handle my user management stuff.
Any help would be greatly appreciated.
Thanks
What you're tring to do is to access something several levels deep in a 'chained' set of relationships.
In order to access an instance of the User model, given a page, you need to get its categorizations, pick one (how?), get its group, then see what the user_id is of that group.
Conceptually a page will actually have many users that might be in charge of categorizing it.
To get something to happen you could arbitrarily pick the first categorization and then do something with its user:
cat = #page.categorizations.first
user = cat.group.user
I don't know what you mean about 'setting' the user id for the categorization - it doesn't have a user, so I don't know what you'll then want to do with that information, sorry!
From your description and your model, only Pages have a User, not Categorization, which is why you get the error that it doesn't exist (it's not in your database).
Also, you are missing the opposite association on Page:
Page
belongs_to :user
This allows you to get back to User from Page:
#page.user.id