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

Lin*_*der 2 ruby-on-rails ruby-on-rails-4

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

User.first.friends # => [BestFriend, RegularFriend, CloseFriend, ...]
Run Code Online (Sandbox Code Playgroud)

每个朋友类都有不同的列,但它们都响应相同的方法#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
Run Code Online (Sandbox Code Playgroud)

这是我正在努力实现的愿望。

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

这是一个如何使用它的示例。

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
Run Code Online (Sandbox Code Playgroud)

怎么可能解决这个问题?

Nik*_*kov 5

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

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

让我们从另一边看。你有用户模型。你有几个 ?Friend 模型,他们实际上都是朋友。因此,即使它们不共享行为,您也必须从一个基类(我猜是 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
Run Code Online (Sandbox Code Playgroud)

幸运的是,您不是第一个 :) 有一些模式将 OOP 继承投射到 RDBMS 上。他们由 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 无可争议的优点是你所有的朋友模型都会有独立的字段。从面向对象的角度来看,它是干净的。
  3. 三是类表继承。如果不使用数据库视图和触发器,它与 ActiveRecord 模式不兼容,但对于您的问题来说太复杂了。类表继承与 DataMapper 模式兼容,但 Rails 严重依赖 ActiveRecord。

您可以使用 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
Run Code Online (Sandbox Code Playgroud)

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

PS对不起。我认为这几乎是博文,而不仅仅是回答。但我对所有这些与数据库相关的事情非常热情:)

PPS 再次抱歉。就是停不下来 具体表继承的奖励。您仍然可以使用数据库视图创建具有单独表的多合一表。例子:

D B:

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
...
Run Code Online (Sandbox Code Playgroud)

导轨:

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
Run Code Online (Sandbox Code Playgroud)

用法:

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
Run Code Online (Sandbox Code Playgroud)