STI, полиморфизм и абстрактные классы — Rails
Бывают случаи, когда определенные модели должны иметь общее поведение, но их идентичность различается. Rails предоставляет некоторые встроенные функции для обработки таких ситуаций, мы рассмотрим 3 из них.
Абстрактные базовые классы
Абстрактный базовый класс в модели Rails — это просто модель, которая не является постоянной, т. е. не поддерживается таблицей. Это будет выглядеть так:
# app/models/citizen.rb
class Citizen < ApplicationRecord
self.abstract_class = true
end
Объявление модели абстрактной сообщает Rails, что она не является постоянной и будет использоваться для совместного использования функциональности со своими подклассами через наследование.
Скажем, у нас есть две другие модели, которые представляют разные категории граждан: electorate
а также candidate
. Эти модели могут иметь общие свойства, такие как fullname
а также eligible
.
Предположим, что в выбранной нами стране минимальный возраст для участия в предвыборной деятельности составляет 18 лет. Тогда модели могут выглядеть так:
# app/models/citizen.rb
class Citizen < ApplicationRecord
self.abstract_class = true
def fullname
"#{first_name} #{last_name}"
end
def eligible?
age >= 18
end
end
# app/models/electorate.rb
class Electorate < Citizen
...
end
# app/models/candidate.rb
class Candidate < Citizen
...
end
Как вы справедливо заметили, модели кандидата и электората имеют age
, first_name
а также last_name
поля. Теперь они оба могут использовать методы, определенные в Citizen
модель. Методы класса и экземпляра, константы и другие члены класса, которые могут быть введены через включение модуля, передаются подклассам в этой иерархии наследования, однако рекомендуется не превращать абстрактный базовый класс в свалку под видом общей функциональности. .
Уместно отметить, что в этой установке citizen
не имеет базовой таблицы, но electorate
а также candidate
имеют базовые таблицы.
Наследование одной таблицы (STI)
Иногда у вас есть модели, которые имеют некоторые общие атрибуты, но также имеют несколько разных. STI — одно из средств Rails для таких ситуаций.
В настройке STI у вас есть модель, которая является родительской (или super
) к другим моделям. Эта родительская модель должен содержать поле с именем type
без необходимости значения по умолчанию. Поле типа автоматически сохраняет имя дочерней модели (подкласса), к которой принадлежит запись. Принимая наши citizen
пример:
# migration file to add field type to citizen
def change
add_column :citizens, :type, :string
end
# app/models/citizen.rb
class Citizen < ApplicationRecord
self.abstract_class = true # remove this line for STI
...
end
# app/models/electorate.rb
class Electorate < Citizen
...
end
# app/models/candidate.rb
class Candidate < Citizen
...
end
Example
>> c = Candidate.create
>> c.type
=> "Candidate"
>> Citizen.first
=> #<Candidate:0x231456...>
В этом случае electorate
а также candidate
модели не обязательно должны иметь базовые таблицы. Нужна только таблица citizens
стол. Как видно из приведенного выше примера, для всех подклассов Rails автоматически определяет модель, к которой принадлежит запись.
Поскольку все подклассы используют одну и ту же таблицу, у вас не может быть одного и того же атрибута в двух подклассах с разными типами данных. По мере того, как таблица STI становится все больше и больше, в ней может быть слишком много null
поля. Поля, которые существуют только в подклассе, будут нулевыми для других подклассов. Есть и другие плюсы и минусы ИППП, но об этом можно было бы поговорить в другой раз.
Полиморфные ассоциации
Бывают ситуации, когда у вас есть модель, которая belongs_to
больше, чем одна другая модель. Rails предоставляет полиморфные ассоциации для этого варианта использования, когда принадлежащая модель имеет имя ассоциации, которое по соглашению имеет able
постфикс. Эта модель должен имеют два поля, которые описывают идентификатор и тип (класс) ассоциированной записи, оканчивающиеся на _id
а также _type
. Ассоциацию можно рассматривать как интерфейс, который эта модель предоставляет другим моделям. Давайте посмотрим на Vote
модель для примера нашего гражданина.
# migration file for votes
def change
create_table :votes do |t|
...
t.references :votable, polymorphic: true, index: true ...
end
end
# app/models/vote.rb
class Vote < ApplicationRecord
belongs_to :votable, polymorphic: true
...
end
# app/models/electorate.rb
class Electorate < ApplicationRecord
has_many :votes, as: :votable
...
end
# app/models/candidate.rb
class Candidate < ApplicationRecord
has_many :votes, as: :votable
...
end
Example
>> c = Candidate.create>> c.votes
>> [#<Vote:0x0003437fdd79a91d0 id:1, votable_type: "Candidate", votable_id: 1...>]
>> v = Vote.first
>> v.votable
>> #<Candidate:0x02332fed3903e id:1, ...>
Ассоциация работает как на belongs_to
иhas_many
— стороны, используя столбцы id и type.
Rails и Active Record обеспечивают некоторую безопасность, гарантируя, что type
а также id
записи, сохраненной в полиморфной модели, представляют собой фактическую запись, принадлежащую этой цепочке отношений. Однако, если кто-то имеет доступ к вашей базе данных, он может создавать потерянные записи, потому что полиморфные ассоциации не имеют ограничений внешнего ключа, характерных для типичных belongs_to
ассоциация.
>> Candidate.find(50)
>> ActiveRecord::RecordNotFound: Couldn't find Candidate with 'id'=50...
>> Vote.create!(votable_id: 50, votable_type: "Candidate")
>> # Candidate Load (0.3ms) SELECT "candidates".* FROM "candidates" WHERE "candidates"."id" = $1 LIMIT $2 [["id", 50], ["LIMIT", 1]]
>> ActiveRecord::RecordInvalid: Validation failed: Votable must exist
SQL Shell
>> INSERT INTO votes (votable_id, votable_type, created_at, updated_at) VALUES (50, 'Candidate', '02-05-2019', '02-05-19');
# The above query succeeds even though a candidate with that id doesn't exist.
Примечание: возможно наличие полиморфных ассоциаций с ИППП, но я решил сосредоточить пример на простых полиморфных ассоциациях. Не стесняйтесь изучить это, если вам интересно 😉
При разработке моделей данных вы, вероятно, поймете, когда уместно использовать каждую из них. В конце концов, выбор правильной стратегии может оказаться столь же важным, как и решение проблемы.