Ruby on rails Rails查询,基于不相关模型的范围
我想找到一个用户的Ruby on rails Rails查询,基于不相关模型的范围,ruby-on-rails,Ruby On Rails,我想找到一个用户的车队中没有连接的地方 我有一个convas表,其中有一个sender\u id和receiver\u id,这两个都是对用户id的引用 # app/models/user.rb has_many :convos, ->(user) { unscope(:where).where("sender_id = :id OR recipient_id = :id", id: user.id) } 注意:CONVA可以属于发送方id或接收方id的用户 # app/model
车队
中没有连接的地方
我有一个convas
表,其中有一个sender\u id
和receiver\u id
,这两个都是对用户id的引用
# app/models/user.rb
has_many :convos, ->(user) {
unscope(:where).where("sender_id = :id OR recipient_id = :id", id: user.id)
}
注意:CONVA可以属于发送方id或接收方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
我想找到用户的conva
中没有connect
我试过这样的方法:
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)
然后我尝试根本不引用用户,只是尝试在没有连接的情况下获取所有车队
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)))
您可以使用
LEFT JOIN
获取用户行,其中id
和corbas.发送者id
和corbas.接收者id
不是NULL
,而是连接之间的匹配。请求者id
和连接之间的匹配。请求者id
是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实施:
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 })
考虑到这样的DB结构:
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)
记录如下:
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)
以下查询将只返回第二个CONVA,因为id为3的用户没有任何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)
Rails查询可以如下所示:
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 })
因此,如果我理解正确,从一个用户与之对话的用户列表中,您需要一个他们没有连接的用户列表 简单地说,这可能类似于:
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
这两个集合也包含user.id
,但我们可以忽略这一点,因为我们对差异感兴趣:这将是我们与之交谈的一组人,没有连接(而且user.id
将同时包含在这两个集合中,除非其中一个为空,否则我们不必从这些集合中单独删除user.id
)
这不是一种最佳方法,因为我们进行两次查询,从数据库中检索所有用户ID,然后丢弃大部分检索到的数据。我们可以通过创建自定义查询来改进这一点,并让数据库为我们完成工作,如下所示
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
sql=使用当前设置回答
如果您正在查找当前用户,您需要从他们的车队开始,左连接到连接
,然后选择连接
为空的行。在表设置中,我们必须在可能的用户id组合上手动执行此操作:
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})
左连接为您提供与COVA相同的两个用户之间的任何连接,这必然涉及当前用户
,因为我们从当前用户.COVA开始。从那里,我们过滤到只有连接
字段为空
的行,得到的行具有不匹配连接的COVA
暗示
在Rails应用程序中,这么多原始SQL有点代码味道,这是因为我们在这里尝试使用按原样设置的模型。我建议重构数据模型,使查询更容易。我想到了两个想法:
始终为connect和COVA创建对称记录,这样您就可以通过单个列进行查找,而不是使用所有的或s。也就是说,每当在用户1和用户2之间创建连接时,也要在用户2和用户1之间创建一个连接。更多的簿记,因为你必须同时销毁和编辑它们。但是它让你。定义没有所有环的简单关联
使用单独的表来引用唯一的用户对(顺序无关紧要)。为此,使用user\u 1\u id
和user\u 2\u id
创建一个UserPair
模型,其中user\u 1\u id
始终设置为两个用户id中的较低者。这样,通过用户对id
可以更容易地识别conva,用户对可以拥有多个:conva
和拥有多个:connections
,并且您可以在conva->user\u pairs->connections之间建立直轨连接
图2中的模型看起来像
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
类用户对(用户\u 1\u id,用户\u 2\u id)之间
#记录总是按已排序的id顺序保存,所以在查询之前进行排序
用户\u 1\u id,用户\u 2\u id=[用户\u 1\u id,用户\u 2\u id]。排序
其中(用户\u 1\u id:用户\u 1\u id,用户\u 2\u id:用户\u 2\u id)
结束
#始终将最低id放在第一位,以便于查找
def sort_用户
如果用户_1.present?&&用户_2.present?&用户_1.id>用户_2.id
self.user_1,self.user_2=用户_2,用户_1
结束
结束
结束
类conva:sender\u id,class\u name:'User'
属于:收件人,:外键=>:收件人id,类别名称:'User'
属于:用户对
验证前:设置用户对
范围:涉及,->(用户)do
其中(“convas.sender\u id=?或convas.receiver\u id=?”,user.id,user.id)
结束
#由于用户对记录总是按用户id排序,所以我们可以使用
#该模型在这里的作用域不需要使用“merge”重复`
范围:介于,->(发送方id、接收方id)之间
联接(:user\u pair).merge(UserPair.between(发送者\u id,接收者\u id))
结束
def设置用户对
self.user\u pair=UserPair.find\u或\u initialize\u by(用户\u 1:发送方,用户\u 2:接收方)
结束
结束
我将首先编写SQL查询来执行此操作。在你的情况下,你也许想要
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)
))
然后可以将其转换为AR查询。class Convaclass 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
def self.no_connects(用户id=nil)
q=连接('
左连接连接打开
中的发件人\u id(connects.requestor\u id,connects.requestee\u id)
或
中的收件人\u id(connects.requestor\u id,connects.requestee\u id)
')
q=q.where('connects.requestor_id为NULL,connects.requestee_id为NULL'))
q=q.where(“convas.sender\u id=:user\u id或convas.recipient\u id=:user\u id”,user\u id:user\u id)如果是user\u id
Q
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
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})
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
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)
))
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