ActiveRecord vs. Ecto část druhá

Toto je druhá část série „ActiveRecord vs. Ecto“, v níž Batman a Batgirl bojují o dotazovací databáze a porovnáváme jablka a pomeranče.

Po prozkoumání databázových schémat a migrací v první části ActiveRecord vs. Ecto tento příspěvek pokrývá, jak ActiveRecord a Ecto umožňují vývojářům dotazovat databázi a jak porovnávat ActiveRecord a Ecto při řešení stejných požadavků. Po cestě také najdeme Batgirlovu identitu z let 1989–2011.

Semenná data

Začněme! Na základě struktury databáze definované v prvním příspěvku této řady předpokládejme, že uživatelé a tabulky faktur obsahují v nich následující data:

uživatelé

* Pole Active_ecord's created_at je standardně pojmenováno insert_at v Ecto.

faktury

* Pole Active_ecord's created_at je ve výchozím nastavení pojmenováno insert_at v Ecto.

Dotazy prováděné prostřednictvím tohoto příspěvku předpokládají, že výše uvedená data jsou uložena v databázi, proto si tyto informace pamatujte při jejich čtení.

Najděte položku pomocí jejího primárního klíče

Začněme získáním záznamu z databáze pomocí jeho primárního klíče.

ActiveRecord

irb (main): 001: 0> User.find (1) Load User (0.4ms) SELECT "users". * FROM "users" WHERE "users". "id" = $ 1 LIMIT $ 2 [["id", 1 ], ["LIMIT", 1]] => # 

Ecto

iex (3)> Repo.get (Uživatel, 1)
[ladění] QUERY OK source = "users" db = 5.2ms decode = 2.5ms front = 0,1ms
VÝBĚR u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" OD "users" AS u0 WHERE (u0. "Id" = $ 1) [1]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: načteno, "users">,
  email: "[email protected]",
  full_name: "Bette Kane",
  id: 1,
  insert_at: ~ N [2018-01-01 10: 01: 00.000000],
  faktury: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [2018-01-01 10: 01: 00.000000]
}

Srovnání

Oba případy jsou velmi podobné. ActiveRecord spoléhá na metodu find class třídy User model. To znamená, že každá podřízená třída ActiveRecord má v sobě svou vlastní metodu hledání.

Ecto používá odlišný přístup a spoléhá na koncept úložiště jako prostředníka mezi mapovací vrstvou a doménou. Při používání Ecto nemá uživatelský modul žádné znalosti o tom, jak se najít. Taková odpovědnost je přítomna v modulu Repo, který ji dokáže mapovat do spodního datového úložiště, kterým je v našem případě Postgres.

Při porovnání samotného dotazu SQL můžeme zaznamenat několik rozdílů:

  • ActiveRecord načte všechna pole (uživatelé. *), Zatímco Ecto načte pouze pole uvedená v definici schématu.
  • ActiveRecord obsahuje LIMIT 1 k dotazu, zatímco Ecto ne.

Načítání všech položek

Pojďme o krok dále a načteme všechny uživatele z databáze.

ActiveRecord

irb (main): 001: 0> User.all User Load (0.5ms) SELECT "users". * FROM "users" LIMIT $ 1 [["LIMIT", 11]] => # , # , # , # ]>

Ecto

iex (4)> Repo.all (Uživatel)
[ladění] QUERY OK source = "users" db = 2,8ms decode = 0,2ms front = 0,2ms
VÝBĚR u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" OD "users" AS u0 []
[
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: načteno, "users">,
    email: "[email protected]",
    full_name: "Bette Kane",
    id: 1,
    insert_at: ~ N [2018-01-01 10: 01: 00.000000],
    faktury: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-01 10: 01: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: načteno, "users">,
    email: "[email protected]",
    full_name: "Barbara Gordon",
    id: 2,
    insert_at: ~ N [2018-01-02 10: 02: 00.000000],
    faktury: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-02 10: 02: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: načteno, "users">,
    email: "[email protected]",
    full_name: "Cassandra Cain",
    id: 3,
    insert_at: ~ N [2018-01-03 10: 03: 00.000000],
    faktury: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-03 10: 03: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: načteno, "users">,
    email: "[email protected]",
    full_name: "Stephanie Brown",
    id: 4,
    insert_at: ~ N [2018-01-04 10: 04: 00.000000],
    faktury: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-04 10: 04: 00.000000]
  }
]

Srovnání

Sleduje přesně stejný vzor jako předchozí část. ActiveRecord používá metodu všech tříd a Ecto se při načítání záznamů spoléhá na vzor úložiště.

V dotazech SQL jsou opět některé rozdíly:

Dotaz na podmínky

Je velmi nepravděpodobné, že musíme načíst všechny záznamy z tabulky. Běžnou potřebou je použití podmínek k odfiltrování vrácených dat.

V tomto příkladu použijeme seznam všech faktur, které je ještě třeba uhradit (WHERE paid_at IS NULL).

ActiveRecord

irb (main): 024: 0> Invoice.where (paid_at: nil) Fakturace Load (18.2ms) VYBRAJTE "faktury". * Z "faktur" WHERE "faktury". "paid_at" JE NULL LIMIT $ 1 [["LIMIT" , 11]] => # , # ]>

Ecto

iex (19)> where (Faktura, [i], is_nil (i.paid_at)) |> Repo.all ()
[ladění] QUERY OK source = "faktury" db = 20,2ms
VÝBĚR i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Vložený_at", i0. "Updated_at" OD "faktur" AS i0 WHERE (i0. "Paid_at" IS NULA) []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 3,
    insert_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],
    user: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 4,
    insert_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],
    user: # Ecto.Association.NotLoaded ,
    user_id: 4
  }
]

Srovnání

V obou příkladech je použito klíčové slovo where, což je připojení k klauzuli SQL WHERE. Přestože jsou generované dotazy SQL docela podobné, způsob, jakým se tam oba nástroje dostávají, má některé důležité rozdíly.

ActiveRecord automaticky převede argument paid_at: nil na příkaz SQL_placené IS NULL SQL. Aby se pomocí Ecto dostali na stejný výstup, musí být vývojáři o svém záměru explicitnější, voláním is_nil ().

Další rozdíl, který je třeba zdůraznit, je „čisté“ chování funkce v Ecto. Když voláte funkci where sám, neinteraguje s databází. Návrat funkce where je struktura Ecto.Query:

iex (20)> where (Faktura, [i], is_nil (i.paid_at))
# Ecto.Query 

Databáze se dotkne pouze při vyvolání funkce Repo.all (), která jako argument předá strukturu Ecto.Query. Tento přístup umožňuje složení dotazu v Ecto, které je předmětem další části.

Složení dotazu

Jedním z nejsilnějších aspektů databázových dotazů je složení. Popisuje dotaz způsobem, který obsahuje více než jednu podmínku.

Pokud stavíte surové dotazy SQL, znamená to, že pravděpodobně použijete nějaký typ zřetězení. Představte si, že máte dvě podmínky:

  1. not_paid = 'paid_at NENÍ NULL'
  2. paid_with_paypal = 'payment_method = "Paypal"'

Chcete-li tyto dvě podmínky kombinovat pomocí surového SQL, znamená to, že je budete muset zřetězit pomocí něco podobného:

VYBERTE * Z Faktur, KDE # {not_paid} A # {paid_with_paypal}

Naštěstí jak ActiveRecord, tak Ecto mají řešení.

ActiveRecord

irb (main): 003: 0> Invoice.where.not (paid_at: nil) .where (payment_method: "Paypal") Fakturace Load (8.0ms) VYBRAJTE "faktury". * Z "faktur" WHERE "faktury". " paid_at "NENÍ NULL AND" faktury "." payment_method "= $ 1 LIMIT $ 2 [[" payment_method "," Paypal "], [" LIMIT ", 11]] => # ]>

Ecto

iex (6)> Faktura |> kde ([i], nikoli is_nil (i.paid_at)) |> kde ([i], i.payment_method == "Paypal") |> Repo.all ()
[ladění] QUERY OK source = "faktury" db = 30.0ms dekódování = 0,6ms fronta = 0,2ms
VÝBĚR i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Vložený_at", i0. "Updated_at" OD "faktur" AS i0 WHERE (NOT (i0. "Paid_at) "JE NULL)) A (i0." Payment_method "= 'Paypal') []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    user: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Srovnání

Oba dotazy odpovídají na stejnou otázku: „Které faktury byly zaplaceny a použity Paypal?“.

Jak již bylo očekáváno, ActiveRecord nabízí stručnější způsob vytváření dotazu (pro tento příklad), zatímco Ecto vyžaduje, aby vývojáři utratili trochu více za psaní dotazu. Batgirl (sirotek, ztlumený s identitou Cassandra Cain) nebo Activerecord není obvyklý.

Nenechte se zmást nad výřečností a zjevnou složitostí výše uvedeného Ecto dotazu. V reálném prostředí by byl tento dotaz přepsán tak, aby vypadal spíš:

Faktura
|> kde ([i], is is_nil (i.paid_at))
|> kde ([i], i.payment_method == "Paypal")
|> Repo.all ()

Z tohoto úhlu vyplývá, že kombinace „čistých“ aspektů funkce, kde, které neprovádí databázové operace samo o sobě, s operátorem potrubí, dělá složení dotazů v Ecto opravdu čistým.

Objednání

Objednávka je důležitým aspektem dotazu. Umožňuje vývojářům zajistit, aby daný výsledek dotazu následoval specifikovanou objednávku.

ActiveRecord

irb (main): 002: 0> Invoice.order (created_at:: desc) Fakturace Load (1.5ms) VYBRAT "faktury". * Z "faktur" OBJEDNÁVAT "faktury". "created_at" DESC LIMIT $ 1 [["LIMIT ", 11]] => # , # , # , # ]>

Ecto

iex (6)> order_by (Faktura, popis:: insert_at) |> Repo.all ()
[ladění] QUERY OK source = "faktury" db = 19,8ms
VÝBĚR i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Vložený_at", i0. "Updated_at" OD "faktur" AS i0 OBJEDNAT i0. "Vložený_at" DESC " []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 3,
    insert_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],
    user: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 4,
    insert_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],
    user: # Ecto.Association.NotLoaded ,
    user_id: 4
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    user: # Ecto.Association.NotLoaded ,
    user_id: 2
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 1,
    insert_at: ~ N [2018-01-02 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Credit Card",
    updated_at: ~ N [2018-01-02 08: 00: 00.000000],
    user: # Ecto.Association.NotLoaded ,
    user_id: 1
  }
]

Srovnání

Přidání objednávky do dotazu je v obou nástrojích rovné.

Ačkoli příklad Ecto používá Fakturu jako první parametr, funkce order_by také přijímá struktury Ecto.Query, což umožňuje použití funkce order_by v kompozicích, jako například:

Faktura
|> kde ([i], is is_nil (i.paid_at))
|> kde ([i], i.payment_method == "Paypal")
|> order_by (desc:: insert_at)
|> Repo.all ()

Omezující

Co by byla databáze bez omezení? Katastrofa. Naštěstí ActiveRecord i Ecto pomáhají omezit počet vrácených záznamů.

ActiveRecord

irb (main): 004: 0> Invoice.limit (2)
Load faktury (0,2ms) VYBERTE "faktury". * Z "faktur" LIMIT $ 1 [["LIMIT", 2]]
=> # , # ]>

Ecto

iex (22)> limit (Faktura, 2) |> Repo.all ()
[ladění] QUERY OK source = "faktury" db = 3,6 ms
VÝBĚR i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Vložený_at", i0. "Updated_at" OD "faktur" AS i0 LIMIT 2 []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 1,
    insert_at: ~ N [2018-01-02 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Credit Card",
    updated_at: ~ N [2018-01-02 08: 00: 00.000000],
    user: # Ecto.Association.NotLoaded ,
    user_id: 1
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    user: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Srovnání

ActiveRecord i Ecto mají způsob, jak omezit počet záznamů vrácených dotazem.

Ecto limit funguje podobně jako order_by, je vhodný pro složení dotazů.

Asociace

ActiveRecord a Ecto mají různé přístupy, pokud jde o to, jak se s asociacemi zachází.

ActiveRecord

V ActiveRecord můžete použít libovolné přidružení definované v modelu, aniž byste s tím museli dělat nic zvláštního, například:

irb (main): 012: 0> user = User.find (2) Load User (0.3ms) SELECT "users". * FROM "users" WHERE "users". "id" = $ 1 LIMIT $ 2 [["id" , 2], ["LIMIT", 1]] => #  irb (main): 013: 0> user.invoices Faktura Load (0,4ms) VYBRAJTE" faktury ". * Z" faktures "WHERE" faktures " . "user_id" = $ 1 LIMIT $ 2 [["user_id", 2], ["LIMIT", 11]] => # ] >

Výše uvedený příklad ukazuje, že při volání user.invoices můžeme získat seznam uživatelských faktur. Přitom aplikace ActiveRecord automaticky dotazovala databázi a načítala faktury spojené s uživatelem. I když tento přístup usnadňuje práci ve smyslu psaní méně kódu nebo obav o další kroky, může to být problém, pokud iterujete více uživatelů a načítáte faktury pro každého uživatele. Tento problém se nazývá „problém N + 1“.

V programu ActiveRecord je navrženou opravou problému „N + 1“ použití metody include:

irb (main): 022: 0> user = User.includes (: faktures) .find (2) User Load (0.3ms) SELECT "users". * FROM "users" WHERE "users". "id" = $ 1 LIMIT $ 2 [["id", 2], ["LIMIT", 1]] Fakturace (0,6 ms) VYBRAT "faktury". * Z "faktur" KDE "faktury". "User_id" = $ 1 [["user_id", 2]] => #  irb (main): 023: 0> user.invoices => # ]>

V tomto případě ActiveRecord při načítání uživatele načte přidružení faktur (jak je vidět ve dvou zobrazených dotazech SQL).

Ecto

Jak jste si možná už všimli, Ecto nemá ráda magii ani implicitu. Vyžaduje, aby vývojáři výslovně vyjadřovali svůj záměr.

Zkusme stejný přístup jako při použití user.invoices with Ecto:

iex (7)> ​​user = Repo.get (Uživatel, 2)
[ladění] QUERY OK source = "users" db = 18.3ms decode = 0.6ms
VÝBĚR u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" OD "users" AS u0 WHERE (u0. "Id" = $ 1) [2]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: načteno, "users">,
  email: "[email protected]",
  full_name: "Barbara Gordon",
  id: 2,
  insert_at: ~ N [2018-01-02 10: 02: 00.000000],
  faktury: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [2018-01-02 10: 02: 00.000000]
}
iex (8)> user.invoices
# Ecto.Association.NotLoaded 

Výsledkem je Ecto.Association.NotLoaded. Není to tak užitečné.

Aby měl vývojář přístup k fakturám, musí o tom Ecto informovat pomocí funkce předběžného načtení:

iex (12)> user = preload (Uživatel,: faktury) |> Repo.get (2)
[ladění] QUERY OK source = "users" db = 11,8ms
VÝBĚR u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" OD "users" AS u0 WHERE (u0. "Id" = $ 1) [2]
[ladění] QUERY OK source = "faktury" db = 4,2ms
VÝBĚR i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Vložený_at", i0. "Updated_at", i0. "User_id" OD "faktur" AS i0 WHERE ( i0. "user_id" = $ 1) OBJEDNAT i0. "user_id" [2]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: načteno, "users">,
  email: "[email protected]",
  full_name: "Barbara Gordon",
  id: 2,
  insert_at: ~ N [2018-01-02 10: 02: 00.000000],
  faktury: [
    % Financex.Accounts.Invoice {
      __meta__: # Ecto.Schema.Metadata <: načteno, "faktury">,
      id: 2,
      insert_at: ~ N [2018-01-03 08: 00: 00.000000],
      paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
      payment_method: "Paypal",
      updated_at: ~ N [2018-01-03 08: 00: 00.000000],
      user: # 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 <: načteno, "faktury">,
    id: 2,
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    user: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Podobně jako ActiveRecord zahrnuje i předběžné načtení přidružených faktur, které je zpřístupní při volání user.invoices.

Srovnání

Bitva mezi ActiveRecord a Ecto opět končí známým bodem: explicitnost. Oba nástroje umožňují vývojářům snadný přístup k přidružením, ale zatímco ActiveRecord z něj činí méně podrobný, výsledek může mít neočekávané chování. Ecto se řídí přístupem typu WYSIWYG, který provádí pouze to, co je vidět v dotazu definovaném vývojářem.

Kolejnice jsou dobře známé pro použití a podporu strategií ukládání do mezipaměti pro všechny různé vrstvy aplikace. Jedním z příkladů je použití přístupu „ruské panenky“ k ukládání do mezipaměti, který se spoléhá výhradně na problém „N + 1“, aby mechanismus ukládání do mezipaměti provedl svoji magii.

Validace

Většina validací přítomných v ActiveRecordu je k dispozici také v Ecto. Zde je seznam běžných validací a jak je definují ActiveRecord i Ecto:

Zabalit

Tady to máte: porovnání základních jablek versus pomeranče.

ActiveRecord se zaměřuje na snadnost provádění databázových dotazů. Převážná většina jeho funkcí je soustředěna na samotné modelové třídy, což nevyžaduje, aby vývojáři měli hluboké znalosti databáze ani dopad takových operací. ActiveRecord implicitně implicitně provádí spoustu věcí. Ačkoli to usnadňuje začátek, je obtížnější pochopit, co se děje v zákulisí, a funguje to pouze tehdy, pokud budete postupovat podle „ActiveRecord způsobem“.

Na druhou stranu Ecto vyžaduje explicitnost, která má za následek podrobnější kód. Výhodou je, že vše je v centru pozornosti, nic v zákulisí a vy můžete určit svou vlastní cestu.

Oba mají svou vzestupnou závislost na vaší perspektivě a preferenci. Srovnáme-li jablka a pomeranče, dospěli jsme na konec této BAT-tle. Skoro zapomněl říct, že BatGirlovo krycí jméno (1989–2001) bylo…. Věštec. Ale jdeme do toho.

Tento příspěvek napsal autor hostu Elvio Vicosa. Elvio je autorem knihy Phoenix for Rails Developers.

Původně zveřejněno na blog.appsignal.com 9. října 2018.