Herencia de tabla única frente a asociaciones polimórficas en Rails: encuentre lo que funciona para usted

Si alguna vez ha creado una aplicación con más de un modelo, ha tenido que pensar en qué tipo de relaciones utilizar entre esos modelos.

A medida que aumenta la complejidad de una aplicación, puede resultar difícil decidir qué relaciones deben existir entre sus modelos.

Una situación que surge con frecuencia es cuando varios de sus modelos necesitan tener acceso a la funcionalidad de un tercer modelo. Dos métodos que nos da Rails para lidiar con este evento son la herencia de una sola tabla y la asociación polimórfica.

En Herencia de tabla única (STI), muchas subclases heredan de una superclase con todos los datos en la misma tabla en la base de datos. La superclase tiene una columna de "tipo" para determinar a qué subclase pertenece un objeto.

En una asociación polimórfica , un modelo "pertenece a" varios otros modelos utilizando una sola asociación. Cada modelo, incluido el modelo polimórfico, tiene su propia tabla en la base de datos.

Echemos un vistazo a cada método para ver cuándo los usaríamos.

Herencia de tabla única

Una excelente manera de saber cuándo es apropiado STI es cuando sus modelos han compartido datos / estado . El comportamiento compartido es opcional.

Supongamos que estamos creando una aplicación que enumera diferentes vehículos que están a la venta en un concesionario local. Este concesionario vende automóviles, motocicletas y bicicletas.

(Sé que los concesionarios no venden bicicletas, pero tengan paciencia conmigo un minuto, ya verán a dónde voy con esto).

Para cada vehículo, el concesionario desea rastrear el precio, el color y si se compró el vehículo. Esta situación es un candidato perfecto para STI, porque estamos usando los mismos datos para cada clase.

Podemos crear una superclase Vehiclecon los atributos de color, precio y compra. Cada una de nuestras subclases puede heredar Vehicley todas pueden obtener esos mismos atributos de una sola vez.

Nuestra migración para crear la tabla de vehículos podría verse así:

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

Es importante que creemos la typecolumna para la superclase. Esto le dice a Rails que estamos usando STI y queremos que todos los datos Vehicley sus subclases estén en la misma tabla en la base de datos.

Nuestras clases modelo se verían así:

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

Esta configuración es excelente porque cualquier método o validación en la Vehicleclase se comparte con cada una de sus subclases. Podemos agregar métodos únicos a cualquiera de las subclases según sea necesario. Son independientes entre sí y su comportamiento no se comparte horizontalmente.

Además, como sabemos que las subclases comparten los mismos campos de datos, podemos realizar las mismas llamadas a objetos de diferentes clases:

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

Añadiendo funcionalidad

Ahora digamos que el concesionario decide recopilar más información sobre los vehículos.

Porque Bicyclesquiere saber si cada bicicleta es una bicicleta de carretera, de montaña o híbrida. Y para Carsy Motorcycles, quiere hacer un seguimiento de los caballos de fuerza.

Entonces creamos una migración para agregar bicycle_typey horsepowera la Vehiclestabla.

De repente, nuestros modelos ya no comparten perfectamente los campos de datos. Cualquier Bicycleobjeto no tendrá un horsepoweratributo, y cualquiera Caro Motorcycleno tendrá un bicycle_type(con suerte, llegaré a esto en un momento).

Sin embargo, cada bicicleta de nuestra mesa tendrá un horsepowercampo, y cada automóvil y motocicleta tendrá un bicycle_typecampo.

Aquí es donde las cosas pueden ponerse pegajosas. En esta situación pueden surgir algunos problemas:

  1. Nuestra tabla tendrá muchos valores nulos ( nilen el caso de Ruby) ya que los objetos tendrán campos que no se aplican a ellos. Estos nullspueden causar problemas a medida que agregamos validaciones a nuestros modelos.
  2. A medida que la tabla crece, podemos incurrir en costos de rendimiento al realizar consultas si no agregamos filtros. Una búsqueda de un cierto bicycle_typemirará cada artículo en la mesa- lo que no sólo Bicycles, pero Carsy Motorcyclestambién.
  3. Tal como está, no hay nada que impida que un usuario agregue datos "inapropiados" al modelo incorrecto. Por ejemplo, un usuario con algunos conocimientos técnicos podría crear un Bicyclecon un horsepower100. Necesitaríamos validaciones y un buen diseño de la aplicación para evitar la creación de un objeto no válido.

Entonces, como podemos ver, las ITS tienen algunos defectos. Es ideal para aplicaciones en las que sus modelos comparten campos de datos y es poco probable que cambien.

PROS DE LAS STI:

  • Simple de implementar
  • DRY: guarda el código replicado mediante herencia y atributos compartidos
  • Permite que las subclases tengan su propio comportamiento según sea necesario

STI CONTRAS:

  • Doesn’t scale well: as data grows, table can become large and possibly difficult to maintain/query
  • Requires care when adding new models or model fields that deviate from the shared fields
  • (conditional) Allows creation of invalid objects if validations are not in place
  • (conditional) Can be difficult to validate or query if many null values exist in table

Polymorphic Associations

With polymorphic associations, a model can belong_to several models with a single association.

This is useful when several models do not have a relationship or share data with each other, but have a relationship with the polymorphic class.

As an example, let’s think of a social media site like Facebook. On Facebook, both individuals and groups can share posts.

The individuals and groups are not related (other than both being a type of user), and so they have different data. A group probably has fields like member_count and group_type that don’t apply to an individual, and vice-versa).

Without polymorphic associations, we would have something like this:

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

Normally, to find out who owns a certain profile, we look at the column that is the foreign_key. A foreign_key is an id used to find the related object in the related model’s table.

However, our Posts table would have two competing foreign keys: group_id and person_id. This would be problematic.

When trying to find the owner of a post, we would have to make a point to check both columns to find the correct foreign_key, rather than relying on one. What happens if we run into a situation where both columns have a value?

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!