Rails query, based on a scope from an unrelated model - ruby-on-rails

I want to find all of a user's convos where there is not a connect
I have a convos table, with a sender_id and recipient_id which are both references to a user id
# app/models/user.rb
has_many :convos, ->(user) {
unscope(:where).where("sender_id = :id OR recipient_id = :id", id: user.id)
}
Note the convo can belong to a user that is either sender_id OR recipient_id.
# app/models/convo.rb
class Convo < ApplicationRecord
belongs_to :sender, :foreign_key => :sender_id, class_name: 'User'
belongs_to :recipient, :foreign_key => :recipient_id, class_name: 'User'
has_many :msgs, dependent: :destroy
validates_uniqueness_of :sender_id, :scope => :recipient_id
scope :involving, -> (user) do
where("convos.sender_id =? OR convos.recipient_id =?",user.id,user.id)
end
scope :between, -> (sender_id,recipient_id) do
where("(convos.sender_id = ? AND convos.recipient_id =?) OR (convos.sender_id = ? AND convos.recipient_id =?)", sender_id,recipient_id, recipient_id, sender_id)
end
end
Connect table has a requestor_id and requestee_id which are both references to a user id.
Connect model
class Connect < ApplicationRecord
belongs_to :requestor, :foreign_key => :requestor_id, class_name: 'User'
belongs_to :requestee, :foreign_key => :requestee_id, class_name: 'User'
scope :between, -> (requestor_id,requestee_id) do
where("(connects.requestor_id = ? AND connects.requestee_id =?) OR (connects.requestor_id = ? AND connects.requestee_id =?)", requestor_id,requestee_id, requestee_id, requestor_id)
end
end
I want to find all of a user's convos where there is not a connect
I've tried something like:
user = User.first
user.convos.where.not(Connect.between(self.requestor_id, self.requestee_id).length > 0 )
# NoMethodError (undefined method `requestor_id' for main:Object)
user.convos.where.not(Connect.between(convo.requestor_id, convo.requestee_id).length > 0 )
# undefined local variable or method `convo' for main:Object
Then I tried without referencing a user at all, and just tried to get all convos without a connect.
Convo.where("Connect.between(? ,?) < ?)", :sender_id, :recipient_id, 1)
# ActiveRecord::StatementInvalid (SQLite3::SQLException: near "between": syntax error: SELECT "convos".* FROM "convos" WHERE (Connect.between('sender_id' ,'recipient_id') < 1)))
Convo.where("Connect.between(? ,?) < ?)", self.sender_id, self.recipient_id, 1)
# NoMethodError (undefined method `sender_id' for main:Object)
What is the best way to get all the user's convos where a connect doesn't exist?
UPDATE
This works, and is what I'm looking for, but obviously this is trashy, and I'd like to understand how get this in 1 call.
#og_connections = []
current_user.convos.each do |convo|
if Connect.between(convo.sender_id, convo.recipient_id).length === 0
#og_connections.push(current_user.id === convo.sender_id ? convo.recipient_id : convo.sender_id)
end
end
#connections = User.select(:id, :first_name, :slug).where(id: #og_connections, status: 'Active')

You can use LEFT JOIN to get the users rows where their match between id and convos.sender_id and convos.recipient_id is not NULL, but their match between connections.requester_id and connections.requestee_id is NULL:
SELECT *
FROM users
LEFT JOIN connects
ON users.id IN (connects.requester_id, connects.requestee_id)
LEFT JOIN convos
ON users.id IN (convos.sender_id, convos.recipient_id)
WHERE connects.requester_id IS NULL AND
connects.requestee_id IS NULL AND
convos.sender_id IS NOT NULL AND
convos.recipient_id IS NOT NULL
AR implementation:
User.joins('LEFT JOIN connects ON users.id IN (connects.requester_id, connects.requestee_id)
LEFT JOIN convos ON users.id IN (convos.sender_id, convos.recipient_id)')
.where(connects: { requester_id: nil, requestee_id: nil })
.where.not(convos: { sender_id: nil, recipient_id: nil })
Considering a DB structure like this:
db=# \d+ users
Table "public.users"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
------------+--------------------------------+-----------+----------+-----------------------------------+----------+--------------+-------------
id | bigint | | not null | nextval('users_id_seq'::regclass) | plain | |
name | character varying | | | | extended | |
created_at | timestamp(6) without time zone | | not null | | plain | |
updated_at | timestamp(6) without time zone | | not null | | plain | |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
db=# \d+ convos
Table "public.convos"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------------+--------------------------------+-----------+----------+------------------------------------+---------+--------------+-------------
id | bigint | | not null | nextval('convos_id_seq'::regclass) | plain | |
sender_id | integer | | | | plain | |
recipient_id | integer | | | | plain | |
created_at | timestamp(6) without time zone | | not null | | plain | |
updated_at | timestamp(6) without time zone | | not null | | plain | |
Indexes:
"convos_pkey" PRIMARY KEY, btree (id)
db=# \d+ connects
Table "public.connects"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------------+--------------------------------+-----------+----------+--------------------------------------+---------+--------------+-------------
id | bigint | | not null | nextval('connects_id_seq'::regclass) | plain | |
requestor_id | integer | | | | plain | |
requestee_id | integer | | | | plain | |
created_at | timestamp(6) without time zone | | not null | | plain | |
updated_at | timestamp(6) without time zone | | not null | | plain | |
Indexes:
"connects_pkey" PRIMARY KEY, btree (id)
With the following records:
db=# select * from users;
id | name | created_at | updated_at
----+------+----------------------------+----------------------------
1 | seb | 2019-11-27 09:59:53.762911 | 2019-11-27 09:59:53.762911
2 | sab | 2019-11-27 09:59:55.455096 | 2019-11-27 09:59:55.455096
3 | foo | 2019-11-27 10:07:19.760675 | 2019-11-27 10:07:19.760675
4 | bar | 2019-11-27 10:07:36.18696 | 2019-11-27 10:07:36.18696
5 | meh | 2019-11-27 10:07:38.465841 | 2019-11-27 10:07:38.465841
(5 rows)
db=# select * from convos;
id | sender_id | recipient_id | created_at | updated_at
----+-----------+--------------+----------------------------+----------------------------
1 | 1 | 2 | 2019-11-27 10:09:36.742426 | 2019-11-27 10:09:36.742426
2 | 1 | 3 | 2019-11-27 10:09:40.555118 | 2019-11-27 10:09:40.555118
(2 rows)
db=# select * from connects;
id | requestor_id | requestee_id | created_at | updated_at
----+--------------+--------------+----------------------------+----------------------------
1 | 1 | 2 | 2019-11-27 10:07:07.76146 | 2019-11-27 10:07:07.76146
2 | 2 | 1 | 2019-11-27 10:07:11.380084 | 2019-11-27 10:07:11.380084
3 | 1 | 4 | 2019-11-27 10:07:47.892944 | 2019-11-27 10:07:47.892944
4 | 5 | 1 | 2019-11-27 10:07:51.406224 | 2019-11-27 10:07:51.406224
(4 rows)
The following query will return only the second convo, because user with id 3 doesn't have any connect.
SELECT convos.*
FROM convos
LEFT JOIN users
ON users.id IN (convos.sender_id, convos.recipient_id)
LEFT JOIN connects
ON users.id IN (connects.requestor_id, connects.requestee_id)
WHERE connects.requestor_id IS NULL AND connects.requestee_id IS NULL
id | sender_id | recipient_id | created_at | updated_at | id | name | created_at | updated_at | id | requestor_id | requestee_id | created_at | updated_at
----+-----------+--------------+----------------------------+----------------------------+----+------+----------------------------+----------------------------+----+--------------+--------------+------------+------------
2 | 1 | 3 | 2019-11-27 10:09:40.555118 | 2019-11-27 10:09:40.555118 | 3 | foo | 2019-11-27 10:07:19.760675 | 2019-11-27 10:07:19.760675 | | | | |
(1 row)
The Rails query for that can be this:
Convo
.joins('LEFT JOIN users ON users.id IN (convos.sender_id, convos.recipient_id)
LEFT JOIN connects ON users.id IN (connects.requestor_id, connects.requestee_id)')
.where(connects: { requestor_id: nil, requestee_id: nil })

Answer with current setup
If you're looking for just current_user, you'll want to start with their convos, do a left join to connects, and select the rows where connects is NULL. With your table setup, we'll have to do this joins manually on the possible user_id combinations:
current_user.convos.joins("
LEFT JOIN connects ON
(connects.requestor_id = convos.sender_id AND connects.requestee_id = convos.recipient_id)
OR
(connects.requestor_id = convos.recipient_id AND connects.requestee_id = convos.sender_id)
").where(connects: {id: nil})
The left joins gives you any connects that are between the same two users as the convo, which is necessarily involving current_user since we started with current_user.convos. From there we filter down to only rows where the connects fields are NULL, getting us rows with a convo that does not have a matching connect.
Suggestion
That much raw SQL is a bit of code smell in a Rails app, and it's because of what we're trying to do here with the models set up as they are. I'd suggest refactoring the data models to make the queries easier. Two ideas come to mind:
Always create a symmetrical record for a connect and a convo, so you can look up by a single column instead of using all the ORs. That is, whenever you create a connect between user 1 and user 2, also create one between user 2 and user 1. More bookkeeping, since you'd have to destroy and edit them together as well. But it lets you . define simple associations without all the hoops.
Use a separate table to refer to unique user-pairs (order doesn't matter). To do this, create a UserPair model with user_1_id and user_2_id, where user_1_id is always set to the lower of the two user ids. That way, a convo can be more easily identified by a user_pair_id, a UserPair can has_many :convos and has_many: connects, and you can to a straight rails join between convos -> user_pairs -> connects.
The models in 2 would look something like
class UserPair < ApplicationRecord
belongs_to :user_1, class_name: "User"
belongs_to :user_2, class_name: "User"
before_save :sort_users
scope :between, -> (user_1_id,user_2_id) do
# records are always saved in sorted id order, so sort before querying
user_1_id, user_2_id = [user_1_id, user_2_id].sort
where(user_1_id: user_1_id, user_2_id: user_2_id)
end
# always put lowest id first for easy lookup
def sort_users
if user_1.present? && user_2.present? && user_1.id > user_2.id
self.user_1, self.user_2 = user_2, user_1
end
end
end
class Convo < ApplicationRecord
belongs_to :sender, :foreign_key => :sender_id, class_name: 'User'
belongs_to :recipient, :foreign_key => :recipient_id, class_name: 'User'
belongs_to :user_pair
before_validation :set_user_pair
scope :involving, -> (user) do
where("convos.sender_id =? OR convos.recipient_id =?",user.id,user.id)
end
# since UserPair records are always user_id sorted, we can just use
# that model's scope here without need to repeat it, using `merge`
scope :between, -> (sender_id,recipient_id) do
joins(:user_pair).merge(UserPair.between(sender_id, recipient_id))
end
def set_user_pair
self.user_pair = UserPair.find_or_initialize_by(user_1: sender, user_2: recipient)
end
end

So if I understand correctly, from the list of users a user has a conversation with, you want the list of users that they do not have a connection with.
In a simple way this could be something like:
users_conversed_with = user.convos.map{|c| [c.sender_id, c.recipient_id]}.flatten.uniq
users_connected_with = user.connections.map{|c| c.requestor_id, c.requestee_id}.flatten.uniq
Both sets also contain the user.id, but we can ignore that, because we are interested in the difference: that would be the set of people we conversed with, without connection (and because user.id will be in both, unless one of them is empty, we do not have to separately remove user.id from those sets).
users_not_connected_with = users_conversed_with - users_connected_with
This is not an optimal approach, because we do two queries, retrieve all the user-ids from the database, to then discard probably most of the retrieved data. We could improve this by creating a custom query, and let the database do the work for us, like so
sql = <<-SQL
(select distinct user_id from
(select sender_id as user_id from convos where sender_id=#{user.id} or recipient_id=#{user.id}
union
select recipient_id as user_id from convos where sender_id=#{user.id} or recipient_id=#{user.id}
)
)
except
(
(select distinct user_id from
(select requestor_id as user_id from connections where requestor_id=#{user.id} or requestee_id=#{user.id}
union
select requestee_id as user_id from convos where requestor_id=#{user.id} or requestee_id=#{user.id}
)
)
SQL
result = Convo.connection.execute(sql)
users_ids_in_convo_without_connection = result.to_a.map(&:values).flatten
But if performance is not an issue, your code has the advantage of being very readable and clearer in it's intention.

I'll first write the SQL query to do so. In your case you perhaps want
SELECT convos.*
FROM convos
WHERE (sender_id = :user_id
AND NOT EXISTS (
SELECT 1
FROM connects
WHERE (requestor_id = sender_id AND requestee_id = recipient_id) OR (requestor_id = recipient_id AND requestee_id = sender_id)
))
OR
(recipient_id = :user_id
AND NOT EXISTS (
SELECT 1
FROM connects
WHERE (requestor_id = recipient_id AND requestee_id = sender_id) OR (requestor_id = sender_id AND requestee_id = recipient_id)
))
This can be then converted into AR query.

class Convo < ApplicationRecord
def self.no_connects(user_id = nil)
q = joins('
LEFT JOIN connects ON
sender_id IN (connects.requestor_id, connects.requestee_id)
OR
recipient_id IN (connects.requestor_id, connects.requestee_id)
')
q = q.where('connects.requestor_id IS NULL AND connects.requestee_id IS NULL')
q = q.where("convos.sender_id = :user_id OR convos.recipient_id = :user_id", user_id: user_id) if user_id
q
end
end
To get all the convos without connects
Convo.no_connects
For single user
Convo.no_connects(current_user.id)

Related

Rails 5 complex query with multiple sub queries

I'm on rails 5 using postgres and I have a users table and a reports table. Users have many reports, and these reports need to be created every day. I want to fetch all of the users that are not archived, that have not completed a report today, and show yesterdays report notes if available.
Here are the models:
Users Model
# == Schema Information
#
# Table name: users
#
# id :bigint(8) not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# archived :boolean default(FALSE)
#
class User < ApplicationRecord
has_many :reports
end
Reports Model
# == Schema Information
#
# Table name: reports
#
# id :bigint(8) not null, primary key
# notes :text
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint(8)
#
class Report < ApplicationRecord
belongd_to :user
end
Here is an example of what I want from this query:
Users Table
------------------------------------------------------------------------------------
| id | name | archived | created_at | updated_at |
------------------------------------------------------------------------------------
| 1 | Jonn | false | 2018-05-11 00:01:36.124999 | 2018-05-11 00:01:36.124999 |
------------------------------------------------------------------------------------
| 2 | Sam | false | 2018-05-11 00:01:36.124999 | 2018-05-11 00:01:36.124999 |
------------------------------------------------------------------------------------
| 3 | Ashley | true | 2018-05-11 00:01:36.124999 | 2018-05-11 00:01:36.124999 |
------------------------------------------------------------------------------------
Reports Table (Imagine this report was yesterdays)
--------------------------------------------------------------------------------------
| id | user_id | notes | created_at | updated_at |
--------------------------------------------------------------------------------------
| 1 | 1 | Nothing | 2018-06-13 16:32:05.139284 | 2018-06-13 16:32:05.139284 |
--------------------------------------------------------------------------------------
Desire output:
-------------------------------------------------------------------------------------------------------
| id | name | archived | created_at | updated_at | yesterdays_notes |
-------------------------------------------------------------------------------------------------------
| 1 | Jonn | false | 2018-05-11 00:01:36.124999 | 2018-05-11 00:01:36.124999 | Nothing |
-------------------------------------------------------------------------------------------------------
| 2 | Sam | false | 2018-05-11 00:01:36.124999 | 2018-05-11 00:01:36.124999 | NULL |
-------------------------------------------------------------------------------------------------------
I was able to get the desired query results writing raw SQL, but I have run into a lot of issues trying to convert it to an active record query. Would this be an appropriate scenario to use the scenic gem?
Here is the raw SQL query:
SELECT u.*, (
SELECT notes AS yesterdays_notes
FROM reports AS r
WHERE r.created_at >= '2018-06-13 04:00:00'
AND r.created_at <= '2018-06-14 03:59:59.999999'
AND r.user_id = u.id
)
FROM users AS u
WHERE u.archived = FALSE
AND u.id NOT IN (
SELECT rr.user_id
FROM reports rr
WHERE rr.created_at >= '2018-06-14 04:00:00'
);
Here is how I would do it:
First, select all active users with most recent reports created yesterday and assign it to a var:
users = User.where(archived: false).joins(:reports)
.where.not('DATE(reports.created_at) IN (?)', [Date.today])
.where('DATE(reports.created_at) IN (?)', [Date.yesterday])
.select('users.id', 'users.name', 'notes')
now the users var will have the attrs listed in .select available so you can call users.map(&:notes) to see the list of nodes, including nil / null notes.
another trick that may come in handy is the ability to alias the attrs your listed in .select. For example, if you want to store users.id as id, you can do so with
...
.select('users.id as id', 'users.name', 'reports.notes')
you can call users.map(&:attributes) to see what these final structs would look like
more info on available Active Record querying can be found here
users = User.joins(:reports).where("(reports.created_at < ? OR reports.created_at BETWEEN ? AND ?) AND (users.archived = ?)", Date.today.beginning_of_day, Date.yesterday.beginning_of_day, Date.yesterday.end_of_day, false).select("users.id, users.name, reports.notes").uniq
users will return as #<ActiveRecord::Relation [....]
Possibly joins returns duplicate records so use uniq
Filter reports
reports.created_at < Date.today.beginning_of_day OR yesterday.beginning_of_day > reports.created_at < Yesterday.end_of_day
which is required reports as "not completed a report today, and show yesterdays report notes if available"
And users.archived = false
I was able to get the desired results from the 2 answers posted here, however, after doing some more research I think using a database view using the scenic gem is the appropriate approach here so I am going to move forward with that.
Thank you for the input! If you want to see some of the reasoning behind my decision this stackoverflow post summarizes it nicely: https://stackoverflow.com/a/4378166/2909095
Here is what I ended up with using the scenic gem. I changed the actual query a little bit to fit my needs better but it resolves this answer:
Model
# == Schema Information
#
# Table name: missing_reports
#
# id :bigint(8)
# name :string
# created_at :datetime
# updated_at :datetime
# previous_notes :text
# previous_notes_date :datetime
class MissingReport < ApplicationRecord
end
Database View
SELECT u.*, rs.notes AS previous_notes, rs.created_at AS previous_notes_date
FROM users AS u
LEFT JOIN (
SELECT r.*
FROM reports AS r
WHERE r.created_at < TIMESTAMP 'today'
ORDER BY created_at DESC
LIMIT 1
) rs ON rs.user_id = u.id
WHERE u.archived = FALSE
AND u.id NOT IN (
SELECT rr.user_id
FROM standups rr
WHERE rr.created_at >= TIMESTAMP 'today'
);
Usage
def index
#missing_reports = MissingReport.all
end

How to set another column with the value of the primary key on create

When creating a record I want to set a column called original_id to the value of its primary key (in this case id). In essence, the rows will become hierarchal and they will look like this:
| id | original_id |
| -- | ----------- |
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| .. | 1 |
| 42 | 42 |
| 43 | 42 |
The model will look like this:
class AccountTransaction < ApplicationRecord
belongs_to :original,
class_name: 'AccountTransaction',
foreign_key: :original_id
end
It's easy to create the record then update like this:
record = AccountTransaction.create
record.update(original: record)
But I am looking for a more elegant solution, one that doesn't require two database transactions. Thanks for your help.
You could do something with an after_create callback.
class AccountTransaction < ApplicationRecord
after_create :upate_original_id
def update_original_id
update_attribute(original_id: id)
end
end

Rails validation In attribute table

i am new to ROR.
i am building a classified ads app, i have the following tables in my database:
(some fields have been removed for simplicity)
Table Uers
This table stores all the users.
user_id
name
email
password
Table Ads
This table stores all the ads.
ad_id
users_user_id (FK)
title
desc
cat_id (FK)
created_at
Sample data:
------------------------------------------------------------------------------
| ad_id | users_user_id | title | desc | cat_id | created_at |
------------------------------------------------------------------------------
| 1 | 1 | iphone 4 | brand new | 2 | 30-11-2015 |
------------------------------------------------------------------------------
Table categories
This table stores all the available categories. cat_id in the ads table relates to cat_id in this table.
cat_id
category
parent_cid
Sample data:
-------------------------------------------
|cat_id| category | parent_cid |
-------------------------------------------
|1 | Electronics | NULL |
|2 | Mobile Phone | 1 |
|3 | Apartments | NULL |
|4 | Apartments - Sale | 3 |
-------------------------------------------
Table ads_attribute
This table contains all the available attributes for a particular category. Relates to categories table.
attr_id
cat_id (FK)
attr_label
attr_name
Sample data:
-----------------------------------------------------------
|attr_id | cat_id | attr_label | attr_name |
-----------------------------------------------------------
|1 | 2 | Operating System | Operating_System |
|2 | 2 | Is Touch Screen | Touch_Screen |
|3 | 2 | Manufacturer | Manufacturer |
|4 | 3 | Bedrooms | Bedrooms |
|5 | 3 | Total Area | Area |
|6 | 3 | Posted By | Posted_By |
-----------------------------------------------------------
Table ads_attr_value
This table stores the attribute value for each ad in ads table.
attr_val_id
attr_id (FK)
ad_id
attr_val
Sample data:
---------------------------------------------
|attr_val_id | attr_id | ad_id | attr_val |
---------------------------------------------
|1 | 1 | 1 | Ios 8 |
|2 | 2 | 1 | 1 |
|3 | 3 | 1 | Apple |
---------------------------------------------
What is the best way (the rails way) to validate the data before storing it in the the ads_attr_value table, given the fact that the values would be in select fields and the user can change them easily for example from Ios 8 to "blabla".
I've thought of storing all the possible values for each attribute in a new table and then check if a value sent by the user exist in that table before storing it in the ads_attr_value. what do you think? I am sure that there is a better way.thanks for sharing.
The rails way would probably to define your relationships with ActiveRecord associations : http://guides.rubyonrails.org/association_basics.html.
Therefore you could easily define on your model
class AdsAttrVal < ActiveRecord::Base
belongs_to :ad
validates :ad, presence: true
end
However please keep in mind that rails way to store an id of the table is to name it "id" and not "model_id" like you did ("user_id", "id"). My exemple suppose that the rails way is respected...
You have to specify the validations you want inside <yourModel>.rb (the model file) . For exame if you want to validate if ad_id is a number you should add the numericality parameter in the validates statement, see below:
class AdsAttrValue < ActiveRecord::Base
validates :ad_id, numericality: true
#validate if add_att_value has the permitted values
validate :myCustomValidation
def myCustomValidation
#your logic of validation goes here
#you can access here all the fields from this object recently created
if attr_val == something
#do something
end
end
end
See that validations from rails have an s at the end (validates), and your own written validations do not have (validate).
This validations are executed when creating the object before storing in database in order to see if it complies the validations and not stored it it does not comply. You can add errors in your own validation to let the user know what gone wrong. Go further with this reading of validations in ruby on rails

Rails Joins and include columns from joins table

I don't understand how to get the columns I want from rails. I have two models - A User and a Profile. A User :has_many Profile (because users can revert back to an earlier version of their profile):
> DESCRIBE users;
+----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| username | varchar(255) | NO | UNI | NULL | |
| password | varchar(255) | NO | | NULL | |
| last_login | datetime | YES | | NULL | |
+----------------+--------------+------+-----+---------+----------------+
> DESCRIBE profiles;
+----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| user_id | int(11) | NO | MUL | NULL | |
| first_name | varchar(255) | NO | | NULL | |
| last_name | varchar(255) | NO | | NULL | |
| . . . . . . |
| . . . . . . |
| . . . . . . |
+----------------+--------------+------+-----+---------+----------------+
In SQL, I can run the query:
> SELECT * FROM profiles JOIN users ON profiles.user_id = users.id LIMIT 1;
+----+-----------+----------+---------------------+---------+---------------+-----+
| id | username | password | last_login | user_id | first_name | ... |
+----+-----------+----------+---------------------+---------+---------------+-----+
| 1 | john | ****** | 2010-12-30 18:04:28 | 1 | John | ... |
+----+-----------+----------+---------------------+---------+---------------+-----+
See how I get all the columns for BOTH tables JOINED together? However, when I run this same query in Rails, I don't get all the columns I want - I only get those from Profile:
# in rails console
>> p = Profile.joins(:user).limit(1)
>> [#<Profile ...>]
>> p.first_name
>> NoMethodError: undefined method `first_name' for #<ActiveRecord::Relation:0x102b521d0> from /Library/Ruby/Gems/1.8/gems/activerecord-3.0.1/lib/active_record/relation.rb:373:in `method_missing' from (irb):8
# I do NOT want to do this (AKA I do NOT want to use "includes")
>> p.user
>> NoMethodError: undefined method `user' for #<ActiveRecord::Relation:0x102b521d0> from /Library/Ruby/Gems/1.8/gems/activerecord-3.0.1/lib/active_record/relation.rb:373:in method_missing' from (irb):9
I want to (efficiently) return an object that has all the properties of Profile and User together. I don't want to :include the user because it doesn't make sense. The user should always be part of the most recent profile as if they were fields within the Profile model. How do I accomplish this?
I think the problem has something to do with the fact that the Profile model doesn't have attributes for User...
Use select() to name the columns you want. At least this works in Rails 3.0.9.
Background: my application has a primary table named :rights. I wanted to be able to ascribe a tag and color to a given :right record so I could easily pick it out of an index listing. This doesn't cleanly fit the Rails picture of associated records; most :rights will never be tagged, and the tags are completely arbitrary (user input via tag/edit).
I could try duplicating the tag data in the :right record, but that violates normal form. Or I could try querying :tags for each :right record, but that is a painfully inefficient approach. I want to be able to join the tables.
MySQL console shows:
mysql> describe rights;
+------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
...
| Tagid | int(11) | YES | | NULL | |
+------------+---------------+------+-----+---------+----------------+
mysql> describe tags;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| TagName | varchar(255) | YES | | NULL | |
| TagColor | varchar(255) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
+------------+--------------+------+-----+---------+----------------+
I am going to use TagName and TagColor in views/rights/index.html.erb, so I want the rights controller to include those columns in the #rights object it passes to the view. Since not every :right has a :tag, I want to use an outer join:
#rights = Right.joins("LEFT OUTER JOIN tags ON rights.Tagid = tags.id")
But, as everyone has found, this alone doesn't work: a block reference to TagName produces a server error. However, if I add a select at the end, all is well:
#rights = Right.joins("LEFT OUTER JOIN tags ON rights.Tagid = tags.id").select("rights.*,tags.TagName as TagName,tags.TagColor as TagColor")
Note added 6/7/13: the select clause does not require aliases - this works too:
.select("rights.*,tags.TagName,tags.TagColor")
Now I can reference TagName and TagColor in my view:
<% #rights.each do |right| %>
<tr ALIGN=Left <%=
# color background if this is tagged
" BGCOLOR=#{right.TagColor}" if right.TagColor
%> > ...
<% end %>
I don't think that you can load users and profiles with join in Rails. I think that in earlier versions of Rails ( < 2.1) loading of associated models was done with joins, but it was not efficient. Here you have some explanation and links to other materials.
So even if you explicite say that you want to join it, Rails won't map it to associated models. So if you say Profile.whatever_here it will always be mapped to Profile object.
If you still want to do what you said in question, then you can call custom sql query and process results by yourself:
p = ActiveRecord::Base.connection.execute("SELECT * FROM profiles JOIN users ON profiles.user_id = users.id LIMIT 1")
and get results row by row with:
p.fetch_row
It will already be mappet to an array.
Your errors are because you are calling first_name and user method on AciveRecord::Relation object and it stores an array of Profile objects, not a single object. So
p = Profile.joins(:user).limit(1)
p[0].first_name
shoud work.
Better way to fetch only one record is to call:
p = Profile.joins(:user).first
p.first_name
p.user
But when you call p.user it will query database. To avoid it, you can use include, but if you load only one profile object, it is useless. It will make a difference if you load many profiles at a time and want to inlcude users table.
Try using select("*").joins(:table)
In this case, you would type:
User.select("*").joins(:profile)
Hope that works for you.
After reading these tips I got the joins to all be loaded in one query by reading 3 ways to do eager loading (preloading) in Rails 3 & 4.
I'm using Rails 4 and this worked like a charm for me:
refs = Referral.joins(:job)
.joins(:referee)
.joins(:referrer)
.where("jobs.poster_id= ?", user.contact_id)
.order(created_at: :desc)
.eager_load(:job, :referee, :referrer)
Here were my other attempts.
#first attempt
#refs = Referral.joins(:job)
# .where("jobs.poster_id= ?", user.contact_id)
# .select("referrals.*, jobs.*")
# works, but each column needs to be explicitly referenced to be used later.
# also there are conflicts for columns with the same name like id
#second attempt
#refs = ActiveRecord::Base.connection.exec_query("SELECT jobs.id AS job_id, jobs.*, referrals.id as referral_id, referrals.* FROM referrals INNER JOIN jobs ON job_id = referrals.job_id WHERE (jobs.poster_id=#{user.contact_id});")
# this worked OK, but returned back a funky object, plus the column name
# conflict from the previous method remains an issue.
#third attempt using a view + rails_db_views
#refs = JobReferral.where(:poster_id => user.contact_id)
# this worked well. Unfortunately I couldn't use the SQL statement from above
# instead of jobs.* I had to explicitly alias and name each column.
# Additionally it brought back a ton of duplicate data that I was putting
# into an array when really it is nice to work with ActiveRecord objects.
#eager_load
#refs = Referral.joins(:job)
# .where("jobs.poster_id= ?", user.contact_id)
# .eager_load(:job)
# this was my base attempt that worked before I added in two more joins :)
I have got round this problem by creating a VIEW in the database which is the join, and then referencing that as if it were a normal ActiveRecord table in the code. This is fine for getting data out of the database, but if you need to update it, then you'll need to go back to the base classes that represent the 'real' tables. I have found this method to be handy when doing reports that use biggish tables - you can get the data out all in one hit. I am surprised that this doesn't seem to be built into ActiveRecord, seems an obvious thing to me!
So for you:
IN SQL:
CREATE VIEW User_Profiles
AS
SELECT P.*, U.first_name
FROM Users U
inner join Profiles P on U.id=P.user_id
IN RUBY models file:
class UserProfile < ActiveRecord::Base
self.primary_key = :id
#same dependencies as profiles
end
**HINT... I always forget to set the owner of the view (I use postgres), so it blows up straight away with much cursing and self-recrimination.

Does it make sense to convert DB-ish queries into Rails ActiveRecord Model lingo?

mysql> desc categories;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(80) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
mysql> desc expenses;
+-------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| created_at | datetime | NO | | NULL | |
| description | varchar(100) | NO | | NULL | |
| amount | decimal(10,2) | NO | | NULL | |
| category_id | int(11) | NO | MUL | 1 | |
+-------------+---------------+------+-----+---------+----------------+
Now I need the top N categories like this...
Expense.find_by_sql("SELECT categories.name, sum(amount) as total_amount
from expenses
join categories on category_id = categories.id
group by category_id
order by total_amount desc")
But this is nagging at my Rails conscience.. it seems that it may be possible to achieve the same thing via Expense.find and supplying options like :group, :joins..
Can someone translate this query into ActiveRecord Model speak ?
Is it worth it... Personally i find the SQL more readable and gets my job done faster.. maybe coz I'm still learning Rails. Any advantages with not embedding SQL in source code (apart from not being able to change DB vendors..sql flavor, etc.)?
Seems like find_by_sql doesn't have the bind variable provision like find. What is the workaround? e.g. if i want to limit the number of records to a user-specified limit.
Expense.find(:all,
:select => "categories.name name, sum(amount) total_amount",
:joins => "categories on category_id = categories.id",
:group => "category_id",
:order => "total_amount desc")
Hope that helps!
Seems like find_by_sql doesn't have the bind variable provision like find.
It sure does. (from the Rails docs)
# You can use the same string replacement techniques as you can with ActiveRecord#find
Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
Well this is the code that finally worked for me.. (Francois.. the resulting sql stmt was missing the join keyword)
def Expense.get_top_n_categories options={}
#sQuery = "SELECT categories.name, sum(amount) as total_amount
# from expenses
# join categories on category_id = categories.id
# group by category_id
# order by total_amount desc";
#sQuery += " limit #{options[:limit].to_i}" if !options[:limit].nil?
#Expense.find_by_sql(sQuery)
query_options = {:select => "categories.name name, sum(amount) total_amount",
:joins => "inner join categories on category_id = categories.id",
:group => "category_id",
:order => "total_amount desc"}
query_options[:limit] = options[:limit].to_i if !options[:limit].nil?
Expense.find(:all, query_options)
end
find_by_sql does have rails bind variable... I don't know how I overlooked that.
Finally is the above use of user-specified a potential entry point for sql-injection or does the to_i method call prevent that?
Thanks for all the help. I'm grateful.

Resources