Associations of records of one table are in another - sql

I have:
Two database tables:
Users: id, title
Infos: id, type, user_id_createdby, user_id_addressedto, text
in Infos I have records of setting a dates of meetings between users:
Infos record: id: 1, type: "nextmeeting", user_id_createdby: 47, user_id_addressedto: 51, text: "2011/01/13"
beside "nextmeeting" I have other types of data between users as well
while a User logged in I'm showing him a list of users with whom he has a meetings by collecting: unique user_id_addressedto and current_user => user_id_createdby
from Info to array #repilents and then User.find(:all, :conditions => {:id => #repilents})
Question:
How I can sort list of Users by dates from Infos.text where type: "nextmeeting"?
like:
User 4 - 2011/01/05
User 8 - 2011/01/13
User 2 - 2011/01/21
User 5 - Next meeting not defined
User 3 - Next meeting not defined

If you're sure that there will only be one row in Infos that has the type "nextmeeting", you can use a left outer join to link users to meetings:
#users = User.joins("LEFT OUTER JOIN infos ON infos.user_id_createdby = users.id")
To order by the descending date:
#users = User.joins("LEFT OUTER JOIN infos ON infos.user_id_createdby = users.id").order("infos.text DESC")
Now some random comments:
type is a reserved word and you shouldn't use it as a column name (http://stackoverflow.com/questions/2293618/rails-form-not-saving-type-field-newbie)
storing dates as text is going to make your life difficult
You might want to rethink this generic "infos" column and make a distinct Meeting model.

Related

Rails, query with Active record does not take into account negative where condition

I have the following data model :
user has_many organization_users
user has_many registrations
a user has a column type
a registration has columns learning_item_type and learning_item_id
The goal of the query is to retrieve all users from a specific organization that dont have registrations to a specific learning item (combinaison of learning_item_type and learning_item_id), it must include users from the organizations that don't have registrations at all (left join)
I came up with this with active record query :
User
.joins(:organizations_users)
.left_joins(:registrations)
.where(
type: 'Collaborator',
'organizations_users.organization_id': organization.id
)
.where.not(
'registrations.learning_item_id': learning_item.id,
'registrations.learning_item_type': learning_item.class.to_s
).distinct
which in raw sql looks like :
"SELECT DISTINCT \"users\".* FROM \"users\" INNER JOIN \"organizations_users\" ON \"organizations_users\".\"user_id\" = \"users\".\"id\" LEFT OUTER JOIN \"registrations\" ON \"registrations\".\"account_id\" = 28 AND \"registrations\".\"user_id\" = \"users\".\"id\" WHERE \"users\".\"account_id\" = 28 AND \"users\".\"type\" = 'Collaborator' AND \"organizations_users\".\"organization_id\" = 1 AND NOT (\"registrations\".\"learning_item_id\" = 10164 AND \"registrations\".\"learning_item_type\" = 'Session')"
I can't figure out what's wrong with this query but it keeps returning users who actually have a registration with learning_item_id = 10164 and learning_item_type 'Session'.
Why is that negative NOT criteria not taken into account here ?
User.left_joins(:registrations)
After the join you get something like this:
users.id
registrations.learning_item_id
1
10164
1
10165
2
10166
Then filter out learning item 10164:
User.left_joins(:registrations)
.where.not("registrations.learning_item_id": 10164)
Result is:
users.id
registrations.learning_item_id
1
10165
2
10166
The returned sql result is correct, but you still get User#1 that is registered to LearningItem#10164.
What you need is to filter out the user:
User.where.not(
id: User.joins(:registrations).where("registrations.learning_item_id": 10164)
)
users.id
registrations.learning_item_id
2
10166

Rails Select one random listings for premium users

In my rails app, I have Users and Listings. The Listings belong to a User. Listing has user_id and its filled with users id who is creating the listing.
A user can be a premium user, gold user or silver user.
What I want is for each premium user, select one random listing to show in premium listings.
I can do it in O(n**2) time or n+1 query as follow:
users_id = User.where(:role => "premium").pluck[:id]
final_array = Array.new
users_id.each do |id|
final_array << Listing.where(:user_id => id).sample(1)
end
final_array
Is there a better way of doing this?
You could try this:
listings = Listing.select(
<<~SQL
DISTINCT ON (users.id) users.id,
listings.*,
row_number() OVER (PARTITION BY users.id ORDER BY random())
SQL
)
.joins(:user)
.includes(:user)
.where(users: { role: :premium })
It gives a random Listing for every premium user.
It produces the only request to db and also it won't make an extra request for getting listing's user, so you are free to do something like this:
listings.each do |listing|
p listing.user
end
random_user_listings = []
User.includes(:listings).where(role: "premium").find_each do |user|
random_user_listings << user.listings.sample(1)
end
random_user_listings
To avoid N+1 query you need to combine them, perform query one time like this:
list = Listing.includes(:user).where(:role => "premium").sample(1)
Feel free to deal with list instead of Listing. Because now you're dealing with variable, not Query.
ids = list.pluck(:user_id).uniq
Getting array of ids like above and doing further steps as you did (but with list, not Listing)
Need to be noticed that, when you deal with Model you're dealing with QUERY. Avoiding doing that in loop statement.

Count total number of objects in list ordered by the number of associated objects

I have two models
class User
has_many :subscriptions
end
and
class Subscription
belongs_to :user
end
one one of my pages I would like to display a list of all users ordered by the number of subscriptions each user has. I am not to good with sql queries but I think that
list = Users.all.joins(:subscriptions).group("user.id").order("count(subscriptions.id) DESC")
dose the job. Now to my problem, when I try to count the total number of objects in list, using list.count, I get a hash with user.id and subscription count, like this
{11 => 5,
8 => 7,
1 => 11,
...}
not the total number of users in list.. .count works fine if I have a list sorted by for example user name (which is in the user table). I would really like to use .count since it in a module for pagination thats in a gem but any ideas is great!
Thanks!
We can just use a single query to finish this:
User.joins("LEFT OUTER JOIN ( SELECT user_id, COUNT(*) as num_subscriptions
FROM subscriptions
GROUP BY user_id
) AS temp
ON temp.user_id = users.id")
.order("temp.num_subscriptions DESC")
Basically, my idea is to try to query the number of subscription for each user_id in the subquery, then join with User. I used LEFT OUTER JOIN, because there will be several users which don't have any subscriptions
Improve option: You can define a scope inside User, it would be more beautiful for later usage:
user.rb
class User < ActiveRecord::Base
has_many :subscriptions
scope :sorted_by_num_subscriptions, -> {
joins("LEFT OUTER JOIN ( SELECT user_id, COUNT(*) as num_subscriptions
FROM subscriptions
GROUP BY user_id
) AS temp
ON temp.user_id = users.id")
.order("temp.num_subscriptions DESC")
}
end
Then just use it:
User.sorted_by_num_subscriptions
When grouping, the count method changes it's behavior and indeed, instead of returning the total count of records, it returns a hash of the counts for each group (see the docs for more info). So what you get with list.count is simply a hash of the subscription counts for each user.
So, your query is correct and all you need is to sum up the individual counts in the groups. This can be done simply by:
total_count = list.count.values.sum
If it is the pagination code that calls just a bare count that makes the issue, usually the pagination code is able to accept a parameter with total count. For example, will_paginate accepts the total_entries parameter, so you should be able to pass it the total count like this:
list.paginate(page: 2, total_entries: list.count.values.sum)

Rails ActiveRecord Join Query With conditions

I have following SQL Query:
SELECT campaigns.* , campaign_countries.points, offers.image
FROM campaigns
JOIN campaign_countries ON campaigns.id = campaign_countries.campaign_id
JOIN countries ON campaign_countries.country_id = countries.id
JOIN offers ON campaigns.offer_id = offers.id
WHERE countries.code = 'US'
This works perfectly well. I want its rails active record version some thing like:
Campaign.includes(campaign_countries: :country).where(countries: {code: "US"})
Above code runs more or less correct query (did not try to include offers table), issue is returned result is collection of Campaign objects so obviously it does not include Points
My tables are:
campaigns --HAS_MANY--< campaign_countries --BELONGS_TO--< countries
campaigns --BELONGS_TO--> offers
Any suggestions to write AR version of this SQL? I don't want to use SQL statement in my code.
I some how got this working without SQL but surely its poor man's solution:
in my controller I have:
campaigns = Campaign.includes(campaign_countries: :country).where(countries: {code: country.to_s})
render :json => campaigns.to_json(:country => country)
in campaign model:
def points_for_country country
CampaignCountry.joins(:campaign, :country).where(countries: {code: country}, campaigns: {id: self.id}).first
end
def as_json options={}
json = {
id: id,
cid: cid,
name: name,
offer: offer,
points_details: options[:country] ? points_for_country(options[:country]) : ""
}
end
and in campaign_countries model:
def as_json options={}
json = {
face_value: face_value,
actual_value: actual_value,
points: points
}
end
Why this is not good solution? because it invokes too many queries:
1. It invokes query when first join is performed to get list of campaigns specific to country
2. For each campaign found in first query it will invoke one more query on campaign_countries table to get Points for that campaign and country.
This is bad, Bad and BAD solution. Any suggestions to improve this?
If You have campaign, You can use campaign.campaign_countries to get associated campaign_countries and just get points from them.
> campaign.campaign_countries.map(&:points)
=> [1,2,3,4,5]
Similarly You will be able to get image from offers relation.
EDIT:
Ok, I guess now I know what's going on. You can use joins with select to get object with attached fields from join tables.
cs = Campaign.joins(campaign_countries: :country).joins(:offers).select('campaigns.*, campaign_countries.points, offers.image').where(countries: {code: "US"})
You can than reference additional fields by their name on Campaign object
cs.first.points
cs.first.image
But be sure, that additional column names do not overlap with some primary table fields or object methods.
EDIT 2:
After some more research I came to conclusion that my first version was actually correct for this case. I will use my own console as example.
> u = User.includes(:orders => :cart).where(:carts => { :id => [5168, 5167] }).first
> u.orders.length # no query is performed
=> 2
> u.orders.count # count query is performed
=> 5
So when You use includes with condition on country, in campaign_countries are stored only campaign_countries that fulfill Your condition.
Try this:
Campaign.joins( [{ :campaign_countries => :countries}, :offers]).where('`countries`.`code` = ?', "US")

SQL order by list

Here is part of code for favourited wallpapers:
...
$profile = mysql_fetch_array(mysql_query("SELECT * FROM users WHERE id = $id"));
}
if ($profile['favourites'] != '') {
$from = (($page * $template['fav_wallpaper_limit']) - $template['fav_wallpaper_limit']);
$favourites = substr($profile['favourites'], 2);
/// Tried to join 2 tables, but favourites still displayed by wallpaper id
$sql = mysql_query("
SELECT
*
FROM
wallpapers AS w
JOIN favourites AS f on f.wallpaper_id = w.id
WHERE
w.id IN ($favourites) AND w.published = 1
ORDER BY
f.wallpaper_id LIMIT $from, $template[fav_wallpaper_limit]");
");
Problem is, that it displays wallpapers by the id column that is stored in wallpapers table. While I need to display them by how they wore favourited. The data is stored in users table, and have column favourites for each user with id list of favourited wallpapers.
EXAMPLE:
, 90, 2031, 1, 34, 460, 432, ..., 2013;
Is there any way do grab this tada and order favourites from it?
I think you need to do this within your PHP code:
Read the value of the favourites column;
Explode it into an array;
Iterate through the array, querying the database to get the favourites in the specified order.
The usual way to do this kind of thing is to have a seperate table, say user_favourites with a row for each fovourite for each user that just includes the user id and the favourite id - in this case, with an order factor as well. With the database set up this way, your can execute a query on the new user_favourites table, where user_id is the user id, ordered by the "order factor" to get the favourites in the right order all in one go.
Which database are you using? You might be able to do something like
SELECT _whatever_
FROM favourites
WHERE favourite_id IN (SELECT favourites FROM users)
and it might return the favourites in the correct order. I think the additional table approach is superior, if you can do it that way.