Спадкування за однотаблицею проти поліморфних асоціацій у Rails: знайдіть, що вам підходить

Якщо ви коли-небудь створювали додаток із кількома моделями, вам доводилось думати про те, який тип відносин використовувати між цими моделями.

У міру зростання складності програми може бути важко вирішити, які взаємозв'язки повинні існувати між вашими моделями.

Часто виникає ситуація, коли кілька ваших моделей повинні мати доступ до функціональних можливостей третьої моделі. Два методи, які Rails дає нам для боротьби з цією подією, - це успадкування однієї таблиці та поліморфна асоціація.

У режимі успадкування однієї таблиці (STI) багато підкласів успадковуються від одного суперкласу з усіма даними в одній таблиці бази даних. Суперклас має стовпець „тип”, щоб визначити, до якого підкласу належить об’єкт.

У поліморфній асоціації одна модель “належить” кільком іншим моделям, що використовують одну асоціацію. Кожна модель, включаючи поліморфну ​​модель, має свою базу даних у базі даних.

Давайте розглянемо кожен метод, щоб побачити, коли ми їх будемо використовувати.

Спадщина за одним столом

Чудовий спосіб дізнатись, коли ІПСШ підходить, це коли ваші моделі мають спільні дані / стан . Спільна поведінка необов’язкова.

Давайте зробимо вигляд, що ми створюємо додаток, в якому перелічено різні транспортні засоби, що продаються у місцевому дилерському центрі. Цей дилерський центр продає машини, мотоцикли та велосипеди.

(Я знаю, що дилерські центри не продають велосипеди, але потерпіть зі мною хвилину - ви побачите, куди я з цим їду.)

Для кожного транспортного засобу автосалон хоче відстежувати ціну, колір та чи був автомобіль придбаний. Ця ситуація є ідеальним кандидатом на ІПСШ, оскільки ми використовуємо однакові дані для кожного класу.

Ми можемо створити суперклас Vehicleз атрибутами кольору, ціни та придбаного. Кожен з наших підкласів може успадковувати Vehicleі може отримувати ці самі атрибути одним махом.

Наша міграція для створення таблиці транспортних засобів може виглядати так:

class CreateVehicles < ActiveRecord::Migration[5.1] def change create_table :vehicles do |t| t.string :type, null: false t.string :color t.integer :price t.boolean :purchased, default: false end end end

Важливо, щоб ми створили typeстовпець для суперкласу. Це повідомляє Rails, що ми використовуємо STI і хочемо, щоб усі дані для Vehicleта його підкласи були в одній таблиці бази даних.

Наші модельні класи виглядатимуть так:

class Vehicle < ApplicationRecordend
class Bicycle < Vehicleend
class Motorcycle < Vehicleend
class Car < Vehicleend

Це налаштування є чудовим, оскільки будь-які методи або перевірки в Vehicleкласі спільно використовуються для кожного з його підкласів. За потреби ми можемо додати унікальні методи до будь-якого з підкласів. Вони незалежні один від одного, і їх поведінка не поділяється горизонтально.

Крім того, оскільки ми знаємо, що підкласи мають однакові поля даних, ми можемо робити однакові виклики для об’єктів з різних класів:

mustang = Car.new(price: 50000, color: red)harley = Motorcycle.new(price: 30000, color: black)
mustang.price=> 50000
harley.price=> 30000

Додавання функціональності

Тепер, припустимо, дилер вирішив зібрати трохи більше інформації про транспортні засоби.

Адже Bicyclesвона хоче знати, чи є кожен велосипед дорожнім, гірським або гібридним. А для Carsі Motorcycles, вона хоче відслідковувати кінські сили.

Отже, ми створюємо міграцію для додавання bicycle_typeта horsepowerдо Vehiclesтаблиці.

Раптом наші моделі вже не ідеально діляться полями даних. Будь-який Bicycleоб’єкт не матиме horsepowerатрибута, а будь-який Carабо Motorcycleне матиме bicycle_type(сподіваюся - я до цього дістанусь за мить).

Проте кожен велосипед на нашому столі матиме horsepowerполе, а кожен автомобіль та мотоцикл - bicycle_typeполе.

Тут все може стати липким. У цій ситуації може виникнути кілька питань:

  1. У нашій таблиці буде багато нульових значень ( nilу випадку Рубі), оскільки об'єкти матимуть поля, які до них не застосовуються. Це nullsможе спричинити проблеми, оскільки ми додаємо перевірки до наших моделей.
  2. У міру зростання таблиці ми можемо зіткнутися з витратами на продуктивність під час запитів, якщо не додамо фільтри. Пошук певного продуктуbicycle_type буде розглядати кожен елемент таблиці - так не тільки Bicycles, але Carsі Motorcyclesтакож.
  3. Як ніколи, ніщо не заважає користувачеві додавати "невідповідні" дані до неправильної моделі. Наприклад, користувач, який володіє деяким ноу-хау, може створити a Bicycleзі horsepowerзначенням 100. Нам потрібні будуть перевірки та хороший дизайн програми, щоб запобігти створенню недійсного об’єкта.

Отже, як ми бачимо, ІПСШ має деякі недоліки. Він чудово підходить для додатків, де ваші моделі діляться полями даних і, швидше за все, не зміняться.

ПІДПРИЄМСТВА:

  • Простий у реалізації
  • DRY - зберігає реплікаційний код за допомогою атрибутів успадкування та спільного використання
  • Дозволяє підкласам мати власну поведінку, якщо це необхідно

ІНТИ ПРОТИ:

  • Масштабується погано: із зростанням даних таблиця може стати великою і, можливо, важкою в обслуговуванні / запиті
  • Потрібна обережність при додаванні нових моделей або полів моделей, що відхиляються від спільних полів
  • (умовно) Дозволяє створювати недійсні об'єкти, якщо перевірки відсутні
  • (умовно) Може бути важко перевірити або здійснити запит, якщо в таблиці існує багато нульових значень

Поліморфні асоціації

З поліморфними асоціаціями модель може belong_toкілька моделей з однією асоціацією.

Це корисно, коли кілька моделей не мають взаємозв'язку або не обмінюються даними між собою, але мають зв'язок з поліморфним класом.

Як приклад, давайте подумаємо про такий сайт у соціальних мережах, як Facebook. У Facebook як окремі особи, так і групи можуть ділитися публікаціями.

Особи та групи не пов’язані між собою (крім того, що обидва вони є типом користувачів), тому вони мають різні дані. Група , ймовірно , має такі поля , як member_countі group_typeщо не застосовується до індивіда, і навпаки).

Без поліморфних асоціацій ми мали б приблизно таке:

class Post belongs_to :person belongs to :groupend
class Person has_many :postsend
class Group has_many :postsend

Зазвичай, щоб дізнатись, кому належить певний профіль, ми переглядаємо стовпець, який є foreign_key. A foreign_key- це ідентифікатор, який використовується для пошуку пов’язаного об’єкта в таблиці пов’язаної моделі.

Однак у нашій таблиці повідомлень будуть два конкуруючі зовнішні ключі: group_idі person_id. Це було б проблематично.

Намагаючись знайти власника допису, нам слід було б вказати, щоб перевірити обидва стовпці, щоб знайти правильний чужорідний ключ, а не покладатися на один. Що станеться, якщо ми потрапимо в ситуацію, коли обидва стовпці мають значення?

A polymorphic association addresses this issue by condensing this functionality into one association. We can represent our classes like this:

class Post belongs_to :postable, polymorphic: trueend
class Person has_many :posts, as :postableend
class Group has_many :posts, as :postableend

The Rails convention for naming a polymorphic association uses “-able” with the class name (:postable for the Post class). This makes it clear in your relationships which class is polymorphic. But you can use whatever name for your polymorphic association that you like.

To tell our database we’re using a polymorphic association, we use special “type” and “id” columns for the polymorphic class.

The postable_type column records which model the post belongs to, while the postable_id column tracks the id of the owning object:

haley = Person.first=> returns Person object with name: "Haley"
article = haley.posts.firstarticle.postable_type=> "Person"
article.postable_id=> 1 # The object that owns this has an id of 1 (in this case a Person)
new_post = haley.posts.new()# Automatically fills in postable_type and postable_id using haley object

A polymorphic association is just a combination of two or more belongs_to associations. Because of this, you can act the same way you would when using two models that have a belongs_to association.

Note: polymorphic associations work with both has_one and has_many associations.

haley.posts# returns ActiveRecord array of posts
haley.posts.first.content=> "The content from my first post was a string..."

One difference is going “backwards” from a post to access its owner, since its owner could come from one of several classes.

To do that quickly, you need to add a foreign key column and a type column to the polymorphic class. You can find the owner of a post using postable:

new_post.postable=> returns Person object
new_post.postable.name=> "Haley"

Additionally, Rails implements some security within polymorphic relationships. Only classes that are part of the relationship can be included as a postable_type:

new_post.update(postable_type: "FakeClass")=> NameError: uninitialized constant FakeClass

Warning

Polymorphic associations come with one huge red flag: compromised data integrity.

In a normal belongs_to relationship, we use foreign keys for reference in an association.

They have more power than just forming a link, though. Foreign keys also prevent referential errors by requiring that the object referenced in the foreign table does, in fact, exist.

If someone tries to create an object with a foreign key that references a null object, they will get an error.

Unfortunately, polymorphic classes can’t have foreign keys for the reasons we discussed. We use the type and id columns in place of a foreign key. This means we lose the protection that foreign keys offer.

Rails and ActiveRecord help us out on the surface, but anyone with direct access to the database can create or update objects that reference null objects.

For example, check out this SQL command where a post is created even though the group it is associated with doesn’t exist.

Group.find(1000)=> ActiveRecord::RecordNotFound: Couldn't find Group with 'id'=1000
# SQLINSERT INTO POSTS (postable_type, postable_id) VALUES ('Group', 1000)=> # returns success even though the associated Group doesn't exist

Thankfully, proper application setup can prevent this from being possible. Because this is a serious issue, you should only use polymorphic associations when your database is contained. If other applications or databases need to access it, you should consider other methods.

Polymorphic association PROS:

  • Easy to scale in amount of data: information is distributed across several database tables to minimize table bloat
  • Easy to scale number of models: more models can be easily associated with the polymorphic class
  • DRY: creates one class that can be used by many other classes

Polymorphic association CONS

  • More tables can make querying more difficult and expensive as the data grows. (Finding all posts that were created in a certain time frame would need to scan all associated tables)
  • Cannot have foreign key. The id column can reference any of the associated model tables, which can slow down querying. It must work in conjunction with the type column.
  • If your tables are very large, a lot of space is used to store the string values for postable_type
  • Your data integrity is compromised.

How to know which method to use

STI and polymorphic associations have some overlap when it comes to use cases. While not the only solutions to a “tree-like” model relationship, they both have some obvious advantages.

Both the Vehicle and Postable examples could have been implemented using either method. However, there were a few reasons that made it clear which method was best in each situation.

Here are four factors to consider when deciding whether either of these methods fits your needs.

  1. Database structure. STI uses only one table for all classes in the relationship, while polymorphic associations use a table per class. Each method has its own advantages and disadvantages as the application grows.
  2. Shared data or state. STI is a great option if your models have many shared attributes. Otherwise a polymorphic association is probably the better choice.
  3. Future concerns. Consider how your application might change and grow. If you’re considering STI but think you’ll add models or model fields that deviate from the shared structure, you might want to rethink your plan. If you think your structure is likely to remain the same, STI will generally be faster for querying.
  4. Data integrity. If data is not going to be contained (one application using your database), polymorphic association is probably a bad choice because your data will be compromised.

Final Thoughts

Neither STI nor polymorphic associations are perfect. They both have pros and cons that often make one or the other more fit for associations with many models.

I wrote this article to teach myself these concepts just as much as to teach them to anyone else. If there is anything incorrect or any points you think should be mentioned, please help me and everyone else out by sharing in the comments!

If you learned something or found this helpful, please click on the ? button to show your support!