在同一关系中关联不同的模型

时间:2014-07-03 00:05:46

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

我试图将不同的模型关联在同一个关系中,就像这样; User可以有许多不同的朋友,但每个朋友模型在验证和数据方面都是不同的。

User.first.friends # => [BestFriend, RegularFriend, CloseFriend, ...]

每个好友类都有不同的列,但它们都响应相同的方法#to_s

class User < ActiveRecord::Base
  has_many :friends # What more to write here?
end

class BestFriend < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :shared_interests # Shared between all friend models
  validates_presence_of :favourite_colour # Does only exists in this model

  def to_s
    "This is my best friend whos favourite colour is #{favourite_colour} ..."
  end
end

class RegularFriend < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :shared_interests # Shared between all friend models
  validates_presence_of :address # Does only exists in this model

  def to_s
    "This is a regular friend who lives at #{address} ..."
  end
end

class CloseFriend < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :shared_interests # Shared between all friend models
  validates_presence_of :car_type # Does only exists in this model

  def to_s
    "This is a close friend who drives a #{car_type} ..."
  end
end

这是我想要实现的目标。

  1. 应该可以对用户user.friends.page(4).per(10)上的朋友进行分页和切换加载。
  2. 朋友必须包含user_id以确保记录是唯一的。没有user_id的朋友的属性不能是唯一的,因为值可能会在朋友之间共享。
  3. 每个朋友都有自己的验证和列,这意味着他们需要自己的类。
  4. 以下是如何使用它的示例。

    user = User.create!
    
    CloseFriend.create!({ car_type: "Volvo", shared_interests: "Diving", user: user })
    RegularFriend.create!({ country: "Norway", shared_interests: "Swim", user: user })
    BestFriend.create!({ favourite_colour: "Blue", shared_interests: "Driving", user: user })
    BestFriend.create({ shared_interests: "Driving", user: user }) # => Invalid record
    
    user.friends.page(1).per(3).order("created_at ASC").each do |friend|
      puts friend.to_s
      # This is a close friend who drives a Volvo ...
      # This is a regular friend who lives in Norway ...
      # This is my best friend whos favourite colour is Blue ...
    end
    

    怎么可能解决这个问题?

6 个答案:

答案 0 :(得分:4)

我喜欢这个问题。我的回答很长:)

首先,您遇到了问题,因为您的Ruby OOP架构与关系数据库原则相冲突。只需考虑存储数据的表格。通常,您无法将数据存储在不同的表中并使用一个查询进行获取。因为SQL会使用相同类型的记录响应您,但表的行具有不同的类型。

让我们从另一边看。你有用户模型。你有几个朋友模特和他们所有的朋友。因此,即使它们不共享行为,您也必须从一个基类继承它们(我猜想称为Friend),因为您希望在分页中将它们用作具有相同行为的对象(r​​uby不需要它,但经典OOP设计需要它) )。现在你在RoR中有类似的东西:

class User < AR::Base
  has_many :friends
end

class Friend < AR::Base
  belongs_to :user
end

class BestFriend < Friend
end

class OldFriend < Friend
end

幸运的是,你不是第一个:)有一些模式在RDBMS上投射OOP继承。他们在Martin Fowler的企业应用程序架构(又名PoEAA)中描述了这些内容。

  1. 这里首先提到STI(单表继承)。这是首选方式,因为Rails已经内置了STI支持。此外,STI允许您使用一个数据库请求获取所有数据,因为它是一个表查询。但是STI有一些缺点。主要是您必须将所有数据存储在一个表中。这意味着您的所有模型将共享一个表的字段。例如,您的BestFriend模型具有alias属性,而OldFriend则没有,但无论如何,ActiveRecord将为您创建aliasalias=方法。从OOP的角度来看,它并不干净。您可以做的就是取消定义这些方法或使用raise 'Not Available for %s' % self.class.name覆盖它们。
  2. 第二个是Concrete Table Inheritance。它不适合你,因为你不会有一个包含所有记录的表,并且不能对分页进行简单的查询(实际上你只做了一些奇怪的数据库内容)。但具体表继承的无可争议的优势是所有的朋友模型都将具有独立的字段。从OOP的角度看它很干净。
  3. 第三是Class Table inheritance。如果不使用数据库视图而不使用触发器,它与ActiveRecord模式不兼容,但对于您的问题而言过于复杂。类表继承与DataMapper模式兼容,但Rails在很大程度上依赖于ActiveRecord。
  4. 您可以使用NoSQL解决方案轻松实现目标。您只需在MongoDB之类的东西中创建朋友集合,并将所有数据存储在集合的文档中。但是不要使用Mongo:)

    所以我建议你使用STI,但我有一些很好的技巧。如果使用PostgreSQL,您可以将所有特定于模型的数据存储在JSON或hstore列中,并将NoSQL的灵活性与RDBMS模式严格性和ActiveRecord功能相结合。只需为特定于模型的列创建setter / getter。像这样:

    class Friend < AR::Base
      def self.json_accessor(*names)
        names.each do |name|
          class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def #{name}
              friend_details['#{name}']
            end
    
            def #{name}=(value)
              # mark attribute as dirty
              friend_details_will_change!
              friend_details['#{name}'] = value
            end
          RUBY
        end
      end
    end
    
    class BestFriend < Friend
      json_accessor :alias
    end
    

    这不是唯一的方法,但我希望所有这些都可以帮助您创建一个可靠的解决方案。

    P.S。抱歉。我认为它几乎是博客文章而不仅仅是答案。但我对所有这些与数据库相关的东西都非常热衷:)

    P.P.S。再次抱歉。只是不能停止。具体表继承的奖金。您仍然可以使用数据库视图创建具有单独表的一体化表。例如:

    DB:

    CREATE VIEW FRIENDS
    AS
    SELECT ID,
           USER_ID,
           NAME,
           'best' TYPE
    FROM   BEST_FRIENDS
    UNION ALL
    SELECT ID,
           USER_ID,
           NAME,
           'old' TYPE
    FROM   OLD_FRIENDS
    UNION ALL
    ...
    

    滑轨:

    class User < AR::Base
      has_many :friends
    end
    
    class Friend < AR::Base
      belongs_to :user
    
      def to_concrete
        ('%sFriend' % type.camelize).find(id)
      end
    end
    
    class BestFriend < Friend
    end
    
    class OldFriend < Friend
    end
    

    用法:

    user = User.create!
    BestFriend.create!(user_id: user.id, alias: 'Foo', name: 'Bar')
    OldFriend.create!(user_id: user.id, name: 'Baz')
    
    user.friends
    # One SQL query to FRIENDS view
    # [#Friend(id: 1, type: 'best', name: 'Bar'), #Friend(id: 2, type: 'old', name: 'Baz')]
    user.friends.map(&:name) 
    # ['Bar', 'Baz']
    user.first.alias
    # NoMethodError
    user.first.to_concrete
    # #BestFriend(id: 1, name: 'Bar', alias: 'Foo')
    user.first.to_concrete.alias
    # 'Foo'
    user.second.to_concrete
    # #OldFriend(id: 2, name: 'Baz')
    user.second.to_concrete.alias
    # NoMethodError
    

答案 1 :(得分:0)

这是未经测试的,但考虑一下,您可以尝试使用同一个表创建多个模型,每个模型都有不同的默认范围。

def BestFriend < ActiveRecord::Base
  self.table_name "friends"
  default_scope -> { where(friendship: 'best') }
end

def RegularFriend < ActiveRecord::Base
  self.table_name "friends"
  default_scope -> { where(friendship: 'regular') }
end

在User类中仍然有has_many :friends,希望在读取记录时,范围会将表记录与相应的模型匹配。

修改

因为你需要对所有记录做出回应的唯一事情是:to_s你可以拥有一个通用的Friend类,只是用于关系。

class Friend < ActiveRecord::Base
  def to_s
    case friendship
    when 'best'
      object = BestFriend.find(self.id)
    when 'regular'
     object = RegularFriend.find(self.id)
    end
    object.to_s
  end
end

答案 2 :(得分:0)

我建议使用store_accessor和STI的组合。商店访问者将允许您添加模型特定属性并支持验证。如果您使用PostgreSQL,您可以利用GIN / GiST索引有效地搜索自定义字段。

class User < ActiveRecord::Base
  has_many :friends
end

class Friend < ActiveRecord::Base      
  belongs_to :user

  # exclude nil values from uniq check 
  validates_uniqueness_of :user_id, allow_nil: true


  # The friends table should have a text column called  
  # custom_attrs. 
  # In Postgres you can use hstore data type. Then
  # next line is not required.
  store      :custom_attrs

  # Shared between all friend models
  store_accessor [ :shared_interests ]
  validates_presence_of :shared_interests

  # bit of meta-programming magic to add helper
  # associations in User model.
  def self.inherited(child_class)
    klass = child_class.name 
    name = klass.pluralize.underscore.to_sym
    User.has_many name, -> {where(type: klass)}, class_name:  klass
  end
end

使用模型特定属性定义各种朋友模型。

class BestFriend < Friend
  store_accessor [ :favourite_colour ]
  validates_presence_of :favourite_colour

  def to_s
    "This is my best friend whos favourite colour is #{favourite_colour} ..."
  end
end

class RegularFriend  < Friend
  store_accessor [ :address ]
  validates_presence_of :address

  def to_s
    "This is a regular friend who lives at #{address} ..."
  end
end

class CloseFriend <  Friend
  store_accessor [ :car_type ]
  validates_presence_of :car_type

  def to_s
    "This is a close friend who drives a #{car_type} ..."
  end
end

<强>用法

user = User.create!

user.close_friends.create!(
  car_type: "Volvo", 
  shared_interests: "Diving"
)

user.regular_friends.create!(
  country: "Norway", 
  shared_interests: "Diving"
)

user.best_friends.create!(
  favourite_colour: "Blue", 
  shared_interests: "Diving"
)

# throws error
user.best_friends.create!(
  shared_interests: "Diving"
)

# returns all friends
user.friends.page(4).per(10)

<强>参考

Store documentation

PostgreSQL Hstore documentation:

答案 3 :(得分:-1)

单表继承

根据评论

,您最好使用STI

您遇到的问题是您在不同版本中引用了相同的数据集。 &#34; Best&#34;,&#34;关闭&#34; &安培; &#34;定期&#34;是所有类型的朋友 - 这意味着将它们存储在单个表/模型中会更好(并且符合Single Source of Truth原则)

你可以这样做:

#app/models/user/close.rb
Class User::Close < Friend
   ...
end

#app/models/user/best.rb
Class User::Best < Friend
   ...
end

#app/models/user/regular.rb
Class User::Regular < Friend
   ...
end

这样您就可以将User模型与不同类型的朋友关联起来:

#app/models/user.rb
Class User < ActiveRecord::Base
    has_many :bffs, class: "User::Best"
    has_many :regs, class: "User::Regular"
    has_many :closies, class: "User::Close"
end

不像您希望的那样干 - 但允许您使用单个数据表来存储您需要的相关文件

-

<强>条件

另一种方法是使用条件关联:

#app/models/user.rb
Class User < ActiveRecord::Base
   has_many :friends, ->(type="regular") { where(type: type) }
end

这将允许您致电:

@user = User.find params[:id]
@user.friends("best")

答案 4 :(得分:-1)

我已经使用STI(单表继承)完成了这项工作,但是使用ActiveRecord的情况是,你在表达连接条件方面的能力有限,并且在搜索相关信息时甚至无法执行诸如union all之类的操作你确切知道你想要的sql。

例如,在标题或趋势中查找过去x天内以'foo'发布的朋友的所有朋友和朋友。当表中没有携带相关信息时,不能搜索简单的组织/人员关系。无论你做什么,当然AR / Arel都会产生你想要的查询。

你不能使用SQL字符串加入,因为你得到一个数组而不是关系,这意味着链接不起作用。

我转到Sequel,我可以实际编写查询并访问有用的sql功能,超越简单的crud模式,这些模式关于AR有什么用处。

下面是一个AR示例(上述警告,AR与续集相比有严重的限制),它定义了Party(人,组织,用户及其子类 - 使用STI)和它们之间的丰富关系(BestFriend等),它们具有属性关系(关系也使用STI)。

由于基表已修复,因此没有多态连接。

首先是党模式:

class Party < ActiveRecord::Base

  ###################################################
  # relationships

  has_many :left_relationships,
    class_name: 'Relationship',
    foreign_key: :left_party_id,
    inverse_of: :left_party

  has_many :right_parties,
    through: :left_relationships,
    source: :right_party

  has_many :right_relationships,
    class_name: 'Relationship',
    foreign_key: :right_party_id,
    inverse_of: :right_party

  has_many :left_parties,
    through: :right_relationships,
    source: :left_party

  def relationships
    Relationship.involving_party(self)
  end

  def peers
    left_parties.merge(right_parties).uniq
  end
end

现在是关系模型,它有一些有用的范围,用于查找与任何一方,一个政党社区或一组政党之间的关系:

class Relationship < ActiveRecord::Base
  before_save :save_parties
  belongs_to :left_party,
    class_name: 'Party',
    inverse_of: :left_relationships
  belongs_to :right_party,
    class_name: 'Party',
    inverse_of: :right_relationships

  # scopes
  #

  # relationships for the given party/parties
  def self.involving_party(party)
    any_of({left_party_id: party}, {right_party_id: party})
  end

  # all relationships between left and right parties/groups
  def self.between_left_and_right(left_group, right_group)
    where(left_party_id: left_group, right_party_id: right_group)
  end

protected
  def save_parties
    left_party.save if left_party
    right_party.save if right_party
  end
end

一些必要且非常有用的AR扩展:

module ARExtensions
  extend ActiveSupport::Concern
    module ClassMethods
      # return a relation/scope that matches any of the provided
      # queries. This is basically OR on steroids.
      def any_of(*queries)
        where(
          queries.map do |query|
            query = where(query) if [String, Hash].any? { |type| query.kind_of? type }
            query = where(*query) if query.kind_of? Array
            query.arel.constraints.reduce(:and)
          end.reduce(:or)
        )
      end

      # polyfil for AR scope removal which has no equivalent
      def scoped(options=nil)
        options ? where(nil).apply_finder_options(options, true) : where(nil)
      end
    end
end

现在是Person模型,如果您愿意,可以将其称为User,但语义对我来说很重要,您可能会有非用户的人,可能会选择将系统用户的概念分开从一个人/党的概念:

class Person < Party

  # BestFriend relationships
  has_many :best_friend_relationships,
    class_name: 'BestFriend',
    foreign_key: :right_party_id,
    inverse_of: :left

  has_many :best_friends,
    class_name: 'Person',
    through: :best_friend_relationships,
    source: :best_friend

  has_many :best_friend_of_relationships,
    class_name: 'BestFriend',
    foreign_key: :left_party_id,
    inverse_of: :right

  has_many :best_friends_of,
    class_name: 'Person',
    through: :client_relationships,
    source: :client

  # etc for whatever additional relationship types you have
end

BestFriend关系:

class BestFriend < Relationship
  belongs_to :best_friend_of,
    class_name: 'Person',
    foreign_key: :left_party_id,
    inverse_of: :best_friend_relationships,
    autosave: true

  belongs_to :best_friend,
    class_name: 'Person',
    foreign_key: :right_party_id,
    inverse_of: :best_friend_of_relationships,
    autosave: true

  validates_existence_of :best_friend_of
  validates_existence_of :best_friend, allow_new: true

protected
  def save_parties
    best_friend_of.save if best_friend_of
    self.left_party = best_friend_of
    best_friend.save if best_friend
    self.right_party = best_friend
  end
end

迁移(使用SchemaPlus扩展名)

class CreateParties < ActiveRecord::Migration
  def change

    create_table :parties do |t|
      t.timestamps
      t.string :type, index: true
      t.string :name, index: true

      # Person attributes
      t.date :date_of_birth, index: true
      t.integer :gender

      # and so on as required for your different types of parties
    end

    create_table :relationships do |t|
      t.timestamps
      t.string :type, index: true
      t.belongs_to :left_party, index: true, references: :parties, on_delete: :cascade
      t.belongs_to :right_party, index: true, references: :parties, on_delete: :cascade
      t.date :began, index: true
      t.date :ended, index: true

      # attributes for best friends

      # attributes for family members

      # attributes for enemies

    end
  end
end

答案 5 :(得分:-3)

你无法通过单表继承和多态关联来实现这一点......这些技术适用于特定的类记录,它可以属于几个不同类之一的记录。

你所做的几乎完全相反......许多不同类中的记录都属于普通类中的记录。

我认为您需要单独指定has_many关系,但要制作实例方法&#34;朋友&#34;返回数组的总和。

class User < ActiveRecord::Base
  has_many :best_friends
  has_many :regular_friends
  has_many :close_friends
  def friends
    best_friends + regular_friends + close_friends
  end
end

如果你想尝试概括一下(我还没有测试过这个),你可以试试......

class User < ActveRecord::Base
  ActiveRecord::Base.connection.tables.each { |t| has_many t.to_sym if t.ends_with?("_friends") }

  def friends
    friends = ActiveRecord::Base.connection.tables.inject("[]") {  |all, t| all << self.send(t) if t.ends_with?("_friends"); all }
    friends
  end
end