Example
class User
has_many :tickets
end
I want to create association which contains logic of count tickets of user and use it in includes (user has_one ticket_count)
Users.includes(:tickets_count)
I tried
has_one :tickets_count, :select => "COUNT(*) as tickets_count,tickets.user_id " ,:class_name => 'Ticket', :group => "tickets.user_id", :readonly => true
User.includes(:tickets_count)
ArgumentError: Unknown key: group
In this case association query in include should use count with group by ...
How can I implement this using rails?
Update
I can't change table structure
I want AR generate 1 query for collection of users with includes
Update2
I know SQL an I know how to select this with joins, but my question is now like "How to get data" . My question is about building association which I can use in includes. Thanks
Update3
I tried create association created like user has_one ticket_count , but
looks like has_one doesn't support association extensions
has_one doesn't support :group option
has_one doesn't support finder_sql
Try this:
class User
has_one :tickets_count, :class_name => 'Ticket',
:select => "user_id, tickets_count",
:finder_sql => '
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
WHERE b.user_id = #{id}
GROUP BY b.user_id
'
end
Edit:
It looks like the has_one association does not support the finder_sql option.
You can easily achieve what you want by using a combination of scope/class methods
class User < ActiveRecord::Base
def self.include_ticket_counts
joins(
%{
LEFT OUTER JOIN (
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
GROUP BY b.user_id
) a ON a.user_id = users.id
}
).select("users.*, COALESCE(a.tickets_count, 0) AS tickets_count")
end
end
Now
User.include_ticket_counts.where(:id => [1,2,3]).each do |user|
p user.tickets_count
end
This solution has performance implications if you have millions of rows in the tickets table. You should consider filtering the JOIN result set by providing WHERE to the inner query.
You can simply use for a particular user:
user.tickets.count
Or if you want this value automatically cached by Rails.
Declare a counter_cache => true option in the other side of the association
class ticket
belongs_to :user, :counter_cache => true
end
You also need a column in you user table named tickets_count.
With this each time you add a new tickets to a user rails will update this column so when you ftech your user record you can simply accs this column to get the ticket count without additional query.
Not pretty, but it works:
users = User.joins("LEFT JOIN tickets ON users.id = tickets.user_id").select("users.*, count(tickets.id) as ticket_count").group("users.id")
users.first.ticket_count
What about adding a method in the User model that does the query?
You wouldn't be modifying the table structure, or you can't modify that either?
How about adding a subselect scope to ApplicationRecord:
scope :subselect,
lambda { |aggregate_fn, as:, from:|
query = self.klass
.select(aggregate_fn)
.from("#{self.table_name} _#{self.table_name}")
.where("_#{self.table_name}.id = #{self.table_name}.id")
.joins(from)
select("(#{query.to_sql}) AS #{as}")
}
Then, one might use the following query:
users = User.select('users.*').subselect('COUNT(*)', as: :tickets_count, from: :tickets)
users.first.ticket_count
# => 5
Related
I'm trying to use active-record query possible connections between airports.
I described the models I created already in another question here:
n:m self-join with ruby on rails active record
Basically, what I can do now is that:
ny = Airport.create({"city" => "New York"})
la = Airport.create({"city" => "Los Angeles"})
ny.destinations << la
la.destinations << ny
I ran into an issue querying the data I'm looking for, which is quite simple in SQL but I had no luck with active record yet.
ny = Airport.where('city = ?', 'New York')
ny.destinations
returns the correct objects, but all of them.
The SQL query looks like that:
SELECT "airports".* FROM "airports" INNER JOIN "connections" ON "airports"."id" = "connections"."destination_id" WHERE "connections"."airport_id" = 3
I'd like to filter those results by cities starting with "s" for example, so an SQL query could look like that:
SELECT "airports".* FROM "airports" INNER JOIN "connections" ON "airports"."id" = "connections"."destination_id" WHERE "connections"."airport_id" = 3 AND airports"."city" LIKE "s%"
I tried it this way:
ny.destinations.where('city LIKE ?', '#{params[:query]}%')
But I always get an empty result.
How could I use active record to filter my resulting objetcs?
edit: Thats the best solution I found so far:
I added the cityLike() method to the Airport model:
app/models/airport.rb:
class Airport < ActiveRecord::Base
attr_accessible :city, :name
has_many :connections
has_many :destinations, :through => :connections
has_many :inverse_connections, :class_name => "Connection", :foreign_key => "destination_id"
has_many :inverse_destinations, :through => :inverse_connections, :source => :airport
def self.cityLike(query)
where("city LIKE ?", "%#{query}%")
end
end
app/model/connection.rb:
class Connection < ActiveRecord::Base
attr_accessible :destination_id, :airport_id
belongs_to :airport
belongs_to :destination, :class_name => "Airport"
end
Now I can query the objects with the following statement:
Airport.find(1).destinations.cityLike("a")
Not sure if it's the best solution, but it produces the query I was looking for.
Thanks a lot to all af you!
ActiveRecord::Base.execute(sql) lets you use pure SQL to do your query and returns the relevant model.
What about this?
Airport.find(:all, joins: "INNER JOIN `connections` ON airports.id = connections.destination_id").where("connections.airport_id = ? AND airports.city LIKE ?", ny_id, "s%")
This code:
ny.destinations.where('city LIKE ?', '#{params[:query]}%')
works like this--first you have an object ny repesenting the city of New York. When you say ".destinations" you have now followed a relation you defined in your model to retrieve all the destinations that you can get to from New York. However, if I'm imagining your database schema correctly, these destinations don't actually have a field called "city"; instead, they have a destination_id, which ties the destination to a particular airport, and it's the airport that has a city associated with it.
So when you query the destination table for 'city LIKE ?', it doesn't find any matching records.
Instead, try
ny.destinations.joins(:airports).where('city LIKE ?', '#{params[:query]}%')
I am trying to get a list, and I will use books as an example.
class Book < ActiveRecord::Base
belongs_to :type
has_and_belongs_to_many :genres
end
class Genre < ActiveRecord::Base
has_and_belongs_to_many :books
end
So in this example I want to show a list of all Genres, but it the first column should be the type. So, if say a genre is "Space", the types could be "Non-fiction" and "Fiction", and it would show:
Type Genre
Fiction Space
Non-fiction Space
The Genre table has only "id", "name", and "description", the join table genres_books has "genre_id" and "book_id", and the Book table has "type_id" and "id". I am having trouble getting this to work however.
I know the sql code I would need which would be:
SELECT distinct genres.name, books.type_id FROM `genres` INNER JOIN genres_books ON genres.id = genres_books.genre_id INNER JOIN books ON genres_books.book_id = books.id order by genres.name
and I found I could do
#genre = Genre.all
#genre.each do |genre|
#type = genre.book.find(:all, :select => 'type_id', :group => 'type_id')
#type.each do |type|
and this would let me see the type along with each genre and print them out, but I couldn't really work with them all at once. I think what would be ideal is if at the Genre.all statement I could somehow group them there so I can keep the genre/type combinations together and work with them further down the road. I was trying to do something along the lines of:
#genres = Genre.find(:all, :include => :books, :select => 'DISTINCT genres.name, genres.description, books.product_id', :conditions => [Genre.book_id = :books.id, Book.genres.id = :genres.id] )
But at this point I am running around in circles and not getting anywhere. Do I need to be using has_many :through?
The following examples use your models, defined above. You should use scopes to push associations back into the model (alternately you can just define class methods on the model). This helps keep your record-fetching calls in check and helps you stick within the Law of Demeter.
Get a list of Books, eagerly loading each book's Type and Genres, without conditions:
def Book < ActiveRecord::Base
scope :with_types_and_genres, include(:type, :genres)
end
#books = Book.with_types_and_genres #=> [ * a bunch of book objects * ]
Once you have that, if I understand your goal, you can just do some in-Ruby grouping to corral your Books into the structure that you need to pass to your view.
#books_by_type = #books.group_by { |book| book.type }
# or the same line, more concisely
#books_by_type = #books.group_by &:type
#books_by_type.each_pair do |type, book|
puts "#{book.genre.name} by #{book.author} (#{type.name})"
end
I am using a join query to get the attributes of another table along with the query.
city = City.first(:select => "cities.*, states.name as state_name, states.time as state_time"
:joins => "LEFT JOIN states on cities.state_id = states.id",
:conditions => ["states.name = ?", params[:state]])
Here, the problem is that when I get the values from the joined tables like city.state_time, I will get the string like 2010-11-12 05:00:00 instead of the time object(no typecasting is done by Rails for these fields). It makes sense since I am calling City model and the methods used for typecasting time column will be in State model. I will have to explicitly parse time like this and will have to fight with the time zone issues as well. (as Rails do some customizations while giving the Time object and I will have to do these for these columns). Is there any way to link the columns to the State while doing the join. One method I thought of was like this.
state = State.new(:name => city.state_name, :time => city.state_time)
and use state.name and state.time. Is there a better way?
here's probably what you want:
class City < ActiveRecord::Base
belongs_to :state
end
class State < ActiveRecord::Base
has_many :cities
end
a = City.joins(:state).includes(:state).where(['states.name = ?', params[:state]]).first
a.state.time
This works using an inner join and has some conditions:
City must belong to only one state. If the city doesn't belong to any state the query won't return it because of the inner join
Rails 2 Syntax
a = City.find(:all, :conditions => ['states.name = ?', params[:state]], :joins => :state, :include => :state)
I have the following models. Users have UserActions, and one possible UserAction can be a ContactAction (UserAction is a polymorphism). There are other actions like LoginAction etc. So
class User < AR::Base
has_many :contact_requests, :class_name => "ContactAction"
has_many :user_actions
has_many_polymorphs :user_actionables, :from => [:contact_actions, ...], :through => :user_actions
end
class UserAction < AR::Base
belongs_to :user
belongs_to :user_actionable, :polymorphic => true
end
class ContactAction < AR::Base
belongs_to :user
named_scope :pending, ...
named_scope :active, ...
end
The idea is that a ContactAction joins two users (with other consequences within the app) and always has a receiving and a sending end. At the same time, a ContactAction can have different states, e.g. expired, pending, etc.
I can say #user.contact_actions.pending or #user.contact_requests.expired to list all pending / expired requests a user has sent or received. This works fine.
What I would now like is a way to join both types of ContactAction. I.e. #user.contact_actions_or_requests. I tried the following:
class User
def contact_actions_or_requests
self.contact_actions + self.contact_requests
end
# or
has_many :contact_actions_or_requests, :finder_sql => ..., :counter_sql => ...
end
but all of these have the problem that it is not possible to use additional finders or named_scopes on top of the association, e.g. #user.contact_actions_or_requests.find(...) or #user.contact_actions_or_requests.expired.
Basically, I need a way to express a 1:n association which has two different paths. One is User -> ContactAction.user_id, the other is User -> UserAction.user_id -> UserAction.user_actionable_id -> ContactAction.id. And then join the results (ContactActions) in one single list for further processing with named_scopes and/or finders.
Since I need this association in literally dozens of places, it would be a major hassle to write (and maintain!) custom SQL for every case.
I would prefer to solve this in Rails, but I am also open to other suggestions (e.g. a PostgreSQL 8.3 procedure or something simliar). The important thing is that in the end, I can use Rails's convenience functions like with any other association, and more importantly, also nest them.
Any ideas would be very much appreciated.
Thank you!
To provide a sort-of answer to my own question:
I will probably solve this using a database view and add appropriate associations as needed. For the above, I can
use the SQL in finder_sql to create the view,
name it "contact_actions_or_requests",
modify the SELECT clause to add a user_id column,
add a app/models/ContactActionsOrRequests.rb,
and then add "has_many :contact_actions_or_requests" to user.rb.
I don't know how I'll handle updating records yet - this seems not to be possible with a view - but maybe this is a first start.
The method you are looking for is merge. If you have two ActiveRecord::Relations, r1 and r2, you can call r1.merge(r2) to get a new ActiveRecord::Relation object that combines the two.
If this will work for you depends largely on how your scopes are set up and if you can change them to produce a meaningful result. Let's look at a few examples:
Suppose you have a Page model. It has the normal created_at and updated_at attributes, so we could have scopes like:
:updated -> { where('created_at != updated_at') }
:not_updated -> { where('created_at = updated_at') }
If you pull this out of the database you'll get:
r1 = Page.updated # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at)
r2 = Page.not_updated # SELECT `pages`.* FROM `pages` WHERE (created_at = updated_at)
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at) AND (created_at = updated_at)
=> []
So it did combine the two relations, but not in a meaningful way. Another one:
r1 = Page.where( :name => "Test1" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test1'
r2 = Page.where( :name => "Test2" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
So, it might work for you, but maybe not, depending on your situation.
Another, and recommended, way of doing this is to create a new scope on you model:
class ContactAction < AR::Base
belongs_to :user
scope :pending, ...
scope :active, ...
scope :actions_and_requests, pending.active # Combine existing logic
scope :actions_and_requests, -> { ... } # Or, write a new scope with custom logic
end
That combines the different traits you want to collect in one query ...
Have Addresses and Lists with many-to-many relationship, as shown below.
Sometimes need all the Lists an Address is not in.
Using the find_by_sql query shown, and it works great. But is there a way to do it without using direct SQL?
class List
has_many :address_list_memberships
has_many :addresses, :through => :address_list_memberships
end
class Address
has_many :address_list_memberships, :dependent => :destroy
has_many :lists, :through => :address_list_memberships
# Lists that this Address is not in
def Address.lists_not_in(address_id)
sql = %Q|
SELECT
l.*
FROM
lists l
WHERE
l.id
NOT IN
(
SELECT
l.id
FROM
addresses a, lists l, address_list_memberships alm
WHERE
a.id = alm.address_id AND l.id = alm.list_id
AND
a.id = #{address_id}
)
|
List.find_by_sql(sql)
end
end
I would do this as a scope in List
class List
named_scope :without_address, lambda { |address_id| { :joins => 'inner join address_list_memberships alm on alm.list_id = lists.id', :conditions => ['alm.address_id <> ?', address_id]}}
end
Now you can call List.without_address(4), and you can call scopes on top of that.
As Matchu points out, you can do it without writing out the join SQL:
class List
named_scope :without_address, lambda { |address_id| { :joins => :address_list_memberships, :conditions => ['address_list_memberships.address_id <> ?', address_id]}}
end
And make sure your join table has indices!
In a migration:
add_index "address_list_memberships", "address_id"
add_index "address_list_memberships", "list_id"
For other ways you can format the named_scope, see Sam Saffron's gist: http://gist.github.com/162489
WHERE (address_list_memberships.address_id <> 13896)
is going to be expensive on a database with 21849 Addresses and 1483 Lists.
Flip your logic:
def lists_not_in
List.all - self.lists
end
That way you are only subtracting one array from another instead of checking each record in the database to see if it's in the list.
You are not going to get the flexibility you get with direct SQL from ActiveRecord, in particular, it is not going to be possible for you to craft the not in clause in active record.
If you want to get a little bit more control you could try using Sequel http://sequel.rubyforge.org/ or just hand crafting.
Note, the solution you have is risky cause you are allowing for a sql injection. (a.id = #{address_id})