In an ecommerce shop application I would like to retrieve all orders that match a first_name that was entered via a search form and where paid == true. The search form submits the search term via params Parameters: {"utf8"=>"✓", "search"=>"john", "commit"=>"Search"}. In the controller
#users = User.search(params[:search]) #returns all users with the matching first_name, e.g. 'john'
#order = Order.where('user_id = ? AND paid = ?', #users.ids, true )
The query in #order works just fine, if only one user is returned, e.g. only one user is named john. But if multiple users are named John, multiple user ids are returned and the error message ActiveRecord::StatementInvalid is returned. My understanding is that the query stops working once `#users.ids is an array with more than one value.
How do I structure the following query: for each user_id return all orders (user.orders) where paid equals true.
Models
user.rb
has_many :orders
order.rb
belongs_to :users
There are various ways to go about this.
You can use a JOIN as Nic Nilov suggests but this can be difficult if your #users query is built using scopes and you don't want to manually inline those scopes.
You could also use a subquery since ActiveRecord in Rails4 is smart enough do The Right Thing when you use a relation in a where, you just have to use the hash form of where:
#users = User.search(params[:search])
# #users should be a User::ActiveRecord_Relation now.
#orders = Order.where(:user_id => #users, :paid => true)
This will end up with SQL like:
select *
from orders
where paid = 't'
and user_id in (
select id
from users
where /* whatever `search` does... */
)
The advantage here is that you don't need to know what User.search does as long as it is returning an ActiveRecord relation.
If your #users is actually an array (of ids or whole User instances) then you'd do it exactly the same way:
# Suppose #users is an array of Users or an array of User ids...
#orders = Order.where(:user_id => #users, :paid => true)
and ActiveRecord will figure out what to do with your #users array without you having to do anything extra.
Instead of two queries, you should use a nested query with a WHERE IN clause
SELECT * from Order WHERE user_id IN (SELECT user_id FROM users WHERE first_name LIKE ?) AND paid = true
This should do:
Order.joins(:user).where(users: { name: params[:search] }, paid: true)
It generates a single query with an INNER JOIN:
SELECT "orders".*
FROM "orders"
INNER JOIN "users" ON "users"."id" = "orders"."user_id"
WHERE "users"."name" = 'Test User' AND "orders"."paid" = 't'
Related
I'm using Rails 4.2 and PostgreSQL 9.4.
I have a basic users, reservations and events schema.
I'd like to return a list of users and the most recent event they attended, along with what date/time this was at.
I've created a query that returns the user and the time of the most recent event. However I need to return the events.id as well.
My application does not allow a user to reserve two events with the same start time, however I appreciate SQL does not know anything about this and thinks there can be multiple events in the result. Hence I am happy for the query to return an appropriate event ID at random in the case of a hypothetical 'tie' for events.starts_at.
User.all.joins(reservations: :event)
.select('users.*, max(events.starts_at)')
.where('reservations.state = ?', "attended")
.where('events.company_id = ?', 1)
.group('users.id')
The corresponding SQL query is:
SELECT users.*, max(events.starts_at) FROM "users" INNER JOIN "reservations" ON "reservations"."user_id" = "users"."id" INNER JOIN "events" ON "events"."id" = "reservations"."event_id" WHERE (reservations.state = 'attended') AND (events.company_id = 1) GROUP BY users.id
The reservations table is very large so loading the entire set into Rails and processing it via Ruby code is undesirable. I'd like to perform the entire query in SQL if it is possible to do so.
My basic model:
User
has_many :reservations
Reservation
belongs_to :user
belongs_to :event
Event
belongs_to :company
has_many :reservations
The generic sql that returns data for the most recent event looks like this:
select yourfields
from yourtables
join
(select someField
, max(datetimefield) maxDateTime
from table1
where whatever
group by someField ) temp on table1.someField = temp.somefield
and table1.dateTimeField = maxDateTime
where whatever
The two "where whatever" things should be the same. All you have to do is adapt this construct into your app. You might consider putting the query into a stored procedure which you then call from your app.
I think your query should focus first to retrieve the most recent reservation.
SELECT MAX(`events.starts_at`),`events"."id`,`user_id` FROM `reservations` WHERE (reservations.state = 'attended')
Then JOIN the Users and Events.
Assuming the results will include every User and Event it may be more efficient to retrieve all users and events and store then in two arrays keyed by id.
The logic behind that is rather than a separate lookup into the user and events table for each resulting reservation by the db engine, it is more efficient to get them all in a single query.
SELECT * FROM Users' WHERE 1 ORDER BYuser_id`
SELECT * FROM Events' WHERE 1 ORDER BYevent_id`
I am not familiar with Rails syntax so cannot give exact code but can show using it in PHP code, the results are put into the array with a single line of code.
while ($row = mysql_fetch_array($results, MYSQL_NUM)){users[$row(user_id)] = $row;}
Then when processing the Reservations you get the user and event data from the arrays.
The Index for reservations is critical and may be worth profiling.
Possible profile choices may be to include and exclude 'attended' in the Index. The events.starts_at should be the first column in the index followed by user_id. But profiling the Index's column order should be profiled.
You may want to use a unique Index to enforce the no duplicate reservations times.
I have an Adventure model, which is a join table between a Destination and a User (and has additional attributes such as zipcode and time_limit). I want to create a query that will return me all the Destinations where an Adventure between that Destination and the User currently trying to create an Adventure does not exist.
The way the app works when a User clicks to start a new Adventure it will create that Adventure with the user_id being that User's id and then runs a method to provide a random Destination, ex:
Adventure.create(user_id: current_user.id) (it is actually doing current_user.adventures.new ) but same thing
I have tried a few things from writing raw SQL queries to using .joins. Here are a few examples:
Destination.joins(:adventures).where.not('adventures.user_id != ?'), user.id)
Destination.joins('LEFT OUTER JOIN adventure ON destination.id = adventure.destination_id').where('adventure.user_id != ?', user.id)
The below should return all destinations that user has not yet visited in any of his adventures:
destinations = Destination.where('id NOT IN (SELECT destination_id FROM adventures WHERE user_id = ?)', user.id)
To select a random one append one of:
.all.sample
# or
.pluck(:id).sample
Depending on whether you want a full record or just id.
No need for joins, this should do:
Destination.where(['id not in ?', user.adventures.pluck(:destination_id)])
In your first attempt, I see the problem to be in the usage of equality operator with where.not. In your first attempt:
Destination.joins(:adventures).where.not('adventures.user_id != ?'), user.id)
you're doing where.not('adventures.user_id != ?'), user.id). I understand this is just the opposite of what you want, isn't it? Shouldn't you be calling it as where.not('adventures.user_id = ?', user.id), i.e. with an equals =?
I think the following query would work for the requirement:
Destination.joins(:adventures).where.not(adventures: { user_id: user.id })
The only problem I see in your second method is the usage of destinations and adventures table in both join and where conditions. The table names should be plural. The query should have been:
Destination
.joins('LEFT OUTER JOIN adventures on destinations.id = adventures.destination_id')
.where('adventures.user_id != ?', user.id)
ActiveRecord doesn't do join conditions but you can use your User destinations relation (eg a has_many :destinations, through: adventures) as a sub select which results in a WHERE NOT IN (SELECT...)
The query is pretty simple to express and doesn't require using sql string shenanigans, multiple queries or pulling back temporary sets of ids:
Destination.where.not(id: user.destinations)
If you want you can also chain the above realation with additional where terms, ordering and grouping clauses.
I solved this problem with a mix of this answer and this other answer and came out with:
destination = Destination.where
.not(id: Adventure.where(user: user)
.pluck(:destination_id)
)
.sample
The .not(id: Adventure.where(user: user).pluck(:destination_id)) part excludes destinations present in previous adventures of the user.
The .sample part will pick a random destination from the results.
I have a has_many (through accounts) association between the model User and the model Player.
I would like to know what is the best way to query all the users, and for each returned record get the associated players usernames attributes (in comma separated values).
So, if for example, a User named 'John' is associated with 3 players with usernames 'john_black', 'john_white', 'john_yellow'. I would like the query to return not just the User attributes but also an attribute called, for example, player_username, that would have this value: john_black, john_white, john_yellow.
I have tried the following Arel query in the User model:
select(`users`.*).select("GROUP_CONCAT(`players`.`username` SEPARATOR ',') AS comma_username").joins(:user)
.joins(:accounts => :player )
Which gives me the following SQL:
SELECT `users`.*, GROUP_CONCAT(`players`.`username` SEPARATOR ',') AS comma_username FROM `users` INNER JOIN `accounts` ON `accounts`.`user_id` = `users`.`id` INNER JOIN `players` ON `players`.`id` = `accounts`.`player_id`
If I execute this in MySQL console it works, but if I try to fetch from the model, I don't get the comma separated values.
What am I missing?
I believe ActiveRecord maps all columns retrieved by the SQL query with all attributes you have on your model, which in most of the cases are exactly the same. Maybe if you create a virtual accessor on your model, ActiveRecord could map your virtual column to the virtual attribute.
class User
attr_accessible :player_username
...
Give it a try and see if this works.
I have 2 models
class User < AR
has_many :friends
end
class Friend < AR
# has a name column
end
I need to find all Users who are Friends with both 'Joe' and 'Jack'
Any idea how i can do this in rails?
One option is to put each of the names as arguments for individual INNER JOINS. In SQL it would be something like this:
SELECT users.* FROM users
INNER JOIN friends AS f1
ON users.id = f1.user_id
AND f1.name = 'Joe'
INNER JOIN friends AS f2
ON users.id = f2.user_id
AND f2.name = 'Jack'
Since it is INNER JOINS, it will only display results where the users table can be joined with both f1 and f2.
And to use it in Rails, maybe do it something like this:
class User < AR
has_many :friends
def self.who_knows(*friend_names)
joins((1..friend_names.length).map{ |n|
"INNER JOIN friends AS f#{n} ON users.id = f#{n}.user_id AND f#{n}.name = ?" }.join(" "),
*friend_names)
})
end
end
Which you then can call like this:
#users = User.who_knows("Joe", "Jack")
Possible way: User.all(:joins => :friends, :conditions => ["friends.name IN (?,?)", "Joe", "Jack"], :group => "users.id") and then iterate over the array to find users with 2 friends.
This is the best solution i got when tried to solve similar problem for myself. If you find the way to do it in pure sql or ActiveRecord – let me know please!
Although using hard-coded SQL as suggested by DanneManne will most often work, and is probably the way you'd want to go, it is not necessarily composable. As soon as you have hard-coded a table name, you can run into problems combining that into other queries where ActiveRecord may decide to alias the table.
So, at the cost of some extra complexity, we can solve this using some ARel as follows:
f = Friend.arel_table
User.
where(:id=>f.project(:user_id).where(f[:name].eq('Joe'))).
where(:id=>f.project(:user_id).where(f[:name].eq('Jack')))
This will use a pair of subqueries to do the job.
I'm fairly certain there's an ARel solution using joins as well, but and I can figure out how to compose that query in ARel, just not how to then use that query as the basis for an ActiveRecord query to get back User model instances.
UPDATE: So thanks to #Erwin Brandstetter, I now have this:
def self.unique_users_by_company(company)
users = User.arel_table
cards = Card.arel_table
users_columns = User.column_names.map { |col| users[col.to_sym] }
cards_condition = cards[:company_id].eq(company.id).
and(cards[:user_id].eq(users[:id]))
User.joins(:cards).where(cards_condition).group(users_columns).
order('min(cards.created_at)')
end
... which seems to do exactly what I want. There are two shortcomings that I would still like to have addressed, however:
The order() clause is using straight SQL instead of Arel (couldn't figure it out).
Calling .count on the query above gives me this error:
NoMethodError: undefined method 'to_sym' for
#<Arel::Attributes::Attribute:0x007f870dc42c50> from
/Users/neezer/.rvm/gems/ruby-1.9.3-p0/gems/activerecord-3.1.1/lib/active_record/relation/calculations.rb:227:in
'execute_grouped_calculation'
... which I believe is probably related to how I'm mapping out the users_columns, so I don't have to manually type in all of them in the group clause.
How can I fix those two issues?
ORIGINAL QUESTION:
Here's what I have so far that solves the first part of my question:
def self.unique_users_by_company(company)
users = User.arel_table
cards = Card.arel_table
cards_condition = cards[:company_id].eq(company.id)
.and(cards[:user_id].eq(users[:id]))
User.where(Card.where(cards_condition).exists)
end
This gives me 84 unique records, which is correct.
The problem is that I need those User records ordered by cards[:created_at] (whichever is earliest for that particular user). Appending .order(cards[:created_at]) to the scope at the end of the method above does absolutely nothing.
I tried adding in a .joins(:cards), but that give returns 587 records, which is incorrect (duplicate Users). group_by as I understand it is practically useless here as well, because of how PostgreSQL handles it.
I need my result to be an ActiveRecord::Relation (so it's chainable) that returns a list of unique users who have cards that belong to a given company, ordered by the creation date of their first card... with a query that's written in Ruby and is database-agnostic. How can I do this?
class Company
has_many :cards
end
class Card
belongs_to :user
belongs_to :company
end
class User
has_many :cards
end
Please let me know if you need any other information, or if I wasn't clear in my question.
The query you are looking for should look like this one:
SELECT user_id, min(created_at) AS min_created_at
FROM cards
WHERE company_id = 1
GROUP BY user_id
ORDER BY min(created_at)
You can join in the table user if you need columns of that table in the result, else you don't even need it for the query.
If you don't need min_created_at in the SELECT list, you can just leave it away.
Should be easy to translate to Ruby (which I am no good at).
To get the whole user record (as I derive from your comment):
SELECT u.*,
FROM user u
JOIN (
SELECT user_id, min(created_at) AS min_created_at
FROM cards
WHERE company_id = 1
GROUP BY user_id
) c ON u.id = c.user_id
ORDER BY min_created_at
Or:
SELECT u.*
FROM user u
JOIN cards c ON u.id = c.user_id
WHERE c.company_id = 1
GROUP BY u.id, u.col1, u.col2, .. -- You have to spell out all columns!
ORDER BY min(c.created_at)
With PostgreSQL 9.1+ you can simply write:
GROUP BY u.id
(like in MySQL) .. provided id is the primary key.
I quote the release notes:
Allow non-GROUP BY columns in the query target list when the primary
key is specified in the GROUP BY clause (Peter Eisentraut)
The SQL standard allows this behavior, and because of the primary key,
the result is unambiguous.
The fact that you need it to be chainable complicates things, otherwise you can either drop down into SQL yourself or only select the column(s) you need via select("users.id") to get around the Postgres issue. Because at the heart of it your query is something like
SELECT users.id
FROM users
INNER JOIN cards ON users.id = cards.user_id
WHERE cards.company_id = 1
GROUP BY users.id, DATE(cards.created_at)
ORDER BY DATE(cards.created_at) DESC
Which in Arel syntax is more or less:
User.select("id").joins(:cards).where(:"cards.company_id" => company.id).group_by("users.id, DATE(cards.created_at)").order("DATE(cards.created_at) DESC")