Query on two HABTM models (Rails3) - ruby-on-rails

I have two models, Clients and Items with a HABTM association. I would like to find the fastest way to select all Clients, which have several (more than one) of the items (or a certain number of items).
class Client < ActiveRecord::Base
has_and_belongs_to_many :items
end
class Item < ActiveRecord::Base
has_and_belongs_to_many :clients
end
The query below fetches the clients, which have any ONE item from the list:
required_item_ids = Item.where(:kind => :book).collect(&:id)
Client.join(:items).where(:items => {:id => required_item_ids})
But needed is the list of clients having SEVERAL or ALL of the required items.
I believe this is a simple question as I am a newbie to Rails, but I looked through all similar questions here and in some other places and did not find the answer.

If this is a one-off job, then you could get this list via looping through all the clients in Ruby. Something like:
clients_with_more_than_one_book_items = []
book_items = Item.where(:kind => :book)
Client.find_each do |client|
books_count = 0
client.items.each do |item|
if book_items.include?(item)
books_count += 1
end
end
clients_with_more_than_one_book_items << client if books_count > 1
end
However, that may take a while to run if you have lots of clients and items, so if this is a query that you're running often, it's probably a good idea to add a books_count attribute to the Client model, and to add code (within callbacks on the model) that keep this updated as a kind of counter cache.

Related

Includes method ( n+ 1 issue ) doesn't work with a push method but does with += when assigning to an array

Consider the simple relation between Employee and Company models(many to many):
Company model:
has_many :employees, through: :company_employees
has_many :company_employees
Employee model:
has_many :companies, through: :company_employees
has_many :company_employees
CompanyEmployee model(join table):
belongs_to :employee
belongs_to :company
also an Owner model:
has_many :companies
So in my system i have an owner which may have several companies and an Employee, which may work for multiple companies.
Now, In my employees controller i want to fetch all of the employees workin for an owner:
def owners_linked
#company_employees = []
owner.companies.each do |company|
#company_employees.push (company.company_employees.includes(:company, :employee)) # when += instead of push - it works
end
respond_to do |format|
format.js {render "employees_list"}
end
end
I need to have an access to Employee instances(personal data), company_employees table (information about the position in the company) and Company(company related data).
To resolve n+1 problem and speed up the performance i use includes method.
Well, the problem is that in my controller action in line:
#company_employees.push company.company_employees.includes(:company, :employee)
when using push method it doesn't work. I obtain the error in the view that employee method is not defined.
On the other hand when i change the push to += sign it works perfectly fine.
Can anyone help me to understand why it's like so?
I know that += is inefficint so i'd rather not stick to it.
This doesn't have anything to do with your use of includes.
When you use += you end up with an array of CompanyEmployee objects. However when you use push you are no longer concatenating arrays but creating an array of collections. You are then calling employee on the collection rather than an element of the collection which is why you get an error.
Personally I would write this as
#company_employees = owner.companies.flat_map do |company|
company.companee_employees.include(...)
end
Although I would do so for reasons of succinctness rather than performance. Any performance difference between += and other ways of concatenating arrays is minuscule compared to the time it takes to fetch data from the database.
This doesn't entirely solve your n+1 problem though, since the data for each company is loaded separately. I would do
#company_employees = owner.companies.include(company_employees: [:company, :employee]).flat_map(&:company_employees)
Which doesn't do as many queries.
I usually attack this by coming in from the other side. I believe this will get what you are looking for:
#company_workers = Employee.where(company_id: owner.companies.pluck(:id))
the where(company_id: ...) can take an array and automatically set it up to be an in(...) command in SQL.
So the SQL will end up being something like:
select * from employees where company_id is in(1,2,3,4)
With the 1, 2, 3, 4 are the owner's company IDs.

Search for model by multiple join record ids associated to model by has_many in rails

I have a product model setup like the following:
class Product < ActiveRecord::Base
has_many :product_atts, :dependent => :destroy
has_many :atts, :through => :product_atts
has_many :variants, :class_name => "Product", :foreign_key => "parent_id", :dependent => :destroy
end
And I want to search for products that have associations with multiple attributes.
I thought maybe this would work:
Product.joins(:product_atts).where(parent_id: params[:product_id]).where(product_atts: {att_id: [5,7]})
But this does not seem to do what I am looking for. This does where ID or ID.
So I tried the following:
Product.joins(:product_atts).where(parent_id: 3).where(product_atts: {att_id: 5}).where(product_atts: {att_id: 7})
But this doesn't work either, it returns 0 results.
So my question is how do I look for a model by passing in attributes of multiple join models of the same model type?
SOLUTION:
att_ids = params[:att_ids] #This is an array of attribute ids
product = Product.find(params[:product_id]) #This is the parent product
scope = att_ids.reduce(product.variants) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE product_id=products.id AND att_id=?)', att_id)
end
product_variant = scope.first
This is a seemingly-simple request made actually pretty tricky by how SQL works. Joins are always just joining rows together, and your WHERE clauses are only going to be looking at one row at a time (hence why your expectations are not working like you expect -- it's not possible for one row to have two values for the same column.
There are a bunch of ways to solve this when dealing with raw SQL, but in Rails, I've found the simplest (not most efficient) way is to embed subqueries using the EXISTS keyword. Wrapping that up in a solution which handles arbitrary number of desired att_ids, you get:
scope = att_ids_to_find.reduce(Product) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE parent_id=products.id AND att_id=?)', att_id)
end
products = scope.all
If you're not familiar with reduce, what's going on is it's taking Product, then adding one additional where clause for each att_id. The end result is something like Product.where(...).where(...).where(...), but you don't need to worry about that too much. This solution also works well when mixed with scopes and other joins.

Rails: Sum of values in all Transactions that belong_to an Activity

Live site: http://iatidata.heroku.com
Github: https://github.com/markbrough/IATI-Data
Based on aid information released through the IATI Registry: iatiregistry.org
I'm a bit of a Rails n00b so sorry if this is a really stupid question.
There are two key Models in this app:
Activity - which contains details
such as recipient country, funding
organisation
Transaction - which contains details such as how much money (value) was committed or disbursed (transaction_type), when, to whom, etc.
All Transactions nest under an Activity. Each Activity has multiple Transactions. They are connected together by activity_id. has_many :transactions and belongs_to :activity are defined in the Activity and Transaction Models respectively.
So: all of this works great when I'm trying to get details of transactions for a single activity - either when looking at a single activity (activity->show) or looping through activities on the all activities page (activity->index). I just call
#activities.each do |activity|
activity.transactions.each do |transaction|
transaction.value # do something like display it
end
end
But what I now really want to do is to get the sum of all transactions for all activities (subject to :conditions for the activity).
What's the best way to do this? I guess I could do something like:
#totalvalue = 0
#activities.each do |activity|
activity.transactions.each do |transaction|
#totalvalue = #totalvalue + transaction.value
end
end
... but that doesn't seem very clean and making the server do unnecessary work. I figure it might be something to do with the model...?! sum() is another option maybe?
This has partly come about because I want to show the total amount going to each country for the nice bubbles on the front page :)
Thanks very much for any help!
Update:
Thanks for all the responses! So, this works now:
#thiscountry_activities.each do |a|
#thiscountry_value = #thiscountry_value + a.transactions.sum(:value)
end
But this doesn't work:
#thiscountry_value = #thiscountry_activities.transactions.sum(:value)
It gives this error:
undefined method `transactions' for #<Array:0xb5670038>
Looks like I have some sort of association problem. This is how the models are set up:
class Transaction < ActiveRecord::Base
belongs_to :activity
end
class Activity < ActiveRecord::Base
has_and_belongs_to_many :policy_markers
has_and_belongs_to_many :sectors
has_many :transactions
end
I think this is probably quite a simple problem, but I can't work out what's going on. The two models are connected together via id (in Activity) and activity_id (in Transactions).
Thanks again!
Use Active Record's awesome sum method, available for classes:
Transaction.sum(:value)
Or, like you want, associations:
activity.transactions.sum(:value)
Let the database do the work:
#total_value = Transaction.sum(:value)
This gives the total for all transactions. If you have some activities already loaded, you can filter them this way:
#total_value = Transaction.where(:activity_id => #activities.map(&:id)).sum(:value)
You can do it with one query:
#total_value = Transaction.joins(:activity).where("activities.name" => 'foo').sum(:value)
My code was getting pretty messy summing up virtual attributes. So I wrote this little method to do it for me. You just pass in a collection and a method name as a string or symbol and you get back a total. I hope someone finds this useful.
def vsum collection, v_attr # Totals the virtual attributes of a collection
total = 0
collection.each { |collect| total += collect.method(v_attr).call }
return total
end
# Example use
total_credits = vsum(Account.transactions, :credit)
Of course you don't need this if :credit is a table column. You are better off using the built in ActiveRecord method above. In my case i have a :quantity column that when positive is a :credit and negative is a :debit. Since :debit and :credit are not table columns they can't be summed using ActiveRecord.
As I understood, you would like to have the sum of all values of the transaction table. You can use SQL for that. I think it will be faster than doing it the Ruby way.
select sum(value) as transaction_value_sum from transaction;
You could do
#total_value = activity.transactions.sum(:value)
http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html

Best implementation of a multi-model association in rails?

Alright, a Rails Noob here, :D
It looks like has__many :through is the latest greatest way to handle many to many relationships, but I am trying to keep this simple. Hopefully one of you guru's out there have handled this situation before:
Here is the basic model setup I have now:
class User < ActiveRecord::Base
has_and_belongs_to_many :products
end
class Product < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :clients
end
class Client < ActiveRecord::Base
has_and_belongs_to_many :products
end
Essentially, I have users in the system, that will have access (through association) to many different products that are being created, those products have many clients, but the clients can also be a part of many products, and the Products accessed by many users.
All of the associations are working well, but now I want users to be able to add clients to their products, but only see clients that are associated with products they have access too.
Scenario:
Given Bob has access to product A and B
And does NOT have access to product C
And and has clients on product B
And wants to add them to product A.
When in product A Bob should see clients from product B in his add list,
And Bob should not see clients from product C
My noobish experience with rails fails to give me the experience on how to best build the array that will hold his client list.
The way I am thinking be to use #bob.products to get the products Bob has access to then to .each those and find the clients associated with each product and then join them into a single array. But is this the BEST way?
Thanks!
Not sure if this is what you're looking for, but if you want to remove all non-authorized clients for a particular user:
user = current_user
#clients_access = Array.new
user.products.each { |p| #clients_access.push(p.clients).uniq! }
#clients_access.flatten!
Alright so I achieved the functionality I wanted by the following:
user = current_user
#clients_no_access = Client.find(:all, :order => :business_name)
user.products.each do |product|
#clients_no_access -= product.clients
end
#all_clients = Client.find(:all,
:order => :business_name) - #clients_no_access - #product.clients
Basically, finding all the the clients, then iterating through the linked authorized products and removing them from the list, basically creating a list of non-authorized clients.. then doing the search again and clearing out the non-authorized clients and the clients already assigned in the group.. But, I have ran out of duct-tape.. any better solutions?

select many through many... kind of

This is my first post on Stack, so please bear with me if I breach any protocol.
I'm working on a project in Rails (2.1.2), and I have a relations scenario that looks like this:
event [has_many] days
People (in different categories) can sign up for event days, giving the following binding results:
category [has_many] customers [has_many] orders [has_many] days
[belongs_to] event
Now, I'd like to have the total number of 'events' for one customer, or for all customers in a certain category, and I'm stuck. AFAIK, there's no way of performing a simple 'find' through an array of objects, correct? So, what would be the solution; nested loops, and a collect method to get the 'events' from the 'days' in orders?
Please let me know if I'm unclear.
Thanks for your help!
I would personally do this using a MySQL statement. I don't know for sure, but I think it is a lot faster then the other examples (using the rails provided association methods).
That means that in the Customer model you could do something like:
(Note that I'm assuming you are using the default association keys: 'model_name_id')
class Customer
def events
Event.find_by_sql("SELECT DISTINCT e.* FROM events e, days d, orders o, customers c WHERE c.id=o.customer_id AND o.id=d.order_id AND e.id=d.event_id")
end
end
That will return all the events associated with the user, and no duplicated (the 'DISTINCT' keyword makes sure of that). You will, as with the example above, lose information about what days exactly the user signed up for. If you need that information, please say so.
Also, I haven't included an example for your Category model, because I assumed you could adapt my example yourself. If not, just let me know.
EDIT:
I just read you just want to count the events. That can be done even faster (or at least, less memory intensive) using the count statement. To use that, just use the following function:
def event_count
Event.count_by_sql(SELECT DISTINCT COUNT(e.*) FROM ... ... ...
end
Your models probably look like this:
class Category
has_many :customers
class Customer
has_many :orders
has_many :days, :through => :orders # I added this
belongs_to :category
class Order
has_many :days
belongs_to :customer
class Day
belongs_to :event
belongs_to :order
class Event
has_many :days
With this you can count events for customer:
events = customer.days.count(:group => 'event_id')
It will return OrderedHash like this:
#<OrderedHash {1=>5, 2=>13, 3=>0}>
You can get events count by:
events[event_id]
events[1] # => 5
events[2] # => 13
etc.
If you want total number of uniq events:
events.size # => 3
In case of counting events for all customers in category I'd do something like this:
events = {}
category.customers.each {|c| events.merge!(c.days.count(:group => 'event_id') ) }
events.size # => 9 - it should be total number of events
But in this case you lose information how many times each event appeared.
I didn't test this, so there could be some mistakes.

Resources