Rails Query with nested association and group ( by associated object) - ruby-on-rails

Given this model relationships:
orders.rb
belongs_to user
user.rb
belongs_to company
company.rb
has_many users
I wanted to count all orders made by each company, so I got this:
Order.joins(user: :company)
.group(['companies.name, companies.id])
.count
and the result is
["Company 1", 1] => 123
BUT I don't know how to change this query to have
<Company id: 1,name: "Jim Beam"> => 5
instead of array of Company's attributes.

This can be made easier by adding another association to the model:
# company.rb
has_many :users
has_many :orders, through: :users
Then you can do this:
companies = Company.
joins(:orders).
group("company.id").
select("company.id, company.name, count(orders.id) as orders_count")
if you then say companies.first you will see something like this:
#<Company:0x005610fa35e678 id: 15 name: 'foo'>
it doesn't look like the orders_count is there. But it actually has been loaded already. You can get at it like this:
companies.first.orders_count
You can also see it if you convert the company to a hash:
companies.first.attributes # => { id: 15, orders_count: 2, name: "foo" }
In Postgres at least, if you want to get more data in the attributes hash you have to individually specify each of the columns in the select string. I'm not sure about other databases.

Related

Rails: foreign keys - how does the DB table looks like?

i have found this example here on stack overflow:
class User < ActiveRecord::Base
has_many :winners, class_name: "Competition", foreign_key: "competition_id"
end
class Competition < ActiveRecord::Base
belongs_to :winner, class_name: "User", foreign_key: "winner_id"
end
What exactly is the "has_many: winners" or "belongs_to: winner" in this case?
Is there a "competition_id" column in the users table and a "winner_id" column in the competitions table?
Greets
The tables would look something like:
users:
id: integer,
# other fields - name, email etc.
competitions:
id: integer,
winner_id: integer,
# other fields - name, place etc.
When someone uses the winners has_many association, for example like:
User.find(3).winners
it'll do a query that ends up as something like:
SELECT * FROM competitions WHERE winner_id = 3 # the 3 here comes from the user's id
When someone uses the winner belongs_to association, for example like:
Competition.find(4).winner
it'll do a query that ends up as something like:
SELECT * FROM users WHERE id = 1 # the 1 here has come from the winner_id on the competition record
The winners association is possibly poorly named; won_competitions would likely be a better name.
More reading: https://guides.rubyonrails.org/association_basics.html

Rails query has_many through association

I have three models associated with the following ways:
User model
has_many :project_memberships
has_many :projects, through: :project_memberships
Project model
has_many :project_memberships
has_many :users, through: :project_memberships
Project membership model
belongs_to :user
belongs_to :project
The project membership model also has additional fields like user_role, invitation_accepted etc.
I want to get all the users in a specified project, with all the project membership fields.
Example:
# user json response
[
{
id: user_id,
name: user_name,
user_role: "admin",
invitation_accepted: true
},
{
// Etc
}
]
Currently, I have something like:
def index
#project = Project.find(params[:project_id])
#team_members = #project.project_memberships
end
The team_members only returns
#<ActiveRecord::Associations::CollectionProxy [#<ProjectMembership id: "42087cd2-31f5-4453-b620-5b47a82de422", user_id: "4f428880-48d0-40d0-b6d6-eed9172ce78d", project_id: "3e758d26-7625-4cbd-8980-77085f8d38a0", role: "admin", invitation_accepted: true, job_title: nil, created_at: "2020-10-24 05:48:38", updated_at: "2020-10-24 05:48:38">]>
I am getting the user_id, but don't know how to merge the actual user fields in the above query.
You can use includes to preload data (behind the scenes, it is performing a join).
#team_members = #project.project_memberships.includes(:user)
Now you can call #team_members[0].user.name (or extract the user name from all of them) and it doesn't fire an additional database query to load the user.
Note that this does work without includes, but it will be slower, and introduces a common pitfall known as "N+1 queries"

Rails: Creating/updating has_many relationships for existing has_many records

Given:
class Group < ApplicationRecord
has_many :customers, inverse_of: :group
accepts_nested_attributes_for :customers, allow_destroy: true
end
class Customer < ApplicationRecord
belongs_to :group, inverse_of: :customers
end
I want to create/update a group and assign existing customers to the group e.g.:
Group.new(customers_attributes: [{ id: 1 }, { id: 2 }])
This does not work though because Rails will just throw ActiveRecord::RecordNotFound: Couldn't find Customer with ID=1 for Group with ID= (or ID=the_group_id if I'm updating a Group). Only way I've found to fix it is just extract customers_attributes and then do a separate Customer.where(id: [1,2]).update_all(group_id: 'groups_id') after the Group save! call.
Anyone else come across this? I feel like a way to fix it would be to have a key like _existing: true inside customers_attributes (much like _destroy: true is used to nullify the foreign key) could work. Or does something like this violate a Rails principle that I'm not seeing?
Actually, you don't need to use nested attributes for this, you can instead set the association_ids attribute directly:
Group.new(customer_ids: [1, 2])
This will automatically update the group_id on each referenced Customer when the record is saved.

How to display unique records from a has_many through relationship?

I'm wondering what is the best way to display unique records from a has_many, through relationship in Rails3.
I have three models:
class User < ActiveRecord::Base
has_many :orders
has_many :products, :through => :orders
end
class Products < ActiveRecord::Base
has_many :orders
has_many :users, :through => :orders
end
class Order < ActiveRecord::Base
belongs_to :user, :counter_cache => true
belongs_to :product, :counter_cache => true
end
Lets say I want to list all the products a customer has ordered on their show page.
They may have ordered some products multiple times, so I'm using counter_cache to display in descending rank order, based on the number of orders.
But, if they have ordered a product multiple times, I need to ensure that each product is only listed once.
#products = #user.products.ranked(:limit => 10).uniq!
works when there are multiple order records for a product, but generates an error if a product has only been ordered once. (ranked is custom sort function defined elsewhere)
Another alternative is:
#products = #user.products.ranked(:limit => 10, :select => "DISTINCT(ID)")
I'm not confident that I'm on the right approach here.
Has anyone else tackled this? What issues did you come up against? Where can I find out more about the difference between .unique! and DISTINCT()?
What is the best way to generate a list of unique records through a has_many, through relationship?
Thanks
Have you tried to specify the :uniq option on the has_many association:
has_many :products, :through => :orders, :uniq => true
From the Rails documentation:
:uniq
If true, duplicates will be omitted from the collection. Useful in conjunction with :through.
UPDATE FOR RAILS 4:
In Rails 4, has_many :products, :through => :orders, :uniq => true is deprecated. Instead, you should now write has_many :products, -> { distinct }, through: :orders. See the distinct section for has_many: :through relationships on the ActiveRecord Associations documentation for more information. Thanks to Kurt Mueller for pointing this out in his comment.
Note that uniq: true has been removed from the valid options for has_many as of Rails 4.
In Rails 4 you have to supply a scope to configure this kind of behavior. Scopes can be supplied through lambdas, like so:
has_many :products, -> { uniq }, :through => :orders
The rails guide covers this and other ways you can use scopes to filter your relation's queries, scroll down to section 4.3.3:
http://guides.rubyonrails.org/association_basics.html#has-many-association-reference
On Rails 6 I got this to work perfectly:
has_many :regions, -> { order(:name).distinct }, through: :sites
I couldn't get any of the other answers to work.
in rails6 use -> { distinct } in scope it will work
class Person
has_many :readings
has_many :articles, -> { distinct }, through: :readings
end
person = Person.create(name: 'Honda')
article = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
You could use group_by. For example, I have a photo gallery shopping cart for which I want order items to be sorted by which photo (each photo can be ordered multiple times and in different size prints). This then returns a hash with the product (photo) as the key and each time it was ordered can be listed in context of the photo (or not). Using this technique, you could actually output an order history for each given product. Not sure if that's helpful to you in this context, but I found it quite useful. Here's the code
OrdersController#show
#order = Order.find(params[:id])
#order_items_by_photo = #order.order_items.group_by(&:photo)
#order_items_by_photo then looks something like this:
=> {#<Photo id: 128>=>[#<OrderItem id: 2, photo_id: 128>, #<OrderItem id: 19, photo_id: 128>]
So you could do something like:
#orders_by_product = #user.orders.group_by(&:product)
Then when you get this in your view, just loop through something like this:
- for product, orders in #user.orders_by_product
- "#{product.name}: #{orders.size}"
- for order in orders
- output_order_details
This way you avoid the issue seen when returning only one product, since you always know that it will return a hash with a product as the key and an array of your orders.
It might be overkill for what you're trying to do, but it does give you some nice options (i.e. dates ordered, etc.) to work with in addition to the quantity.

Rails 3, belongs_to, has one? For 3 models, Users, Instances, Books

I have the following models:
Users (id, name, email, instance_id, etc...)
Instances (id, domain name)
Books (id, name, user_id, instance_id)
In Rails 3, When a new book is created, I need the user_id, and instance_id to be populated based on the current_user.
Currently, user_id is being assigned when I create a new book but not instance_id? What needs to happen in rails to make that field get filled out on book creation?
Also, shouldn't rails be error'ing given that I can create books without that instance_id filled out?
thxs
It looks like you have de-normalized User and Book models by adding reference to Instance model. You can avoid the redundant reference unless you have a specific reason.
I would rewrite your models as follows:
class Instance < ActiveRecord::Base
has_many :users
has_many :books, :through => :users, :order => "created_at DESC"
end
class User < ActiveRecord::Base
belongs_to :instance
has_many :books, :order => "created_at DESC"
end
class Book < ActiveRecord::Base
belongs_to :user
has_one :instance, :through => :user
end
Now to create a new book for a user.
current_user.books.build(...)
To get a list of the books belonging to user's instance:
current_user.instance.books
To get a list of the books created by the user:
current_user.books
Make sure you index the instance_id column in users table and user_id column in books table.
Rails will only produce an error in this case if (a) you have a validation that's failing, or (b) you have database foreign keys that aren't being satisfied.
What's an instance? i.e. if instance_id is to be populated based on the current user, what attribute of the user should supply it? (the instance_id? why?)

Resources