【问题标题】:How to model this `has_one` `belongs_to` relationship?如何建模这种 `has_one` `belongs_to` 关系?
【发布时间】:2015-08-04 19:58:51
【问题描述】:

UserOrganization 通过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

更新从外观上看,我知道我不应该以这种方式建模,而应该使用@DavidAldridge 在他的回答中建议的has_onebelongs_to 关系。但我不明白如何为这种关系建模(请参阅我在答案下方的评论)。非常感谢任何建议。

【问题讨论】:

  • 这并不是验证的真正用途。您可以考虑改用回调
  • 你的模型是什么样的?你到底定义了什么关系?
  • 我已将 validate :default 更改为 before_save :default 以将其转换为回调,但它在播种时产生了相同的错误。我会将模型关系添加到帖子中。
  • 对于您遇到的错误,那是因为 default 是 Postgres 关键字。见stackoverflow.com/questions/31809764/…
  • 好的,谢谢!将其更改为回调并更改名称解决了它。您将其添加为答案,然后我会接受吗?我有时确实会听到有关回调的坏消息(例如,参见 samuelmullen.com/2013/05/the-problem-with-rails-callbacks)。我的用例有更好的选择吗?另外,我找不到信息是否使用before_save 创建新记录时也会触发回调?还是我需要添加both before_save :newname before_create :default?因为在Relationship 中所做的每个更改都应该触发验证。

标签: ruby-on-rails ruby validation ruby-on-rails-4 associations


【解决方案1】:

这很困难的原因是您的数据模型不正确。用户的默认组织的身份是用户的属性,而不是关系的属性,因为每个用户只能有一个默认值。如果您有一级、二级、三级组织,那么这将是关系的一个属性。

不要在关系上放置“关系是用户的默认关系”属性,而是在用户上放置“default_relationship_id”属性,以便...

belongs_to :default_relationship

...和...

has_one :default_organisation, :through => :default_relationship

这保证:

  1. 用户的默认组织只能是一个组织
  2. 用户与其默认组织之间必须存在关系

你也可以在 :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

默认组织也很容易预先加载(不是不能,但不需要范围)。

【讨论】:

  • 谢谢。所以我将has_one :default_organisation, :through =&gt; :default_relationship 放在User 模型中。我将belongs_to :default_relationship 放在Organization 模型中。我实际上不需要创建DefaultRelationship 模型,还是我需要?如果用户不是任何组织的成员,User 模型中的 default_organisation_id 也可以为空(这需要查看 Relationship 的验证)。如何使用:dependent =&gt; :nullify?如果用户的默认组织被删除怎么办?那么如果其他组织的成员应该成为默认值。
  • 首先在用户中有一个 default_relationship_id,然后是与关系模型的适当关联。我为三个模型添加了一个相当完整的、非语法检查的关联集(不需要新的模型或类)。如果用户不再与其默认组织相关,则不会留下任何默认组织,因此必须选择另一个。
  • 谢谢,太好了!我按照建议实施。只是为了确认:我是否正确理解我不需要将default_for_stakeholder_id 添加到关系迁移文件中?我没有这样做并按照您的建议进行建模。然后在控制台中测试:如果我直接为 @user.default_relationship_id 保存一个值,它可以工作:@user.default_organization 然后生成与该 ID 的关系的组织。
  • 但是,这在控制台中不起作用:@user = User.first @user.default_organization => nil。 @user.default_organization = Organization.first@user.default_organization => 显示组织。 @user.save@user.reload。最后仍然显示@user: default_relationship_id: nil。我做错了什么吗?对@user.default_organization = Organization.first,控制台回复:BEGIN Relationship Exists SELECT 1 AS one FROM "relationships" WHERE ("relationships"."user_id" IS NULL AND "relationships"."organization_id" = 34) LIMIT 1 ROLLBACK
  • 设置 default_relationship 而不是 default_organisation 怎么样?
【解决方案2】:

@Brad Werth 认为您的 validate 方法作为回调会更好地工作。

我会在你的Relationship模型中推荐这样的东西:

before_save :set_default

private

  def set_default
    self.default = true unless self.user.relationships.where(member: true, default: true).any?
  end

如果用户的其他关系都没有,这应该强制用户的关系设置为默认值。

【讨论】:

  • 谢谢,但我认为这没有考虑到default(或is_default)对于member: true 组织只能是true。假设没有默认组织(例如,因为默认组织的帐户被删除)并且用户将保存与它不是其成员的组织的关系。然后在这里它仍然会将default 设置为true 对于它不是其成员的组织。
【解决方案3】:

default 更改为is_default(正如cmets 中的另一个用户所指出的,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

请确保在非高峰时间运行此迁移,因为它可能需要相当长的时间才能完成。或者,您可以从服务器控制台本身运行该代码 sn-p(当然,只需事先在开发环境中进行测试)。

然后,使用回调(正如另一位评论者正确建议的那样)在记录更新时设置默认组织

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

【讨论】:

  • 假设用户想要更改他们的默认组织。 1) 将旧组织的 is_default 值保存到 false。 2)Before_save of is_default 的新组织,它设置is_defaulttrue 为随机成员组织。 3) 将新组织的is_default 值保存到true。结果:两个具有true 值的组织。如果我将before_save 替换为after_save,也会出现类似的问题。
  • 拥有此签入用户无论如何都不会削减它,在再次保存用户之前,您仍然会有用户拥有 2 个默认组织的状态(并且调用了用户的此 before_save/validate 方法)。我会用解决方案更新我的答案。
  • @Marty 已更新。您可能会说这是一种复杂的方法,但是将检查委托给数据库本身是您可以确保您的数据在每个给定时刻保持一致的唯一方法。否则,随着或多或少的高请求更多的二流,您经常会遇到默认组织为 0 个或 2 个以上的情况。顺便说一句,#first 也不是随机的,它每次都会选择第一个(按 ID)记录。您还需要考虑从组织中删除用户(和/或组织本身被删除)的情况,您可以使用类似的解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-18
相关资源
最近更新 更多