Rails 2 :joins and :include resultset - ruby-on-rails

When fetching content from a database using activerecord, I would like to fetch a custom resultset with specified columns across two tables.
SELECT users.name, users.username, users.age, users.city_id, cities.name as city_name FROM users INNER JOIN cities ON users.city_id = cities.id
Which would be in AR as
Users.find(:all,
:joins => :cities,
:select => "users.name, users.username, users.age, users.city_id,
cities.name as city_name")
But this only returns the user table results and not the city results. I am 100% sure that the inner join statement is going through (that both tables are being joined).
It seems as if the return object only has the columns associated with the model. So UserModel would only have the columns that the users table has and won't allow to fetch the columns of the cities table even though they're specified in the select.
Should I be using :joins or :include? Any idea what's going on?

If you alias the joined column name then returned object should have an attribute by the alias name, i.e.
u = User.first( :joins => :cities,
:select => "users.*, cities.name AS city_name")
u.city_name # returns the city_name.
In your case, :joins is appropriate than :include.
I checked this in my setup and it works for me ( I am on Rails 2.3.8)

In your returned instances, if the column's name is city_name, you should be using user.city_name. Alternatively, if you use :include, you would be telling ActiveRecord to load the associated city models, which you would then reference as user.city.name.
To summarize:
users = User.find(:all, :joins => :cities, :select => "users.name, users.username, users.age, users.city_id, cities.name as city_name")
users.map(&:city_name)
users = User.find(:all, :include => :cities)
users.map(&:city).map(&:name)

you can use specific column name in user table in place of "users.*" if you dont need all column. I think its good programming practice.
u = User.first( :joins => :cities,
:select => "users.name, users.username, users.age, users.city_id, cities.name AS city_name")
u.city_name # returns the city_name.

Related

ActiveRecord Association select counts for included records

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

Data from joined tables not typecast

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)

How to sort results by count and return in order of most to least using active record in Rails

I have a searches table, which has all the searches that get run on our site. I want to pull up the most popular searches. Like say there are 130 records with the column of phrase being "cheese", how do I sort the results by count and return them in order of most to least using active record?
Taken from the example I linked in the comments above.
Searches.find(:all, :select => '*, count(*) AS count, phrase', :group => 'phrase', :order => 'count DESC')
Although I just tried this on my own sqlite db and it worked fine (rails 3)
Searches.count(:all, :group => 'phrase', :order => 'count(*) DESC')

Querying with conditions in Rails

Assume, we have two tables: "Items" and "Types". The relationships are:
item belongs_to type
type has_many items
Also, the Item table have a column, let's call it "mark". What would be a query (in a Rails secure way if it's possible) to extract all the types from them Types table, which have connected items in Items table with a "mark"?
This:
Type.find :all, :include => items, :conditions => ['items.mark = ?', somevalue]
should work.
Note: you shouldn't use Type as class name, nor :type as attribute, as this name can lead to conflicts.

Is there an ActiveRecord way to do this SQL 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})

Resources