ActiveRecord vs. Ecto Part Two

Questa è la seconda parte della serie "ActiveRecord vs. Ecto", in cui Batman e Batgirl combattono per interrogare database e confrontiamo mele e arance.

Dopo aver esaminato gli schemi e le migrazioni del database in ActiveRecord rispetto a Ecto, parte 1, questo post illustra come sia ActiveRecord che Ecto consentano agli sviluppatori di interrogare il database e in che modo sia ActiveRecord che Ecto si confrontano quando gestiscono gli stessi requisiti. Lungo la strada, scopriremo anche l'identità di Batgirl 1989–2011.

Dati sui semi

Iniziamo! Sulla base della struttura del database definita nel primo post di questa serie, si supponga che gli utenti e le tabelle delle fatture abbiano i seguenti dati memorizzati:

utenti

* Il campo creato_at di ActiveRecord è chiamato inserito_at in Ecto per impostazione predefinita.

fatture

* Il campo creato_at di ActiveRecord è chiamato inserito_at in Ecto per impostazione predefinita.

Le query eseguite tramite questo post presuppongono che i dati sopra riportati siano archiviati nel database, quindi tieni a mente queste informazioni durante la lettura.

Trova l'elemento usando la sua chiave primaria

Iniziamo con l'ottenimento di un record dal database utilizzando la sua chiave primaria.

ActiveRecord

irb (principale): 001: 0> User.find (1) Carico utente (0.4ms) SELEZIONA "utenti". * DA "utenti" DOVE "utenti". "id" = $ 1 LIMIT $ 2 [["id", 1 ], ["LIMIT", 1]] => # 

Ecto

iex (3)> Repo.get (Utente, 1)
[debug] QUERY OK source = "utenti" db = decodifica 5,2ms = coda 2,5ms = 0,1ms
SELEZIONA u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" DA "utenti" COME u0 DOVE (u0. "Id" = $ 1) [1]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: caricato, "utenti">,
  e-mail: "bette@kane.test",
  full_name: "Bette Kane",
  id: 1,
  inserito_at: ~ N [01-01-2010 10: 01: 00.000000],
  fatture: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [01-01-2010 10: 01: 00.000000]
}

Confronto

Entrambi i casi sono abbastanza simili. ActiveRecord si basa sul metodo find class della classe del modello User. Significa che ogni classe figlio di ActiveRecord ha il proprio metodo find in essa.

Ecto utilizza un approccio diverso, basandosi sul concetto di repository come mediatore tra il livello di mappatura e il dominio. Quando si utilizza Ecto, il modulo Utente non ha alcuna conoscenza di come trovarsi. Tale responsabilità è presente nel modulo Repo, che è in grado di mapparlo sul datastore sottostante, che nel nostro caso è Postgres.

Quando si confronta la query SQL stessa, possiamo individuare alcune differenze:

  • ActiveRecord carica tutti i campi (utenti. *), Mentre Ecto carica solo i campi elencati nella definizione dello schema.
  • ActiveRecord include un LIMIT 1 alla query, mentre Ecto no.

Recupero di tutti gli elementi

Facciamo un passo avanti e cariciamo tutti gli utenti dal database.

ActiveRecord

irb (principale): 001: 0> Carica utente User.all (0,5 ms) SELEZIONA "utenti". * DA "utenti" LIMIT $ 1 [["LIMIT", 11]] => # , # , # , # ]>

Ecto

iex (4)> Repo.all (Utente)
[debug] QUERY OK source = "utenti" db = decodifica 2.8ms = coda 0.2ms = 0.2ms
SELEZIONA u0. "Id", u0. "Full_name", u0. "Email", u0. "Inserito_at", u0. "Aggiornato_at" DA "utenti" AS u0 []
[
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: caricato, "utenti">,
    e-mail: "bette@kane.test",
    full_name: "Bette Kane",
    id: 1,
    inserito_at: ~ N [01-01-2010 10: 01: 00.000000],
    fatture: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [01-01-2010 10: 01: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: caricato, "utenti">,
    e-mail: "barbara@gordon.test",
    full_name: "Barbara Gordon",
    id: 2,
    insert_at: ~ N [02-01-2010 10: 02: 00.000000],
    fatture: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [02/01/2018 10: 02: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: caricato, "utenti">,
    e-mail: "cassandra@cain.test",
    full_name: "Cassandra Cain",
    id: 3,
    inserito_at: ~ N [03-01-2010 10: 03: 00.000000],
    fatture: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [03-01-2010 10: 03: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: caricato, "utenti">,
    e-mail: "stephanie@brown.test",
    full_name: "Stephanie Brown",
    id: 4,
    inserito_at: ~ N [04-01-2010 10: 04: 00.000000],
    fatture: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [04/01/2018 10: 04: 00.000000]
  }
]

Confronto

Segue esattamente lo stesso modello della sezione precedente. ActiveRecord utilizza il metodo di tutte le classi ed Ecto si basa sul modello di repository per caricare i record.

Ci sono ancora alcune differenze nelle query SQL:

Interrogazione con condizioni

È molto improbabile che dobbiamo recuperare tutti i record da una tabella. Un'esigenza comune è l'uso di condizioni per filtrare i dati restituiti.

Usiamo questo esempio per elencare tutte le fatture che devono ancora essere pagate (WHERE paid_at IS NULL).

ActiveRecord

irb (principale): 024: 0> Invoice.where (paid_at: nil) Carica fattura (18,2 ms) SELEZIONA "fatture". * DA "fatture" DOVE "fatture". "paid_at" È NULL LIMIT $ 1 [["LIMIT" , 11]] => # , # ]>

Ecto

iex (19)> where (Invoice, [i], is_nil (i.paid_at)) |> Repo.all ()
[debug] QUERY OK source = "fatture" db = 20.2ms
SELEZIONA i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "fatture" COME i0 DOVE (i0. "Paid_at" IS NULLO) []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 3,
    insert_at: ~ N [04-01-2018 08: 00: 00.000000],
    paid_at: zero,
    metodo_pagamento: zero,
    updated_at: ~ N [04-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 4,
    insert_at: ~ N [04-01-2018 08: 00: 00.000000],
    paid_at: zero,
    metodo_pagamento: zero,
    updated_at: ~ N [04-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 4
  }
]

Confronto

In entrambi gli esempi, viene utilizzata la parola chiave where, che è una connessione alla clausola WHERE di SQL. Sebbene le query SQL generate siano abbastanza simili, il modo in cui entrambi gli strumenti arrivano lì presenta alcune differenze importanti.

ActiveRecord trasforma automaticamente l'argomento paid_at: nil nell'istruzione SQL IS NULL di paid_at. Per ottenere lo stesso output utilizzando Ecto, gli sviluppatori devono essere più espliciti sul loro intento, chiamando is_nil ().

Un'altra differenza da evidenziare è il comportamento "puro" della funzione in cui in Ecto. Quando si chiama la sola funzione where, non interagisce con il database. Il ritorno della funzione where è una struttura Ecto.Query:

iex (20)> dove (Fattura, [i], is_nil (i.paid_at))
# Ecto.Query 

Il database viene toccato solo quando viene chiamata la funzione Repo.all (), passando la struttura Ecto.Query come argomento. Questo approccio consente la composizione delle query in Ecto, che è l'argomento della sezione successiva.

Composizione query

Uno degli aspetti più potenti delle query del database è la composizione. Descrive una query in un modo che contiene più di una singola condizione.

Se stai creando query SQL non elaborate, significa che probabilmente utilizzerai un tipo di concatenazione. Immagina di avere due condizioni:

  1. not_paid = 'paid_at IS NOT NULL'
  2. paid_with_paypal = 'payment_method = "Paypal"'

Al fine di combinare queste due condizioni utilizzando SQL non elaborato, è necessario concatenarle utilizzando qualcosa di simile a:

SELEZIONA * DA fatture DOVE # {not_paid} E # {paid_with_paypal}

Fortunatamente sia ActiveRecord che Ecto hanno una soluzione per questo.

ActiveRecord

irb (main): 003: 0> Invoice.where.not (paid_at: nil) .where (payment_method: "Paypal") Invoice Load (8.0ms) SELEZIONA "fatture". * DA "fatture" DOVE "fatture". " paid_at "NON È NULL E" fatture "." payment_method "= $ 1 LIMIT $ 2 [[" payment_method "," Paypal "], [" LIMIT ", 11]] => # ]>

Ecto

iex (6)> Fattura |> dove ([i], non is_nil (i.paid_at)) |> dove ([i], i.payment_method == "Paypal") |> Repo.all ()
[debug] QUERY OK source = "fatture" db = decodifica 30.0ms = coda 0.6ms = 0.2ms
SELEZIONA i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "fatture" COME i0 DOVE (NON (i0. "Paid_at "IS NULL)) AND (i0." Payment_method "= 'Paypal') []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    metodo_pagamento: "Paypal",
    updated_at: ~ N [03-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Confronto

Entrambe le query rispondono alla stessa domanda: "Quali fatture sono state pagate e utilizzate Paypal?".

Come già previsto, ActiveRecord offre un modo più conciso di comporre la query (ad esempio), mentre Ecto richiede agli sviluppatori di spendere un po 'di più per scrivere la query. Come al solito, Batgirl (l'orfano, muto con l'identità di Cassandra Caino) o Activerecord non è così prolisso.

Non lasciarti ingannare dalla verbosità e dall'apparente complessità della query Ecto mostrata sopra. In un ambiente reale, quella query verrebbe riscritta per assomigliare di più a:

Fattura
|> dove ([i], non is_nil (i.paid_at))
|> dove ([i], i.payment_method == "Paypal")
|> Repo.all ()

Vista da quell'angolazione, la combinazione degli aspetti "puri" della funzione in cui, che non esegue da sola le operazioni del database, con l'operatore pipe, rende la composizione della query in Ecto davvero pulita.

ordinazione

L'ordinamento è un aspetto importante di una query. Consente agli sviluppatori di garantire che un determinato risultato della query segua un ordine specificato.

ActiveRecord

irb (principale): 002: 0> Invoice.order (Created_at:: desc) Caricamento fattura (1,5 ms) SELEZIONA "fatture". * DA "fatture" ORDINA PER "fatture". "Created_at" LIMITE DESC $ $ [["LIMIT ", 11]] => # , # , # , # ]>

Ecto

iex (6)> order_by (Fattura, desc:: insert_at) |> Repo.all ()
[debug] QUERY OK source = "fatture" db = 19,8ms
SELEZIONA i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "fatture" COME i0 ORDINA DA i0. "Inserito_at" DESC []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 3,
    insert_at: ~ N [04-01-2018 08: 00: 00.000000],
    paid_at: zero,
    metodo_pagamento: zero,
    updated_at: ~ N [04-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 4,
    insert_at: ~ N [04-01-2018 08: 00: 00.000000],
    paid_at: zero,
    metodo_pagamento: zero,
    updated_at: ~ N [04-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 4
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    metodo_pagamento: "Paypal",
    updated_at: ~ N [03-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 2
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 1,
    insert_at: ~ N [02-01-2018 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Carta di credito",
    updated_at: ~ N [02-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 1
  }
]

Confronto

L'aggiunta di un ordine a una query è semplice in entrambi gli strumenti.

Sebbene l'esempio Ecto utilizzi una fattura come primo parametro, la funzione order_by accetta anche le strutture Ecto.Query, che consente di utilizzare la funzione order_by nelle composizioni, come:

Fattura
|> dove ([i], non is_nil (i.paid_at))
|> dove ([i], i.payment_method == "Paypal")
|> order_by (desc:: insert_at)
|> Repo.all ()

limitativo

Quale sarebbe un database senza limiti? Un disastro. Fortunatamente, sia ActiveRecord che Ecto aiutano a limitare il numero di record restituiti.

ActiveRecord

irb (principale): 004: 0> Invoice.limit (2)
Caricamento fattura (0,2 ms) SELEZIONA "fatture". * DA "fatture" LIMIT $ 1 [["LIMIT", 2]]
=> # , # ]>

Ecto

iex (22)> limit (Invoice, 2) |> Repo.all ()
[debug] QUERY OK source = "fatture" db = 3,6 ms
SELEZIONA i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "fatture" AS i0 LIMIT 2 []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 1,
    insert_at: ~ N [02-01-2018 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Carta di credito",
    updated_at: ~ N [02-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 1
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    metodo_pagamento: "Paypal",
    updated_at: ~ N [03-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Confronto

Sia ActiveRecord che Ecto hanno un modo per limitare il numero di record restituiti da una query.

Il limite di Ecto funziona in modo simile a order_by, essendo adatto per composizioni di query.

associazioni

ActiveRecord ed Ecto hanno approcci diversi quando si tratta di come vengono gestite le associazioni.

ActiveRecord

In ActiveRecord, è possibile utilizzare qualsiasi associazione definita in un modello, senza dover fare nulla di speciale, ad esempio:

irb (main): 012: 0> user = User.find (2) User Load (0.3ms) SELEZIONA "utenti". * DA "utenti" DOVE "utenti". "id" = $ 1 LIMIT $ 2 [["id" , 2], ["LIMIT", 1]] => #  irb (principale): 013: 0> user.invoices Invoice Load (0.4ms) SELEZIONA" fatture ". * DA" fatture "DOVE" fatture " . "user_id" = $ 1 LIMIT $ 2 [["user_id", 2], ["LIMIT", 11]] => # ] >

L'esempio sopra mostra che possiamo ottenere un elenco delle fatture dell'utente quando si chiamano user.invoices. Nel fare ciò, ActiveRecord ha automaticamente interrogato il database e caricato le fatture associate all'utente. Mentre questo approccio semplifica le cose, nel senso di scrivere meno codice o doversi preoccupare di passaggi aggiuntivi, potrebbe essere un problema se si sta iterando su un numero di utenti e recuperando le fatture per ciascun utente. Questo problema è noto come "problema N + 1".

In ActiveRecord, la correzione proposta per il "problema N + 1" consiste nell'utilizzare il metodo include:

irb (principale): 022: 0> user = User.includes (: fatture) .find (2) User Load (0.3ms) SELEZIONA "utenti". * DA "utenti" DOVE "utenti". "id" = $ 1 LIMIT $ 2 [["id", 2], ["LIMIT", 1]] Invoice Load (0.6ms) SELEZIONA "fatture". * FROM "fatture" DOVE "fatture". "User_id" = $ 1 [["user_id", 2]] => #  irb (main): 023: 0> user.invoices => # ]>

In questo caso, ActiveRecord carica con impazienza l'associazione delle fatture durante il recupero dell'utente (come si vede nelle due query SQL mostrate).

Ecto

Come forse avrai già notato, a Ecto non piacciono davvero la magia o l'implicazione. Richiede agli sviluppatori di essere espliciti sui loro intenti.

Proviamo lo stesso approccio dell'utilizzo di user.invoices con Ecto:

iex (7)> ​​user = Repo.get (Utente, 2)
[debug] QUERY OK source = "users" db = decodifica 18.3ms = 0.6ms
SELEZIONA u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" DA "utenti" COME u0 DOVE (u0. "Id" = $ 1) [2]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: caricato, "utenti">,
  e-mail: "barbara@gordon.test",
  full_name: "Barbara Gordon",
  id: 2,
  insert_at: ~ N [02-01-2010 10: 02: 00.000000],
  fatture: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [02/01/2018 10: 02: 00.000000]
}
iex (8)> user.invoices
# Ecto.Association.NotLoaded 

Il risultato è un Ecto.Association.NotLoaded. Non così utile

Per avere accesso alle fatture, uno sviluppatore deve informare Ecto a tale proposito, utilizzando la funzione di precaricamento:

iex (12)> utente = precarico (Utente,: fatture) |> Repo.get (2)
[debug] QUERY OK source = "users" db = 11,8ms
SELEZIONA u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" DA "utenti" COME u0 DOVE (u0. "Id" = $ 1) [2]
[debug] QUERY OK source = "fatture" db = 4.2ms
SELEZIONA i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at", i0. "User_id" FROM "fatture" COME i0 DOVE ( i0. "user_id" = $ 1) ORDINA PER i0. "user_id" [2]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: caricato, "utenti">,
  e-mail: "barbara@gordon.test",
  full_name: "Barbara Gordon",
  id: 2,
  insert_at: ~ N [02-01-2010 10: 02: 00.000000],
  fatture: [
    % Financex.Accounts.Invoice {
      __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
      id: 2,
      insert_at: ~ N [2018-01-03 08: 00: 00.000000],
      paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
      metodo_pagamento: "Paypal",
      updated_at: ~ N [03-01-2018 08: 00: 00.000000],
      utente: # Ecto.Association.NotLoaded ,
      user_id: 2
    }
  ],
  updated_at: ~ N [02/01/2018 10: 02: 00.000000]
}

iex (15)> user.invoices
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: caricato, "fatture">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    metodo_pagamento: "Paypal",
    updated_at: ~ N [03-01-2018 08: 00: 00.000000],
    utente: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Analogamente ad ActiveRecord include, il precarico con recupero delle fatture associate, che le renderà disponibili quando si chiamano user.invoices.

Confronto

Ancora una volta, la battaglia tra ActiveRecord ed Ecto termina con un punto noto: esplicativo. Entrambi gli strumenti consentono agli sviluppatori di accedere facilmente alle associazioni, ma mentre ActiveRecord lo rende meno dettagliato, il risultato potrebbe avere comportamenti imprevisti. Ecto segue il tipo di approccio WYSIWYG, che fa solo ciò che viene visto nella query definita dallo sviluppatore.

Rails è noto per l'utilizzo e la promozione di strategie di memorizzazione nella cache per tutti i diversi livelli dell'applicazione. Un esempio riguarda l'uso dell'approccio di memorizzazione nella cache "Russian doll", che si basa interamente sul "problema N + 1" per il suo meccanismo di memorizzazione nella cache per eseguire la sua magia.

Validazioni

La maggior parte delle validazioni presenti in ActiveRecord sono disponibili anche in Ecto. Ecco un elenco di convalide comuni e in che modo sia ActiveRecord che Ecto le definiscono:

Incartare

Ecco qua: il confronto essenziale tra mele e arance.

ActiveRecord si concentra sulla facilità di eseguire query sul database. La maggior parte delle sue funzionalità si concentra sulle classi del modello stesso, senza richiedere agli sviluppatori una conoscenza approfondita del database, né l'impatto di tali operazioni. ActiveRecord fa molte cose implicitamente di default. Sebbene ciò renda più facile iniziare, rende più difficile capire cosa sta succedendo dietro le quinte e funziona solo se segui il "modo ActiveRecord".

Ecto, d'altra parte, richiede esplicitazione che si traduce in un codice più dettagliato. Come vantaggio, tutto è sotto i riflettori, nulla dietro le quinte e puoi specificare la tua strada.

Entrambi hanno il loro lato positivo a seconda della prospettiva e delle preferenze. Quindi, confrontando mele e arance, arriviamo alla fine di questo BAT-tle. Quasi dimenticato di dirti che il nome in codice di BatGirl (1989-2001) era ... Oracolo. Ma non entriamo in questo.

Questo post è stato scritto dall'autore ospite Elvio Vicosa. Elvio è l'autore del libro Phoenix for Rails Developers.

Originariamente pubblicato su blog.appsignal.com il 9 ottobre 2018.