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.

Примечание: возможно наличие полиморфных ассоциаций с ИППП, но я решил сосредоточить пример на простых полиморфных ассоциациях. Не стесняйтесь изучить это, если вам интересно 😉

При разработке моделей данных вы, вероятно, поймете, когда уместно использовать каждую из них. В конце концов, выбор правильной стратегии может оказаться столь же важным, как и решение проблемы.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *