ActiveRecord vs. Partea a doua Ecto

Aceasta este a doua parte a seriei „ActiveRecord vs. Ecto”, în care Batman și Batgirl luptă pentru bazele de date interogative și comparăm mere și portocale.

După analizarea schemelor de baze de date și a migrațiilor în ActiveRecord și Ecto partea întâi, acest post acoperă modul în care ActiveRecord și Ecto permit dezvoltatorilor să interogheze baza de date și cum se compară atât ActiveRecord cât și Ecto atunci când se ocupă de aceleași cerințe. Pe parcurs, vom afla și identitatea lui Batgirl în perioada 1989-2011.

Date despre semințe

Să începem! Pe baza structurii bazei de date definită în primul post al acestei serii, presupunem că utilizatorii și tabelele de facturi au următoarele date stocate în ele:

utilizatori

* Câmpul creat_at al ActiveRecord este numit implicit inserat_at în Ecto.

facturi

* Câmpul creat_at al ActiveRecord este numit implicit inserat_at în Ecto.

Întrebările efectuate prin acest post presupun că datele de mai sus sunt stocate în baza de date, deci țineți minte aceste informații în timp ce o citiți.

Găsiți elementul folosind cheia principală

Să începem să obținem o înregistrare din baza de date folosind cheia principală.

ActiveRecord

irb (principal): 001: 0> User.find (1) Încărcarea utilizatorului (0.4ms) SELECTĂ "utilizatorii". * DE la "utilizatori" UNDE "utilizatori". "id" = $ 1 LIMITE $ 2 [["id", 1 ], ["LIMIT", 1]] => # 

ecto

iex (3)> Repo.get (utilizator, 1)
[debug] QUERY OK source = "utilizatori" db = 5,2ms decode = 2,5ms coadă = 0,1ms
SELECTĂ u0. "Id", u0. "Nume complet", u0. "E-mail", u0. "Inserit_at", u0.
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: încărcat, „utilizatori”>,
  e-mail: „[email protected]”,
  nume complet: "Bette Kane",
  id: 1,
  inserat_at: ~ N [2018-01-01 10: 01: 00.000000],
  facturi: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [2018-01-01 10: 01: 00.000000]
}

Comparaţie

Ambele cazuri sunt destul de similare. ActiveRecord se bazează pe metoda find class a clasei de model User. Înseamnă că fiecare clasă de copii ActiveRecord are propria metodă de găsire în ea.

Ecto utilizează o abordare diferită, bazându-se pe conceptul Repository ca mediator între stratul de mapare și domeniu. Când utilizați Ecto, modulul User nu are cunoștințe despre cum se poate găsi. O astfel de responsabilitate este prezentă în modulul Repo, care este capabil să-l asigure pe baza de date de dedesubt, care este în cazul nostru Postgres.

Atunci când comparăm interogarea SQL în sine, putem observa câteva diferențe:

  • ActiveRecord încarcă toate câmpurile (utilizatori. *), În timp ce Ecto încarcă doar câmpurile enumerate în definiția schemei.
  • ActiveRecord include un LIMIT 1 la interogare, în timp ce Ecto nu.

Obținerea tuturor articolelor

Haideți să facem un pas mai departe și să încărcăm toți utilizatorii din baza de date.

ActiveRecord

irb (principal): 001: 0> User.all User Load (0,5ms) SELECTĂ "utilizatori". * DIN "utilizatori" LIMIT $ 1 [["LIMIT", 11]] => # , # , # , # ]>

ecto

iex (4)> Repo.all (utilizator)
[debug] QUERY OK source = "utilizatori" db = 2.8ms decode = 0.2ms coada = 0.2ms
SELECTĂ u0.
[
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: încărcat, „utilizatori”>,
    e-mail: „[email protected]”,
    nume complet: "Bette Kane",
    id: 1,
    inserat_at: ~ N [2018-01-01 10: 01: 00.000000],
    facturi: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-01 10: 01: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: încărcat, „utilizatori”>,
    e-mail: "[email protected]",
    nume complet: "Barbara Gordon",
    id: 2,
    inserat_at: ~ N [2018-01-02 10: 02: 00.000000],
    facturi: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-02 10: 02: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: încărcat, „utilizatori”>,
    e-mail: „[email protected]”,
    nume complet: "Cassandra Cain",
    id: 3,
    inserat_at: ~ N [2018-01-03 10: 03: 00.000000],
    facturi: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-03 10: 03: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: încărcat, „utilizatori”>,
    e-mail: „[email protected]”,
    nume complet: "Stephanie Brown",
    id: 4,
    inserat_at: ~ N [2018-01-04 10: 04: 00.000000],
    facturi: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-04 10: 04: 00.000000]
  }
]

Comparaţie

Urmează exact același tipar ca secțiunea anterioară. ActiveRecord folosește metoda all class și Ecto se bazează pe modelul de depozit pentru a încărca înregistrările.

Există din nou unele diferențe în interogările SQL:

Interogare cu condiții

Este foarte puțin probabil să fie nevoie să preluăm toate înregistrările dintr-un tabel. O nevoie comună este utilizarea condițiilor, pentru a filtra datele returnate.

Să folosim acest exemplu pentru a enumera toate facturile care mai sunt de plătit (UNDE plătit_ este NUL).

ActiveRecord

irb (principal): 024: 0> Factură.unde (plătit_at: nil) Încărcarea facturii (18.2ms) SELECTĂ "facturile". * DIN "facturi" UNDE "facturi". "plătit_at" ESTE LIMIT NULL $ 1 [["LIMIT" , 11]] => # , # ]>

ecto

iex (19)> unde (Factură, [i], este_nil (i.paid_at)) |> Repo.all ()
[debug] QUERY OK source = "factures" db = 20.2ms
SELECT i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. NUL) []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 3,
    inserat_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    Payment_method: nil,
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 4,
    inserat_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    Payment_method: nil,
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 4
  }
]

Comparaţie

În ambele exemple, se folosește cuvântul cheie unde este o conexiune la clauza SQL WHERE. Deși interogările SQL generate sunt destul de similare, modul în care ambele instrumente ajung acolo au unele diferențe importante.

ActiveRecord transformă în mod automat argumentul pay_at: nil în pay_at IS NULL instrucțiunea SQL. Pentru a ajunge la aceeași ieșire folosind Ecto, dezvoltatorii trebuie să fie mai explicit cu privire la intenția lor, apelând is_nil ().

O altă diferență de subliniat este comportamentul „pur” al funcției unde se află în Ecto. Când se apelează numai la funcția unde, nu interacționează cu baza de date. Întoarcerea funcției unde este o structură Ecto.Query:

iex (20)> unde (Factură, [i], este_nil (i.paid_at))
# Ecto.Query 

Baza de date este atinsă doar când funcția Repo.all () este apelată, trecând structura Ecto.Query ca argument. Această abordare permite compoziția de interogare în Ecto, care este subiectul secțiunii următoare.

Compoziția de interogare

Unul dintre cele mai puternice aspecte ale interogărilor de baze de date este compoziția. Descrie o interogare într-un mod care conține mai mult de o singură condiție.

Dacă creați interogări SQL brute, înseamnă că veți folosi probabil un fel de concatenare. Imaginați-vă că aveți două condiții:

  1. not_paid = 'pay_at NU ESTE NULL'
  2. paid_with_paypal = 'Payment_method = "Paypal"'

Pentru a combina aceste două condiții folosind SQL brut, înseamnă că va trebui să le concateni folosind ceva similar cu:

SELECT * DIN facturile WHERE # {not_paid} AND # {paid_with_paypal}

Din fericire, atât ActiveRecord cât și Ecto au o soluție pentru asta.

ActiveRecord

irb (principal): 003: 0> Factură.unde.not (plătit_at: nil). Unde (plata_metod: "Paypal") Încărcarea facturilor (8,0ms) SELECTEAZĂ "facturile". * DIN "facturi" UNDE "facturi". " paid_at "NU ESTE NULL ȘI" facturi "." Payment_method "= 1 $ LIMIT $ 2 [[" Payment_method "," Paypal "], [" LIMIT ", 11]] => # ]>

ecto

iex (6)> Factură |> unde ([i], nu este_nil (i.paid_at))>> unde ([i], i.payment_method == "Paypal") |> Repo.all ()
[debug] QUERY OK source = "factures" db = 30.0ms decode = 0.6ms coada = 0.2ms
SELECTĂ i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "IS NULL)) ȘI (i0." Payment_method "= 'Paypal') []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 2,
    inserat_at: ~ N [2018-01-03 08: 00: 00.000000],
    pay_at: #DataTime <2018-02-01 08: 00: 00.000000Z>,
    Payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Comparaţie

Ambele întrebări răspund la aceeași întrebare: „Ce facturi au fost plătite și utilizate Paypal?”.

După cum se aștepta deja, ActiveRecord oferă un mod mai succint de a compune interogarea (de exemplu), în timp ce Ecto cere dezvoltatorilor să cheltuiască un pic mai mult în scrierea interogării. Ca de obicei, Batgirl (Orfanul, mut unul cu identitatea Cassandra Cain) sau Activerecord nu este la fel de veros.

Nu vă lăsați păcăliți de veridicitatea și complexitatea aparentă a interogării Ecto prezentate mai sus. Într-un mediu mondial real, această interogare va fi rescrisă pentru a arăta mai mult ca:

Factura fiscala
|> unde ([i], nu este_nil (i.paid_at))
|> unde ([i], i.payment_method == "Paypal")
|> Repo.all ()

Văzând din acest unghi, combinația dintre aspectele „pure” ale funcției în care, care nu efectuează operațiunile bazei de date de unul singur, cu operatorul de conductă, face ca compoziția de interogare în Ecto să fie curată.

ordonare

Comanda este un aspect important al unei interogări. Permite dezvoltatorilor să se asigure că un rezultat de interogare dat urmează o ordine specificată.

ActiveRecord

irb (principal): 002: 0> Invoice.order (created_at:: desc) Încărcarea facturilor (1.5ms) SELECTĂ "facturile". * DIN "facturi" COMANDĂ CU "facturi". "create_at" DESC LIMIT $ 1 [["LIMIT ", 11]] => # , # , # , # ]>

ecto

iex (6)> order_by (Factură, desc:: inserează_at) |> Repo.all ()
[debug] QUERY OK source = "facturi" db = 19.8ms
SELECTĂ i0. "Id", i0. "Plată_metodă", i0. "Plătit_at", i0. "User_id", i0. "Inserit_at", i0. []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 3,
    inserat_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    Payment_method: nil,
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 4,
    inserat_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    Payment_method: nil,
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 4
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 2,
    inserat_at: ~ N [2018-01-03 08: 00: 00.000000],
    pay_at: #DataTime <2018-02-01 08: 00: 00.000000Z>,
    Payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 2
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 1,
    inserat_at: ~ N [2018-01-02 08: 00: 00.000000],
    pay_at: #DataTime <2018-02-01 08: 00: 00.000000Z>,
    Payment_method: "Card de credit",
    updated_at: ~ N [2018-01-02 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 1
  }
]

Comparaţie

Adăugarea de ordine la o interogare este simplă în ambele instrumente.

Deși exemplul Ecto folosește ca prim parametru o factură, funcția order_by acceptă și structurile Ecto.Query, care permite funcției order_by să fie utilizată în compoziții, cum ar fi:

Factura fiscala
|> unde ([i], nu este_nil (i.paid_at))
|> unde ([i], i.payment_method == "Paypal")
|> order_by (desc:: insert_at)
|> Repo.all ()

limitativ

Care ar fi o bază de date fără limită? Un dezastru. Din fericire, atât ActiveRecord cât și Ecto ajută la limitarea numărului de înregistrări returnate.

ActiveRecord

irb (principal): 004: 0> Factură.limit (2)
Încărcarea facturilor (0.2ms) SELECTA „facturile”. * DIN „facturi” LIMIT $ 1 [[„LIMIT”, 2]]
=> # , # ]>

ecto

iex (22)> limită (Factură, 2) |> Repo.all ()
[debug] QUERY OK source = "factures" db = 3.6ms
SELECT i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "facturi" AS i0 LIMIT 2 []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 1,
    inserat_at: ~ N [2018-01-02 08: 00: 00.000000],
    pay_at: #DataTime <2018-02-01 08: 00: 00.000000Z>,
    Payment_method: "Card de credit",
    actualizat_at: ~ N [2018-01-02 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 1
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 2,
    inserat_at: ~ N [2018-01-03 08: 00: 00.000000],
    pay_at: #DataTime <2018-02-01 08: 00: 00.000000Z>,
    Payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Comparaţie

Atât ActiveRecord, cât și Ecto au o modalitate de a limita numărul de înregistrări returnate de o interogare.

Limita Ecto funcționează similar cu comanda_by, fiind potrivită pentru compoziții de interogare.

Asociațiile

ActiveRecord și Ecto au abordări diferite în ceea ce privește modul în care se gestionează asociațiile.

ActiveRecord

În ActiveRecord, puteți utiliza orice asociere definită într-un model, fără a fi necesar să faceți ceva special în acest sens, de exemplu:

irb (principal): 012: 0> user = User.find (2) Încărcarea utilizatorului (0.3ms) SELECTĂ „utilizatorii". * DE la „utilizatori" UNDE "utilizatori". "id" = 1 $ LIMIT $ 2 [["id" , 2], ["LIMIT", 1]] => #  irb (principal): 013: 0> user.invoices Încărcarea facturilor (0,4ms) SELECTĂ" facturile ". * DIN" facturi "UNDE" facturi " . "user_id" = 1 $ LIMIT $ 2 [["user_id", 2], ["LIMIT", 11]] => # ] >

Exemplul de mai sus arată că putem apela o listă a facturilor utilizatorului atunci când apelăm la user.invoices. Când faceți acest lucru, ActiveRecord a interogat automat baza de date și a încărcat facturile asociate utilizatorului. În timp ce această abordare ușurează lucrurile, în sensul de a scrie mai puțin cod sau de a vă face griji cu privire la pași suplimentari, poate fi o problemă dacă iterați un număr de utilizatori și preluați facturile pentru fiecare utilizator. Această problemă este cunoscută sub denumirea de „problema N + 1”.

În ActiveRecord, soluția propusă pentru „N + 1 problem” este să utilizeze metoda include:

irb (principal): 022: 0> user = User.include (: facturi) .find (2) Sarcina utilizatorului (0.3ms) SELECTĂ "utilizatori". * DE LA "utilizatori" UNDE "utilizatori". "id" = $ 1 LIMIT $ 2 [["id", 2], ["LIMIT", 1]] Încărcarea facturilor (0.6ms) SELECTĂ "facturile". * DIN "facturi" UNDE "facturi". "User_id" = $ 1 [["user_id", 2]] => #  irb (principal): 023: 0> user.invoices => # ]>

În acest caz, ActiveRecord încarcă cu nerăbdare asocierea facturilor la preluarea utilizatorului (așa cum se vede în cele două interogări SQL afișate).

ecto

După cum ați observat deja, Ecto nu-i place nici magia sau implicarea. Este necesar ca dezvoltatorii să fie expliciți cu privire la intențiile lor.

Să încercăm aceeași abordare a utilizării facturilor user.in Ecto:

iex (7)> ​​user = Repo.get (utilizator, 2)
[debug] QUERY OK source = "utilizatori" db = 18.3ms decode = 0.6ms
SELECTĂ u0. "Id", u0. "Nume complet", u0. "E-mail", u0. "Inserit_at", u0.
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: încărcat, „utilizatori”>,
  e-mail: "[email protected]",
  nume complet: "Barbara Gordon",
  id: 2,
  inserat_at: ~ N [2018-01-02 10: 02: 00.000000],
  facturi: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [2018-01-02 10: 02: 00.000000]
}
iex (8)> user.invoices
# Ecto.Association.NotLoaded 

Rezultatul este un Ecto.Association.NotLoaded. Nu este atât de util.

Pentru a avea acces la facturi, un dezvoltator trebuie să anunțe Ecto despre asta, folosind funcția de preîncărcare:

iex (12)> user = preîncărcare (utilizator,: facturi) |> Repo.get (2)
[debug] QUERY OK source = "utilizatori" db = 11.8ms
SELECTĂ u0. "Id", u0. "Nume complet", u0. "E-mail", u0. "Inserit_at", u0.
[debug] QUERY OK source = "facturi" db = 4.2ms
SELECT i0. "Id", i0. "Plată_metod", i0. "Plătit_at", i0. "User_id", i0. "Inserit_at", i0. "Actualizat_at", i0. i0. "user_id" = $ 1) COMANDĂ prin i0. "user_id" [2]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: încărcat, „utilizatori”>,
  e-mail: "[email protected]",
  nume complet: "Barbara Gordon",
  id: 2,
  inserat_at: ~ N [2018-01-02 10: 02: 00.000000],
  facturi: [
    % Financex.Accounts.Invoice {
      __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
      id: 2,
      inserat_at: ~ N [2018-01-03 08: 00: 00.000000],
      pay_at: #DataTime <2018-02-01 08: 00: 00.000000Z>,
      Payment_method: "Paypal",
      updated_at: ~ N [2018-01-03 08: 00: 00.000000],
      utilizator: # Ecto.Association.NotLoaded ,
      user_id: 2
    }
  ],
  updated_at: ~ N [2018-01-02 10: 02: 00.000000]
}

iex (15)> user.invoices
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: încărcat, "facturi">,
    id: 2,
    inserat_at: ~ N [2018-01-03 08: 00: 00.000000],
    pay_at: #DataTime <2018-02-01 08: 00: 00.000000Z>,
    Payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    utilizator: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

În mod similar ActiveRecord include, preîncărcare cu preluarea facturilor asociate, ceea ce le va face disponibile la apelarea user.invoices.

Comparaţie

Încă o dată, bătălia dintre ActiveRecord și Ecto se încheie cu un punct cunoscut: explicarea. Ambele instrumente permit dezvoltatorilor să acceseze cu ușurință asociațiile, dar în timp ce ActiveRecord îl face mai puțin verosimil, rezultatul acestuia poate avea comportamente neașteptate. Ecto urmează tipul de abordare WYSIWYG, care face doar ceea ce se vede în interogarea definită de dezvoltator.

Rails este bine cunoscut pentru utilizarea și promovarea strategiilor de cache la toate nivelurile diferite ale aplicației. Un exemplu este folosirea abordării de cache a „păpușii rusești”, care se bazează în totalitate pe „problema N + 1” pentru ca mecanismul său de cache să își îndeplinească magia.

validări

Majoritatea validărilor prezente în ActiveRecord sunt disponibile și în Ecto. Iată o listă de validări comune și modul în care atât ActiveRecord cât și Ecto le definesc:

Învelire

Acolo îl aveți: merele esențiale comparativ cu portocalele

ActiveRecord se concentrează pe ușurința de a efectua interogări în baza de date. Marea majoritate a caracteristicilor sale este concentrată pe clasele de model în sine, nefiind necesar ca dezvoltatorii să aibă o înțelegere profundă a bazei de date și nici impactul acestor operațiuni. ActiveRecord face implicit o mulțime de lucruri implicit. Deși acest lucru face mai ușor să începeți, este mai dificil să înțelegeți ce se întâmplă în spatele scenei și funcționează numai dacă urmați „calea ActiveRecord”.

Ecto, pe de altă parte, necesită o mărturie explicată, ceea ce duce la mai multe coduri verbose. Ca beneficiu, totul este în centrul atenției, nimic în spatele scenei și puteți specifica propriul mod.

Ambele au rapoartele lor în funcție de perspectiva și preferințele tale. Prin urmare, comparând mere și portocale, ajungem la sfârșitul acestui BAT-tle. Aproape că am uitat să-ți spun că numele de cod al lui BatGirl (1989-2001) a fost ... Oracol. Dar să nu intrăm în asta.

Acest post este scris de autorul invitat Elvio Vicosa. Elvio este autorul cărții Phoenix for Rails Developers.

Publicat inițial pe blog.appsignal.com pe 9 octombrie 2018.