#ruby-on-rails #ruby-on-rails-4
#ruby-on-rails #ruby-on-rails-4
Вопрос:
Я пытаюсь связать разные модели в одном и том же отношении, вот так; У User
может быть много разных друзей, но каждая модель друзей отличается как в отношении проверки, так и в отношении данных.
User.first.friends # => [BestFriend, RegularFriend, CloseFriend, ...]
Каждый класс friend имеет разные столбцы, но все они реагируют на один и тот же метод #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
Как можно было бы это решить?
Комментарии:
1. Два метода: STI и полиморфный. Слишком долго объяснять здесь, и тонны объяснений легко найти. Начните отсюда: guides.rubyonrails.org/association_basics.html
2. @BenjaminSinclaire STI и полиморфная ассоциация не будут работать «из коробки» в моем случае (если вы не знаете чего-то, чего не знаю я). Таблица friends (
close_friends
например) должна иметь хотя бы один внешний ключ для проверки ее уникальности. Использование толькоbelongs_to ..., polymorphic: true
onUser
позволит использовать внешний ключ только дляFriend
,User
отношения onUser
.3. Я согласен с @BenjaminSinclaire. Не уверен, почему STI не будет работать. Можете ли вы объяснить это подробнее?
4. @Oleander — Сколько друзей у пользователя в этой системе? Часто ли обновляются данные? В зависимости от этого вы можете либо присвоить идентификаторы друга, либо создать представление базы данных
5. Я предоставил исчерпывающий ответ, который касается вашего вопроса, и вы проголосовали против, несмотря на то, что он взят из реального приложения, не использующего полиморфные объединения, использующего отдельные классы для проверки и функциональность to_s, не требующего отдельных запросов и поддерживающего объединение в цепочки и разбивку на страницы (я прилагаю большие усилия, чтобы объяснить, где у вас возникнут проблемы с AR в будущем). Ваше поведение отражает уровень невежества и оскорбляет всех тех, кто потратил время на предоставление законных ответов. У меня был этот вопрос, помеченный для модерации
Ответ №1:
Мне нравится этот вопрос. Мой ответ будет длинным 🙂
Прежде всего, у вас возникают проблемы, потому что ваша архитектура Ruby ООП противоречит принципам реляционной базы данных. Просто подумайте о таблицах, в которых вы храните свои данные. Обычно вы не можете хранить данные в разных таблицах и извлекать их одним запросом. Потому что SQL отвечает вам записями одного типа, но строки таблиц имеют разные типы.
Давайте посмотрим с другой стороны. У вас есть пользовательская модель. У вас их несколько?Подружите модели, и все они друзья на самом деле. Таким образом, даже если они не имеют общего поведения, вы должны унаследовать их от одного базового класса (называемого Friend, я полагаю), потому что вы хотите использовать их в разбивке на страницы как объекты с одинаковым поведением (это не требуется ruby, но требуется классическим дизайном ООП). Теперь у вас есть что-то подобное в 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. Они описаны Мартином Фаулером в его книге «Архитектура корпоративных приложений» (она же PoEAA).
- Здесь упоминается первое STI (наследование одной таблицы). Это предпочтительный способ, потому что Rails уже имеет встроенную поддержку STI. Также STI позволяет извлекать все данные одним запросом к БД, поскольку это запрос к одной таблице. Но STI имеет некоторые недостатки. Главное, что вы должны хранить все свои данные в одной таблице. Это означает, что все ваши модели будут совместно использовать поля одной таблицы. Например, у вашей модели BestFriend есть
alias
атрибут, а у OldFriend его нет, но в любом случае ActiveRecord создаст для васalias
иalias=
методы. Это не чисто с точки зрения ООП. Все, что вы можете сделать, это отменить определение этих методов или переопределить их с помощьюraise 'Not Available for %s' % self.class.name
. - Второе — это наследование конкретной таблицы. Это не совсем подходит для вас, потому что у вас не будет одной таблицы со всеми записями и вы не сможете выполнить простой запрос для разбивки на страницы (на самом деле вы делаете, но только с некоторыми странными материалами DB). Но неоспоримым преимуществом наследования конкретной таблицы является то, что все ваши знакомые модели будут иметь независимые поля. Это чисто с точки зрения ООП.
- Третье — это наследование таблицы классов. Это несовместимо с шаблоном ActiveRecord без использования представлений базы данных и вместо триггеров, но слишком сложно для вашей проблемы. Наследование таблицы классов совместимо с шаблоном DataMapper, но Rails сильно зависит от ActiveRecord.
Вы можете легко достичь своей цели с помощью решений NoSQL. Вы просто создаете коллекцию друзей в чем-то вроде MongoDB и просто сохраняете все данные в документах коллекции. Но не используйте Mongo 🙂
Итак, я рекомендую вам использовать STI, но у меня есть несколько приятных трюков. Если вы используете PostgreSQL, вы можете хранить все данные, относящиеся к вашей модели, в столбце JSON или hstore и сочетать гибкость NoSQL со строгостью схемы RDBMS и возможностью ActiveRecord. Просто создайте установщики / геттеры для столбцов, специфичных для вашей модели. Вот так:
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. Извините. Я думаю, что это почти публикация в блоге, а не просто ответ. Но я очень увлечен всеми этими вещами, связанными с DB 🙂
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(amp;: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
Ответ №2:
Я бы предложил использовать комбинацию 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)
Ссылка
Комментарии:
1. 1 Это почти именно то, с чем я экспериментировал последние пару дней. Единственное отличие заключается в том, что я использовал flex_columns для хранения своих данных.
2. @Oleander,
store
функция встроена в rails. В базе данных Postgres,store_accessor
наряду сhstore
типом данных, позволяет реализовать NoSQL в СУБД.3. Для этого нет причин использовать hstore. Вы не можете обеспечить ссылочную целостность, если ваши атрибуты ссылаются на другую информацию в связанных таблицах, все является строкой, вы не можете использовать ограничения проверки на уровне базы данных. Обычно я прибегаю к hstore / JSON / JSONB только тогда, когда заранее не знаю всех атрибутов. Поскольку вопрос очень связан с проверкой определенных атрибутов у определенных типов друзей, этот ответ не соответствует этим целям.
4. Хочу добавить, что, хотя это отличные функции и postgres имеет отличную поддержку индексации, они все еще работают не так быстро, как обычный столбец для чтения, а индексы GIst / gin намного больше индексов и намного медленнее при создании / обновлении.
5. @AndrewHacking, OP был заинтересован в проверке уникальности, которая достигается с помощью
belongs_to
ассоциации, предложенной в моем ответе. Из описания проблемы пользователя я понял, что он пытается сохранить расширенные атрибуты в отдельных моделях. Если вы видите комментарий OP, он собирался использовать github.com/ageweke/flex_columns . Мой ответ предлагает собственное решение для такого варианта использования.
Ответ №3:
Это непроверено, но, как мысль, вы могли бы попробовать создать несколько моделей, используя одну и ту же таблицу, каждая с разной областью по умолчанию.
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
у вас все еще есть has_many :friends
в классе User, надеюсь, когда записи будут прочитаны, область видимости будет соответствовать записи таблицы соответствующей модели.
Редактировать
Поскольку единственное, на что вам нужно отвечать для всех записей, это :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
Комментарии:
1. Я не думаю, что это сработает. Пользовательский класс по-прежнему будет загружать данные в
Friend
класс, который не существует. Пользовательский класс должен просмотреть (в вашем случае)friendship
столбец и передать данные этому классу после загрузки содержимого из базы данных.2. Дай мне попробовать … Подожди.
3. Да, вы правы. 🙁 неинициализированный постоянный пользователь::Friend … Я не верю, что мы можем получить обратный вызов after_find для возврата записи замены. Но имеет ли значение, есть ли у вас класс Friend? Поскольку единственное, на что вам нужно отвечать для всех записей, это :to_s, у вас мог бы быть класс User с … def to_s; case friendship; когда ‘best’; object = BestFriend.find(self.id );когда ‘regular’;объект = RegularFriend.find(self.id );end;object.to_s; end
4. Трудно прочитать мой комментарий… Я изменил ответ, чтобы сделать его более понятным!
5. Хотя я ценю, что это дополнительный ввод-вывод … : (
Ответ №4:
Наследование одной таблицы
Вероятно, вам лучше всего использовать STI
, согласно комментариям
Проблема в том, что вы ссылаетесь на один и тот же набор данных в разных вариантах. «Лучший», «Близкий» и «Обычный» — это все типы друзей, что означает, что будет намного лучше (и в соответствии с принципом единого источника истины) хранить их в одной таблице / модели
Вы могли бы сделать это:
#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")
Комментарии:
1. Как я объяснил выше относительно первого ответа; проблема в том, что у меня есть
n
классы друзей. В вашем первом примере это заставит меня добавить n строк has_many и выполнить n запросов для извлечения всех данных (разбивка на страницы невозможна), что не является относительным. Второй пример больше не будет работать, так как мне нужныn
разные классы, а не только один дружественный класс.2. Кто сказал, что вам нужны
n
разные классы? в чем причина?3. Каждая дружественная модель требует своих собственных проверок, поскольку у них разные столбцы. Вот почему для вашего решения требуются
n
строки, по одной для каждого класса друзей.
Ответ №5:
Я сделал это с помощью STI (наследование одной таблицы), но с ActiveRecord проблема заключается в том, что у вас ограниченная способность выражать условия объединения и вы не можете выполнять такие вещи, как union all, когда дело доходит до поиска связанной информации, даже если вы точно знаете, какой sql вам нужен.
Например, найти всех друзей и подруг друзей, которые размещали публикации за последние x дней с ‘foo’ в названии или в тренде. Поиск простых связей между организацией и пользователем невозможен, если соответствующая информация не перенесена в таблицу. Конечно, AR / Arel будет генерировать запросы, но не тот, который вы хотите, независимо от того, что вы делаете.
И вы не можете использовать строки SQL для объединения, поскольку вы получаете массив, а не обратную связь, что означает, что объединение в цепочку не работает.
Я перешел к Sequel, где я действительно могу составлять свои запросы и получать доступ к полезным функциям sql, выходящим за рамки простых шаблонов crud, для которых AR хорош.
Вот пример AR (с оговоркой выше, что AR имеет серьезные ограничения по сравнению с sequel), который определяет участник (люди, Организации, пользователи и их подтипы — использует 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
Комментарии:
1. -1 Извините, но это не решает ни одной из моих проблем.
2. Это предназначено для решения проблем, о которых вы, вероятно, еще не знаете. Я прошел через то, что вы делаете, все работало с расширенными отношениями, а затем обнаружил, что AR не может эффективно запрашивать таблицы из-за недостатков. Способ, которым AR выполняет свои объединения, не причинит вам бесконечной боли. Желаю удачи.
3. @Oleander Я обновил, чтобы включить модели AR и миграции из рабочего приложения, пожалуйста, уберите голосование «Против».
Ответ №6:
На самом деле вы не можете сделать это с помощью наследования одной таблицы и полиморфных ассоциаций… эти методы предназначены для записи определенного класса, которая может принадлежать записи для одного из нескольких разных классов.
То, что вы делаете, в значительной степени противоположно… записи во многих разных классах могут принадлежать записи в общем классе.
Я думаю, вам нужно будет указать отношения has_many по отдельности, но создать метод экземпляра «friends», который возвращает сумму массивов.
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
Комментарии:
1.Проблема в том, что у меня есть
n
классы друзей. В вашем примере это заставит меня добавлятьn
has_many
строки и выполнятьn
запросы, что не является относительным.2. Хорошо, я отредактировал ответ, чтобы включить обобщенный метод, но я его не тестировал. У кого-то могут быть предложения получше.
3. Для этого потребовались бы
n
запросы к базе данных. Извините.4. Ну, да, у вас есть
n
таблицы, которые вы хотите запросить. Я все еще склонен думать, что вашим лучшим решением является одна таблица с атрибутом type… которое вы можете использовать для проверки условий. Удачи!5. Я надеялся избежать условных проверок, используя один единственный класс. Было бы лучше, если бы можно было использовать одну таблицу (как вы сказали) и разделить логику на разные классы.