de Haley Mnatzaganian

Moștenirea cu o singură masă vs. asociațiile polimorfe în Rails: găsiți ce funcționează pentru dvs.

Mostenirea cu o singura masa vs asociatiile polimorfe in Rails
Fotografie de Sanwal Deen pe Unsplash

Dacă ați creat vreodată o aplicație cu mai multe modele, a trebuit să vă gândiți la ce tip de relații să utilizați între aceste modele.

Pe măsură ce complexitatea unei aplicații crește, poate fi dificil să decideți ce relații ar trebui să existe între modelele dvs.

O situație care apare frecvent este atunci când mai multe dintre modelele dvs. trebuie să aibă acces la funcționalitatea unui al treilea model. Două metode pe care ni le oferă Rails pentru a face față acestui eveniment sunt moștenirea cu o singură masă și asociere polimorfă.

Mostenirea cu o singura masa vs asociatiile polimorfe in Rails

În moștenirea cu un singur tabel (STI), multe subclase moștenesc dintr-o superclasă cu toate datele din același tabel din baza de date. Superclasa are o coloană „tip” pentru a determina cărei subclasă aparține un obiect.

Într-o asociere polimorfă, un model „aparține” altor câteva modele folosind o singură asociere. Fiecare model, inclusiv modelul polimorf, are propriul tabel în baza de date.

Să aruncăm o privire la fiecare metodă pentru a vedea când le-am folosi.

Moștenirea cu o singură masă

O modalitate excelentă de a ști când STI este adecvată este atunci când modelele tale au date / stare partajate. Comportamentul partajat este opțional.

Să ne prefacem că creăm o aplicație care listează diferite vehicule care sunt de vânzare la o reprezentanță locală. Această reprezentanță vinde mașini, motociclete și biciclete.

(Știu că reprezentanțele nu vând biciclete, dar suportă-mă un minut – vei vedea unde mă duc cu asta.)

Pentru fiecare vehicul, reprezentanța dorește să urmărească prețul, culoarea și dacă vehiculul a fost achiziționat. Această situație este un candidat perfect pentru ITS, deoarece folosim aceleași date pentru fiecare clasă.

Putem crea o superclasă Vehicle cu atributele pentru culoare, preț și achiziționat. Fiecare dintre subclasele noastre poate moșteni de la Vehicle și pot obține toți aceleași atribute dintr-o singură lovitură.

Migrarea noastră pentru a crea tabelul vehiculelor ar putea arăta astfel:

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

Este important să creăm type coloană pentru superclasă. Acest lucru îi spune lui Rails că folosim STI și că dorim toate datele Vehicle iar subclasele sale să fie în același tabel din baza de date.

Clasele noastre de modele ar arăta astfel:

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

Această configurare este excelentă, deoarece orice metode sau validări în Vehicle clasa este partajată cu fiecare dintre subclasele sale. Putem adăuga metode unice la oricare dintre subclasele după cum este necesar. Sunt independenți unul de celălalt și comportamentul lor nu este împărțit pe orizontală.

În plus, deoarece știm că subclasele împărtășesc aceleași câmpuri de date, putem efectua aceleași apeluri pe obiecte din clase diferite:

mustang = Car.new(price: 50000, color: red)harley = Motorcycle.new(price: 30000, color: black)
mustang.price=> 50000
harley.price=> 30000
1611675010 615 Mostenirea cu o singura masa vs asociatiile polimorfe in Rails
Umm, unde pot găsi această reprezentanță? (sursă)

Adăugarea funcționalității

Acum, să presupunem că dealerul decide să colecteze mai multe informații despre vehicule.

Pentru Bicycles, vrea să știe dacă fiecare bicicletă este o bicicletă rutieră, montană sau hibridă. Si pentru Cars și Motorcycles, vrea să țină evidența puterii.

Deci, creăm o migrare de adăugat bicycle_type și horsepower la Vehicles masa.

Dintr-o dată, modelele noastre nu mai partajează perfect câmpurile de date. Orice Bicycle obiectul nu va avea un horsepower atribut și orice Car sau Motorcycle nu va avea o bicycle_type (sperăm – voi ajunge la asta într-o clipă).

Cu toate acestea, fiecare bicicletă din masa noastră va avea o horsepower teren, și fiecare mașină și motocicletă va avea un bicycle_type camp.

Aici lucrurile pot deveni lipicioase. Câteva probleme pot apărea în această situație:

  1. Tabelul nostru va avea o mulțime de valori nule (nil în cazul lui Ruby) deoarece obiectele vor avea câmpuri care nu se aplică acestora. Aceste nulls poate provoca probleme pe măsură ce adăugăm validări la modelele noastre.
  2. Pe măsură ce tabelul crește, putem întâlni costuri de performanță atunci când interogăm dacă nu adăugăm filtre. O căutare pentru un anumit bicycle_type se va uita la fiecare articol în tabel – deci nu numai Bicycles, dar Cars și Motorcycles de asemenea.
  3. Ca atare, nimic nu împiedică un utilizator să adauge date „inadecvate” la modelul greșit. De exemplu, un utilizator cu un anumit know-how ar putea crea un Bicycle cu horsepower din 100. Am avea nevoie de validări și un design bun al aplicațiilor pentru a preveni crearea unui obiect nevalid.

Deci, după cum putem vedea, ITS are unele defecte. Este excelent pentru aplicațiile în care modelele dvs. partajează câmpuri de date și este puțin probabil să se schimbe.

STI PROS:

  • Simplu de implementat
  • DRY – salvează codul replicat folosind moștenirea și atributele partajate
  • Permite subclaselor să aibă propriul comportament, după cum este necesar

CONTRA STI:

  • Nu scară bine: pe măsură ce datele cresc, tabelul poate deveni mare și, probabil, dificil de întreținut / interogat
  • Necesită atenție la adăugarea de noi modele sau câmpuri de modele care se abat de la câmpurile partajate
  • (condițional) Permite crearea de obiecte nevalide dacă validările nu sunt în loc
  • (condițional) Poate fi dificil de validat sau interogat dacă există multe valori nule în tabel

Asociații polimorfe

Cu asocieri polimorfe, un model poate belong_to mai multe modele cu o singură asociere.

1611675010 934 Mostenirea cu o singura masa vs asociatiile polimorfe in Rails
E vremea morfinilor. (sursă)

Acest lucru este util atunci când mai multe modele nu au o relație sau partajează date între ele, dar au o relație cu clasa polimorfă.

De exemplu, să ne gândim la un site de socializare precum Facebook. Pe Facebook, atât persoanele, cât și grupurile pot partaja postări.

Persoanele și grupurile nu sunt înrudite (altele decât ambele fiind un tip de utilizator) și, prin urmare, au date diferite. Un grup are probabil câmpuri precum member_count și group_type care nu se aplică unei persoane și viceversa).

Fără asocieri polimorfe, am avea așa ceva:

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

În mod normal, pentru a afla cine deține un anumit profil, ne uităm la coloana care este foreign_key. A foreign_key este un id folosit pentru a găsi obiectul asociat în tabelul modelului asociat.

Cu toate acestea, tabelul nostru de mesaje va avea două chei străine concurente: group_id și person_id. Acest lucru ar fi problematic.

Când încercăm să găsim proprietarul unei postări, ar trebui să facem un punct pentru a verifica ambele coloane pentru a găsi cheia străină corectă, mai degrabă decât să ne bazăm pe una. Ce se întâmplă dacă ajungem într-o situație în care ambele coloane au o valoare?

O asociație polimorfă abordează această problemă condensând această funcționalitate într-o singură asociere. Ne putem reprezenta clasele astfel:

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

Convenția Rails pentru denumirea unei asociații polimorfe utilizează „-able” cu numele clasei (:postable pentru Post clasă). Acest lucru arată clar în relațiile dvs. care clasă este polimorfă. Dar puteți utiliza orice nume pentru asociația dvs. polimorfă care vă place.

Pentru a informa baza noastră de date că folosim o asociere polimorfă, folosim coloane speciale „tip” și „id” pentru clasa polimorfă.

postable_type coloana înregistrează cărui model îi aparține postarea, în timp ce postable_id coloana urmărește ID-ul obiectului proprietar:

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

O asociere polimorfă este doar o combinație de două sau mai multe asociații de apartenență. Din această cauză, puteți acționa la fel cum ați face atunci când utilizați două modele care au o asociere belong_to.

Notă: asociațiile polimorfe funcționează atât cu asociațiile has_one, cât și has_many.

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

O diferență este să mergi „înapoi” de la o postare pentru a-i accesa proprietarul, deoarece proprietarul său ar putea proveni dintr-una din mai multe clase.

Pentru a face acest lucru rapid, trebuie adăugați o coloană cu cheie străină și o coloană tip la clasa polimorfă. Puteți găsi proprietarul unei postări folosind postable:

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

În plus, Rails implementează o anumită securitate în cadrul relațiilor polimorfe. Numai clasele care fac parte din relație pot fi incluse ca postable_type:

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

Avertizare

Asociațiile polimorfe vin cu un steag roșu imens: compromis integritatea datelor.

Mostenirea cu o singura masa vs asociatiile polimorfe in Rails
De Scott Adams din http://dilbert.com/

Într-o relație normală apartin_to, folosim chei străine ca referință într-o asociație.

Totuși, au mai multă putere decât să formeze o legătură. Cheile străine previn, de asemenea, erorile referențiale, cerând ca obiectul la care se face referire în tabelul străin să existe, de fapt.

Dacă cineva încearcă să creeze un obiect cu o cheie străină care face referire la un obiect nul, va primi o eroare.

Din păcate, clasele polimorfe nu pot avea chei străine pentru motive pentru care am discutat. Noi folosim type și id coloane în locul unei chei străine. Aceasta înseamnă că pierdem protecția oferită de cheile străine.

Rails și ActiveRecord ne ajută la suprafață, dar oricine are acces direct la baza de date poate crea sau actualiza obiecte care fac referire la obiecte nule.

De exemplu, verificați această comandă SQL în care este creată o postare, chiar dacă grupul cu care este asociat nu 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

Din fericire, configurarea corectă a aplicației poate împiedica acest lucru. Deoarece aceasta este o problemă serioasă, ar trebui să utilizați asociații polimorfe numai atunci când baza de date este conținută. Dacă alte aplicații sau baze de date trebuie să o acceseze, ar trebui să luați în considerare alte metode.

Asociația polimorfă PROS:

  • Ușor de scalat în cantitatea de date: informațiile sunt distribuite în mai multe tabele de baze de date pentru a minimiza umflarea tabelului
  • Număr ușor la scară de modele: mai multe modele pot fi ușor asociate cu clasa polimorfă
  • DRY: creează o clasă care poate fi utilizată de multe alte clase

Asocierea polimorfă CONS

  • Mai multe tabele pot face interogarea mai dificilă și mai costisitoare pe măsură ce datele cresc. (Găsirea tuturor postărilor care au fost create într-un anumit interval de timp ar trebui să scaneze toate tabelele asociate)
  • Nu pot avea cheie străină. id coloana poate face referire la oricare dintre tabelele de model asociate, ceea ce poate încetini interogarea. Trebuie să funcționeze împreună cu type coloană.
  • Dacă tabelele dvs. sunt foarte mari, se folosește mult spațiu pentru a stoca valorile șirului pentru postable_type
  • Integritatea datelor dvs. este compromisă.

Cum să știi ce metodă să folosești

STI și asociațiile polimorfe au unele suprapuneri atunci când vine vorba de cazuri de utilizare. Deși nu sunt singurele soluții la o relație de tip „copac”, ambele au unele avantaje evidente.

Amandoua Vehicle și Postable exemple ar fi putut fi puse în aplicare folosind oricare dintre metode. Cu toate acestea, au existat câteva motive care au clarificat care este metoda cea mai bună în fiecare situație.

Iată patru factori de luat în considerare atunci când decideți dacă oricare dintre aceste metode se potrivește nevoilor dumneavoastră.

  1. Structura bazei de date. STI utilizează un singur tabel pentru toate clasele din relație, în timp ce asociațiile polimorfe utilizează un tabel pe clasă. Fiecare metodă are propriile sale avantaje și dezavantaje pe măsură ce aplicația crește.
  2. Date sau stare partajate. STI este o opțiune excelentă dacă modelele dvs. au multe atribute comune. În caz contrar, o asociere polimorfă este probabil cea mai bună alegere.
  3. Preocupări viitoare. Luați în considerare modul în care aplicația dvs. s-ar putea schimba și crește. Dacă aveți în vedere STI, dar credeți că veți adăuga modele sau câmpuri de modele care se abat de la structura partajată, vă recomandăm să vă regândiți planul. Dacă credeți că structura dvs. va rămâne probabil aceeași, ITS va continua în general fi mai rapid pentru interogări.
  4. Integritatea datelor. Dacă datele nu vor fi conținute (o aplicație care folosește baza de date), asocierea polimorfă este probabil o alegere proastă, deoarece datele dvs. vor fi compromise.

Gânduri finale

Nici asociațiile ITS, nici asociațiile polimorfe nu sunt perfecte. Amândoi au argumente pro și dezavantaje care adesea fac unul sau altul mai potrivit pentru asociații cu multe modele.

Am scris acest articol pentru a-mi învăța aceste concepte la fel de mult ca să le învăț altcuiva. Dacă este ceva incorect sau vreun punct despre care credeți că ar trebui menționat, vă rugăm să mă ajutați și pe toți ceilalți, împărtășind comentariile!

Dacă ați aflat ceva sau ați găsit acest lucru util, vă rugăm să faceți clic pe ? buton pentru a vă arăta sprijinul!