如何对这种 `has_one` `belongs_to` 关系建模?
How to model this `has_one` `belongs_to` relationship?
User
和 Organization
通过 Relationship
有一个 many-to-many
关联。 Relationship
模型包括几个关于关系的布尔变量,例如 moderator
(true/false) 和 member
(true/false)。另外,我添加了一个名为 default
的布尔值来设置默认组织。
我需要验证 如果(且仅当)用户是一个或多个组织 (member == true
) 的 成员,一个这些组织中(恰好 1 个)必须有 default == true
.
所以基本上这意味着如果用户是多个组织的成员,则这些组织之一需要是默认组织,如果用户是多个组织的成员,则必须存在这样的默认组织。
这个验证怎么写?我当前的验证在播种时生成以下错误:
PG::SyntaxError: ERROR: syntax error at or near "default"
LINE 1: ...ERE (user_id = 1) AND (member = 't' and default = ...
^
: SELECT COUNT(*) FROM "relationships" WHERE (user_id = 1) AND (member = 't' and default = 't')
我在 Relationship
模型中的实现:
validate :default
private
def default
@relationships = Relationship.where('user_id = ?', self.user_id)
@members = @relationships.where('member = ?', true)
@defaults = @members.where('default = ?', true)
# If more than 1 organization has been set as default for user
if @defaults.count > 1
@defaults.drop(0).each do |invalid|
invalid.update_columns(default: false)
end
end
# If user is member but has no default organization yet
if !@defaults.any? && @members.any?
@members.first.update_columns(default: true)
end
end
Update 从表面上看,我知道我不应该这样建模,而应该使用 has_one
belongs_to
关系作为@DavidAldridge 在他的回答中提出了建议。但我不明白如何为这种关系建模(请参阅我在答案下方的评论)。非常感谢任何建议。
@Brad Werth 是正确的,您的 validate
方法作为回调会更好地工作。
我会在你的关系模型中推荐这样的东西:
before_save :set_default
private
def set_default
self.default = true unless self.user.relationships.where(member: true, default: true).any?
end
如果 none 用户的其他关系已经是默认值,这应该会强制将用户关系设置为默认值。
将default
更改为is_default
(正如另一位用户在评论中指出的那样,default
是postgres关键字)。为此创建单独的迁移。 (或者,如果您希望保持原样,您可以在任何地方引用它。)
那么,有两点。
首先,为什么每次都需要检查单个is_default
组织?您只需要迁移您当前的数据集,然后保持一致。
要迁移您当前的数据集,请创建迁移并在其中写入如下内容:
def self.up
invalid_defaults = Relationship.
where(member: true, is_default: true).
group(:user_id).
having("COUNT(*) > 1")
invalid_defaults.each do |relationship|
this_user_relationships = relationship.user.relationships.where(member: true, is_default: true)
this_user_relationships.where.not(id: this_user_relationships.first.id).update_all(is_default: false)
end
end
请确保 运行 在非高峰时段进行此迁移,因为完成此迁移可能需要相当长的时间。或者,您可以 运行 来自服务器控制台本身的代码片段(当然,只需事先在开发环境中进行测试)。
然后,在更新记录时使用回调(正如另一位评论者正确建议的那样)设置默认组织
before_save :set_default
private
def set_default
relationships = Relationship.where(user_id: self.user_id)
members = relationships.where(member: true)
defaults = members.where(is_default: true)
# No need to migrate records in-place
# Change #any? to #exists?, to check existance via SQL, without actually fetching all the records
if !defaults.exists? && members.exists?
# Choosing the earliest record
members.first.update_columns(is_default: true)
end
end
考虑到正在编辑组织的情况,还应添加对组织的回调:
class Organization
before_save :unset_default
after_commit :set_default
private
# Just quque is_default for update...
def remember_and_unset_default
if self.is_default_changed? && self.is_default
@default_was_set = true
self.is_default = false
end
end
# And now update it in a multi-thread safe way: let the database handle multiple queries being sent at once,
# and let only one of them to actually complete, keeping base in always consistent state
def set_default
if @default_was_set
self.class.
# update this record...
where(id: self.id).
# but only if there is still ZERO default organizations for this user
# (thread-safety will be handled by database)
where(
"id IN (SELECT id FROM organizations WHERE member = ?, is_default = ?, user_id = ? GROUP BY user_id HAVING COUNT(*)=0)",
true, true, self.user_id
)
end
end
造成困难的原因是您的数据模型不正确。用户默认组织的标识是用户的属性,而不是关系的属性,因为每个用户只能有一个默认值。如果你有一个主要的、次要的、第三个组织,那么这将是关系的一个属性。
不是在关系上放置 "relationship is default for user" 属性,而是在用户上放置 "default_relationship_id" 属性,这样它...
belongs_to :default_relationship
...和...
has_one :default_organisation, :through => :default_relationship
这保证:
- 用户只能默认一个组织
- 用户与其默认组织之间必须存在关系
您还可以在 :default_relationship 的反向关联上使用 :dependent => :nullify,并根据是否:
轻松测试个体关系是否为默认关系
self == user.default_relationship.
所以像这样:
class User << ActiveRecord::Base
has_many :relationships, :inverse_of => :user, :dependent => :destroy
has_many :organisations, :through => :relationships, :dependent => :destroy
belongs_to :default_relationship, :class_name => "Relationship", :foreign_key => :default_relationship_id, :inverse_of => :default_for_user
has_one :default_organisation, :through => :default_relationship, :source => :organisation
class Relationship << ActiveRecord::Base
belongs_to :user , :inverse_of => :relationships
belongs_to :organisation, :inverse_of => :relationships
has_one :default_for_user, :class_name => "User", :foreign_key => :default_relationship_id, :inverse_of => :default_relationship, :dependent => :nullify
class Organisation << ActiveRecord::Base
has_many :relationships, :inverse_of => :organisation, :dependent => :destroy
has_many :users , :through => :relationships
has_many :default_for_users, :through => :relationships, :source => :default_for_user
因此你可以做这样简单的事情:
@user = User.find(34)
@user.default_organisation
默认组织也很容易预先加载(并不是说它不能,但不需要范围来这样做)。
User
和 Organization
通过 Relationship
有一个 many-to-many
关联。 Relationship
模型包括几个关于关系的布尔变量,例如 moderator
(true/false) 和 member
(true/false)。另外,我添加了一个名为 default
的布尔值来设置默认组织。
我需要验证 如果(且仅当)用户是一个或多个组织 (member == true
) 的 成员,一个这些组织中(恰好 1 个)必须有 default == true
.
所以基本上这意味着如果用户是多个组织的成员,则这些组织之一需要是默认组织,如果用户是多个组织的成员,则必须存在这样的默认组织。
这个验证怎么写?我当前的验证在播种时生成以下错误:
PG::SyntaxError: ERROR: syntax error at or near "default"
LINE 1: ...ERE (user_id = 1) AND (member = 't' and default = ...
^
: SELECT COUNT(*) FROM "relationships" WHERE (user_id = 1) AND (member = 't' and default = 't')
我在 Relationship
模型中的实现:
validate :default
private
def default
@relationships = Relationship.where('user_id = ?', self.user_id)
@members = @relationships.where('member = ?', true)
@defaults = @members.where('default = ?', true)
# If more than 1 organization has been set as default for user
if @defaults.count > 1
@defaults.drop(0).each do |invalid|
invalid.update_columns(default: false)
end
end
# If user is member but has no default organization yet
if !@defaults.any? && @members.any?
@members.first.update_columns(default: true)
end
end
Update 从表面上看,我知道我不应该这样建模,而应该使用 has_one
belongs_to
关系作为@DavidAldridge 在他的回答中提出了建议。但我不明白如何为这种关系建模(请参阅我在答案下方的评论)。非常感谢任何建议。
@Brad Werth 是正确的,您的 validate
方法作为回调会更好地工作。
我会在你的关系模型中推荐这样的东西:
before_save :set_default
private
def set_default
self.default = true unless self.user.relationships.where(member: true, default: true).any?
end
如果 none 用户的其他关系已经是默认值,这应该会强制将用户关系设置为默认值。
将default
更改为is_default
(正如另一位用户在评论中指出的那样,default
是postgres关键字)。为此创建单独的迁移。 (或者,如果您希望保持原样,您可以在任何地方引用它。)
那么,有两点。
首先,为什么每次都需要检查单个is_default
组织?您只需要迁移您当前的数据集,然后保持一致。
要迁移您当前的数据集,请创建迁移并在其中写入如下内容:
def self.up
invalid_defaults = Relationship.
where(member: true, is_default: true).
group(:user_id).
having("COUNT(*) > 1")
invalid_defaults.each do |relationship|
this_user_relationships = relationship.user.relationships.where(member: true, is_default: true)
this_user_relationships.where.not(id: this_user_relationships.first.id).update_all(is_default: false)
end
end
请确保 运行 在非高峰时段进行此迁移,因为完成此迁移可能需要相当长的时间。或者,您可以 运行 来自服务器控制台本身的代码片段(当然,只需事先在开发环境中进行测试)。
然后,在更新记录时使用回调(正如另一位评论者正确建议的那样)设置默认组织
before_save :set_default
private
def set_default
relationships = Relationship.where(user_id: self.user_id)
members = relationships.where(member: true)
defaults = members.where(is_default: true)
# No need to migrate records in-place
# Change #any? to #exists?, to check existance via SQL, without actually fetching all the records
if !defaults.exists? && members.exists?
# Choosing the earliest record
members.first.update_columns(is_default: true)
end
end
考虑到正在编辑组织的情况,还应添加对组织的回调:
class Organization
before_save :unset_default
after_commit :set_default
private
# Just quque is_default for update...
def remember_and_unset_default
if self.is_default_changed? && self.is_default
@default_was_set = true
self.is_default = false
end
end
# And now update it in a multi-thread safe way: let the database handle multiple queries being sent at once,
# and let only one of them to actually complete, keeping base in always consistent state
def set_default
if @default_was_set
self.class.
# update this record...
where(id: self.id).
# but only if there is still ZERO default organizations for this user
# (thread-safety will be handled by database)
where(
"id IN (SELECT id FROM organizations WHERE member = ?, is_default = ?, user_id = ? GROUP BY user_id HAVING COUNT(*)=0)",
true, true, self.user_id
)
end
end
造成困难的原因是您的数据模型不正确。用户默认组织的标识是用户的属性,而不是关系的属性,因为每个用户只能有一个默认值。如果你有一个主要的、次要的、第三个组织,那么这将是关系的一个属性。
不是在关系上放置 "relationship is default for user" 属性,而是在用户上放置 "default_relationship_id" 属性,这样它...
belongs_to :default_relationship
...和...
has_one :default_organisation, :through => :default_relationship
这保证:
- 用户只能默认一个组织
- 用户与其默认组织之间必须存在关系
您还可以在 :default_relationship 的反向关联上使用 :dependent => :nullify,并根据是否:
轻松测试个体关系是否为默认关系self == user.default_relationship.
所以像这样:
class User << ActiveRecord::Base
has_many :relationships, :inverse_of => :user, :dependent => :destroy
has_many :organisations, :through => :relationships, :dependent => :destroy
belongs_to :default_relationship, :class_name => "Relationship", :foreign_key => :default_relationship_id, :inverse_of => :default_for_user
has_one :default_organisation, :through => :default_relationship, :source => :organisation
class Relationship << ActiveRecord::Base
belongs_to :user , :inverse_of => :relationships
belongs_to :organisation, :inverse_of => :relationships
has_one :default_for_user, :class_name => "User", :foreign_key => :default_relationship_id, :inverse_of => :default_relationship, :dependent => :nullify
class Organisation << ActiveRecord::Base
has_many :relationships, :inverse_of => :organisation, :dependent => :destroy
has_many :users , :through => :relationships
has_many :default_for_users, :through => :relationships, :source => :default_for_user
因此你可以做这样简单的事情:
@user = User.find(34)
@user.default_organisation
默认组织也很容易预先加载(并不是说它不能,但不需要范围来这样做)。