Enkeltbord arv vs. polymorfe foreninger i Rails: find hvad der fungerer for dig

Hvis du nogensinde har oprettet en applikation med mere end en model, har du været nødt til at tænke over, hvilken type forhold du skal bruge mellem disse modeller.

Efterhånden som en applikations kompleksitet vokser, kan det være svært at afgøre, hvilke forhold der skal eksistere mellem dine modeller.

En situation, der ofte opstår, er når flere af dine modeller skal have adgang til funktionaliteten i en tredje model. To metoder, som Rails giver os til at tackle denne begivenhed, er arv med enkelt bord og polymorf tilknytning.

I enkeltbordsarv (STI) arver mange underklasser fra en superklasse med alle dataene i den samme tabel i databasen. Superklassen har en "type" kolonne til at bestemme, hvilken underklasse et objekt tilhører.

I en polymorf forening hører en model til flere andre modeller ved hjælp af en enkelt forening. Hver model, inklusive den polymorfe model, har sin egen tabel i databasen.

Lad os se på hver metode for at se, hvornår vi bruger dem.

Enkeltbord arv

En god måde at vide, hvornår STI er passende, er, når dine modeller har delt data / tilstand . Delt adfærd er valgfri.

Lad os foregive, at vi opretter en app, der viser forskellige køretøjer, der er til salg hos en lokal forhandler. Denne forhandler sælger biler, motorcykler og cykler.

(Jeg ved, at forhandlere ikke sælger cykler, men hold med mig et øjeblik - du vil se, hvor jeg skal hen med dette.)

For hvert køretøj vil forhandleren spore pris, farve og om køretøjet blev købt. Denne situation er en perfekt kandidat til STI, fordi vi bruger de samme data for hver klasse.

Vi kan oprette en superklasse Vehiclemed attributterne for farve, pris og købt. Hver af vores underklasser kan arve fra Vehicleog kan alle få de samme attributter på én gang.

Vores migration til at oprette køretøjstabellen kan se sådan ud:

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

Det er vigtigt, at vi opretter typesøjlen til superklassen. Dette fortæller Rails, at vi bruger STI og ønsker, at alle data til Vehicleog dens underklasser skal være i den samme tabel i databasen.

Vores modelklasser ser sådan ud:

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

Denne opsætning er fantastisk, fordi alle metoder eller valideringer i Vehicleklassen deles med hver af dens underklasser. Vi kan tilføje unikke metoder til enhver underklasse efter behov. De er uafhængige af hinanden, og deres adfærd deles ikke vandret.

Da vi ved, at underklasserne deler de samme datafelter, kan vi desuden foretage de samme opkald på objekter fra forskellige klasser:

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

Tilføjelse af funktionalitet

Lad os sige, at forhandleren beslutter at indsamle nogle flere oplysninger om køretøjerne.

For Bicycleshun vil vide, om hver cykel er en vej-, bjerg- eller hybridcykel. Og for Carsog Motorcycleshun vil holde styr på hestekræfterne.

Så vi opretter en migration, der skal tilføjes bicycle_typeog horsepowertil Vehiclestabellen.

Pludselig deler vores modeller ikke datafelter perfekt længere. Ethvert Bicycleobjekt vil ikke have en horsepowerattribut, og nogen Careller Motorcyclevil ikke have en bicycle_type(forhåbentlig - jeg kommer til dette om et øjeblik).

Alligevel vil hver cykel i vores bord have et horsepowerfelt, og hver bil og motorcykel vil have et bicycle_typefelt.

Det er her, ting kan blive klæbrig. Der kan opstå et par problemer i denne situation:

  1. Vores tabel har mange nulværdier ( nili Rubys tilfælde), da objekter vil have felter, der ikke gælder for dem. Disse nullskan forårsage problemer, når vi tilføjer valideringer til vores modeller.
  2. Efterhånden som tabellen vokser, kan vi løbe ind i ydelsesomkostninger, når vi spørger, hvis vi ikke tilføjer filtre. En søgning efter et bestemt bicycle_typevil se på hvert element i tabellen - så ikke kun Bicycles, men Carsog Motorcyclesogså.
  3. Som det er, er der intet, der forhindrer en bruger i at tilføje "upassende" data til den forkerte model. For eksempel kan en bruger med en vis know-how oprette en Bicyclemed en horsepowerpå 100. Vi har brug for valideringer og godt app-design for at forhindre oprettelsen af ​​et ugyldigt objekt.

Så som vi kan se, har STI nogle mangler. Det er fantastisk til applikationer, hvor dine modeller deler datafelter og sandsynligvis ikke ændres.

STI PROS:

  • Enkel at implementere
  • TØRR - gemmer replikeret kode ved hjælp af arv og delte attributter
  • Tillader, at underklasser har egen adfærd efter behov

STI CONS:

  • Skalerer ikke godt: Efterhånden som data vokser, kan tabellen blive stor og muligvis vanskelig at vedligeholde / spørge
  • Kræver pleje, når du tilføjer nye modeller eller modelfelter, der afviger fra de delte felter
  • (betinget) Tillader oprettelse af ugyldige objekter, hvis valideringer ikke er på plads
  • (betinget) Kan være svært at validere eller forespørge, hvis der findes mange nulværdier i tabellen

Polymorfe foreninger

Med polymorfe associeringer kan en model belong_toflere modeller med en enkelt association.

Dette er nyttigt, når flere modeller ikke har et forhold eller deler data med hinanden, men har et forhold til den polymorfe klasse.

Lad os som et eksempel tænke på et socialt medieside som Facebook. På Facebook kan både enkeltpersoner og grupper dele indlæg.

Individerne og grupperne er ikke beslægtede (bortset fra at begge er en type bruger), og derfor har de forskellige data. En gruppe har sandsynligvis felter som, member_countog group_typesom ikke gælder for en person, og omvendt).

Uden polymorfe foreninger ville vi have noget som dette:

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

For at finde ud af, hvem der ejer en bestemt profil, ser vi normalt på den kolonne, der er foreign_key. A foreign_keyer et id, der bruges til at finde det relaterede objekt i den relaterede models tabel.

Imidlertid ville vores Posts-tabel have to konkurrerende udenlandske nøgler: group_idog person_id. Dette ville være problematisk.

Når vi prøver at finde ejeren af ​​et indlæg, bliver vi nødt til at gøre et punkt for at kontrollere begge kolonner for at finde den rigtige fremmednøgle i stedet for at stole på en. Hvad sker der, hvis vi løber ind i en situation, hvor begge kolonner har en værdi?

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.

Jeg skrev denne artikel for at lære mig selv disse begreber lige så meget som for at lære dem til andre. Hvis der er noget forkert eller nogle punkter, som du mener skal nævnes, så hjælp mig og alle andre ved at dele i kommentarerne!

Hvis du har lært noget eller fundet dette nyttigt, skal du klikke på ? knap for at vise din støtte!