我试图将不同的模型关联在同一个关系中,就像这样;
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
这是我想要实现的目标。
user.friends.page(4).per(10)
上的朋友进行分页和切换加载。user_id
以确保记录是唯一的。没有user_id
的朋友的属性不能是唯一的,因为值可能会在朋友之间共享。以下是如何使用它的示例。
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
怎么可能解决这个问题?
答案 0 :(得分:4)
我喜欢这个问题。我的回答很长:)
首先,您遇到了问题,因为您的Ruby OOP架构与关系数据库原则相冲突。只需考虑存储数据的表格。通常,您无法将数据存储在不同的表中并使用一个查询进行获取。因为SQL会使用相同类型的记录响应您,但表的行具有不同的类型。
让我们从另一边看。你有用户模型。你有几个朋友模特和他们所有的朋友。因此,即使它们不共享行为,您也必须从一个基类继承它们(我猜想称为Friend),因为您希望在分页中将它们用作具有相同行为的对象(ruby不需要它,但经典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)中描述了这些内容。
alias
属性,而OldFriend则没有,但无论如何,ActiveRecord将为您创建alias
和alias=
方法。从OOP的角度来看,它并不干净。您可以做的就是取消定义这些方法或使用raise 'Not Available for %s' % self.class.name
覆盖它们。您可以使用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)
<强>参考强>
答案 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