Temui Ecto, Pembungkus Basis Data Tanpa Kompromi untuk Aplikasi Elixir Serentak
Diterbitkan: 2022-03-11Ecto adalah bahasa khusus domain untuk menulis kueri dan berinteraksi dengan database dalam bahasa Elixir. Versi terbaru (2.0) mendukung PostgreSQL dan MySQL. (dukungan untuk MSSQL, SQLite, dan MongoDB akan tersedia di masa mendatang). Jika Anda baru mengenal Elixir atau memiliki sedikit pengalaman dengannya, saya sarankan Anda membaca Kleber Virgilio Correia's Getting Started with Elixir Programming Language.
Ecto terdiri dari empat komponen utama:
- Ecto.Repo. Mendefinisikan repositori yang membungkus penyimpanan data. Dengan menggunakannya, kita dapat menyisipkan, membuat, menghapus, dan meminta repo. Adaptor dan kredensial diperlukan untuk berkomunikasi dengan database.
- Ecto.Skema. Skema digunakan untuk memetakan sumber data apa pun ke dalam struct Elixir.
- Ecto.Changeset. Changesets menyediakan cara bagi pengembang untuk memfilter dan mentransmisikan parameter eksternal, serta mekanisme untuk melacak dan memvalidasi perubahan sebelum diterapkan ke data.
- Ecto.Query. Menyediakan kueri SQL seperti DSL untuk mengambil informasi dari repositori. Kueri di Ecto aman, menghindari masalah umum seperti SQL Injection, sementara masih dapat dikomposisi, memungkinkan pengembang membuat kueri sepotong demi sepotong alih-alih sekaligus.
Untuk tutorial ini, Anda akan membutuhkan:
- Elixir diinstal (panduan instalasi untuk 1.2 atau lebih baru)
- PostgreSQL diinstal
- Seorang pengguna yang ditentukan dengan izin untuk membuat database (Catatan: Kami akan menggunakan pengguna "postgres" dengan kata sandi "postgres" sebagai contoh di seluruh tutorial ini.)
Instalasi dan Konfigurasi
Sebagai permulaan, mari buat aplikasi baru dengan supervisor menggunakan Mix. Mix adalah alat build yang disertakan dengan Elixir yang menyediakan tugas untuk membuat, mengkompilasi, menguji aplikasi Anda, mengelola dependensinya, dan banyak lagi.
mix new cart --supIni akan membuat keranjang direktori dengan file proyek awal:
* creating README.md * creating .gitignore * creating mix.exs * creating config * creating config/config.exs * creating lib * creating lib/ecto_tut.ex * creating test * creating test/test_helper.exs * creating test/ecto_tut_test.exs Kami menggunakan opsi --sup karena kami membutuhkan pohon supervisor yang akan menjaga koneksi ke database. Selanjutnya, kita masuk ke direktori cart dengan cd cart dan buka file mix.exs dan ganti isinya:
defmodule Cart.Mixfile do use Mix.Project def project do [app: :cart, version: "0.0.1", elixir: "~> 1.2", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps] end def application do [applications: [:logger, :ecto, :postgrex], mod: {Cart, []}] end # Type "mix help deps" for more examples and options defp deps do [{:postgrex, ">= 0.11.1"}, {:ecto, "~> 2.0"}] end end Dalam def application do kita harus menambahkan sebagai aplikasi :postgrex, :ecto sehingga ini dapat digunakan di dalam aplikasi kita. Kita juga harus menambahkannya sebagai dependensi dengan menambahkan defp deps do postgrex (yang merupakan adaptor database) dan ecto . Setelah Anda mengedit file, jalankan di konsol:
mix deps.get Ini akan menginstal semua dependensi dan membuat file mix.lock yang menyimpan semua dependensi dan sub-dependensi dari paket yang diinstal (mirip dengan Gemfile.lock di bundler).
Ecto.Repo
Sekarang kita akan melihat bagaimana mendefinisikan repo di aplikasi kita. Kita dapat memiliki lebih dari satu repo, artinya kita dapat terhubung ke lebih dari satu database. Kita perlu mengkonfigurasi database di file config/config.exs :
use Mix.Config config :cart, ecto_repos: [Cart.Repo] Kami hanya mengatur minimum, sehingga kami dapat menjalankan perintah berikutnya. Dengan baris :cart, cart_repos: [Cart.Repo] kita memberi tahu Ecto repo mana yang kita gunakan. Ini adalah fitur keren karena memungkinkan kita memiliki banyak repo, yaitu kita dapat terhubung ke banyak database.
Sekarang jalankan perintah berikut:
mix ecto.gen.repo ==> connection Compiling 1 file (.ex) Generated connection app ==> poolboy (compile) Compiled src/poolboy_worker.erl Compiled src/poolboy_sup.erl Compiled src/poolboy.erl ==> decimal Compiling 1 file (.ex) Generated decimal app ==> db_connection Compiling 23 files (.ex) Generated db_connection app ==> postgrex Compiling 43 files (.ex) Generated postgrex app ==> ecto Compiling 68 files (.ex) Generated ecto app ==> cart * creating lib/cart * creating lib/cart/repo.ex * updating config/config.exs Don't forget to add your new repo to your supervision tree (typically in lib/cart.ex): supervisor(Cart.Repo, []) And to add it to the list of ecto repositories in your configuration files (so Ecto tasks work as expected): config :cart, ecto_repos: [Cart.Repo] Perintah ini menghasilkan repo. Jika Anda membaca hasilnya, ini memberitahu Anda untuk menambahkan supervisor dan repo di aplikasi Anda. Mari kita mulai dengan pengawas. Kami akan mengedit lib/cart.ex :
defmodule Cart do use Application def start(_type, _args) do import Supervisor.Spec, warn: false children = [ supervisor(Cart.Repo, []) ] opts = [strategy: :one_for_one, name: Cart.Supervisor] Supervisor.start_link(children, opts) end end Dalam file ini, kita mendefinisikan supervisor supervisor(Cart.Repo, []) dan menambahkannya ke daftar anak (dalam Elixir, daftar mirip dengan array). Kami mendefinisikan anak-anak yang diawasi dengan strategi strategy: :one_for_one yang berarti bahwa, jika salah satu proses yang diawasi gagal, supervisor hanya akan memulai kembali proses itu ke status default. Anda dapat mempelajari lebih lanjut tentang supervisor di sini. Jika Anda melihat lib/cart/repo.ex Anda akan melihat bahwa file ini telah dibuat, artinya kami memiliki Repo untuk aplikasi kami.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end Sekarang mari kita edit file config/config.exs :
use Mix.Config config :cart, ecto_repos: [Cart.Repo] config :cart, Cart.Repo, adapter: Ecto.Adapters.Postgres, database: "cart_dev", username: "postgres", password: "postgres", hostname: "localhost"Setelah mendefinisikan semua konfigurasi untuk database kami, sekarang kami dapat membuatnya dengan menjalankan:
mix ecto.createPerintah ini membuat database dan, dengan itu, kita pada dasarnya telah menyelesaikan konfigurasi. Kita sekarang siap untuk memulai pengkodean, tetapi mari kita tentukan ruang lingkup aplikasi kita terlebih dahulu.
Membuat Faktur dengan Item Sebaris
Untuk aplikasi demo kami, kami akan membangun alat faktur sederhana. Untuk changeset (model) kita akan memiliki Invoice , Item dan InvoiceItem . InvoiceItem milik Faktur dan Item . Diagram ini menunjukkan bagaimana model kami akan terkait satu sama lain:
Diagramnya cukup sederhana. Kami memiliki tabel invoice yang memiliki banyak invoice_items tempat kami menyimpan semua detailnya dan juga tabel item yang memiliki banyak invoice_items . Anda dapat melihat bahwa tipe untuk invoice_id dan item_id pada tabel invoice_items adalah UUID. Kami menggunakan UUID karena membantu mengaburkan rute, jika Anda ingin mengekspos aplikasi melalui API dan mempermudah sinkronisasi karena Anda tidak bergantung pada nomor urut. Sekarang mari kita buat tabel menggunakan tugas Mix.
Ecto.Migration
Migrasi adalah file yang digunakan untuk memodifikasi skema database. Ecto.Migration memberi Anda serangkaian metode untuk membuat tabel, menambahkan indeks, membuat batasan, dan hal-hal terkait skema lainnya. Migrasi sangat membantu menjaga aplikasi tetap sinkron dengan database. Mari buat skrip migrasi untuk tabel pertama kita:
mix ecto.gen.migration create_invoices Ini akan menghasilkan file yang mirip dengan priv/repo/migrations/20160614115844_create_invoices.exs di mana kita akan mendefinisikan migrasi kita. Buka file yang dihasilkan dan ubah isinya menjadi sebagai berikut:
defmodule Cart.Repo.Migrations.CreateInvoices do use Ecto.Migration def change do create table(:invoices, primary_key: false) do add :id, :uuid, primary_key: true add :customer, :text add :amount, :decimal, precision: 12, scale: 2 add :balance, :decimal, precision: 12, scale: 2 add :date, :date timestamps end end end Di dalam metode def change do kita mendefinisikan skema yang akan menghasilkan SQL untuk database. create table(:invoices, primary_key: false) do akan membuat tabel invoices . Kami telah menetapkan primary_key: false tetapi kami akan menambahkan bidang ID dari jenis UUID, bidang pelanggan dari jenis teks, bidang tanggal dari jenis tanggal. Metode timestamps akan menghasilkan field-field yang inserted_at dan diperbarui_at yang secara otomatis diisi oleh updated_at dengan waktu catatan dimasukkan dan waktu diperbarui, masing-masing. Sekarang buka konsol dan jalankan migrasi:
mix ecto.migrate Kami telah membuat tabel invoice s dengan semua bidang yang ditentukan. Mari kita buat tabel item :
mix ecto.gen.migration create_itemsSekarang edit skrip migrasi yang dihasilkan:
defmodule Cart.Repo.Migrations.CreateItems do use Ecto.Migration def change do create table(:items, primary_key: false) do add :id, :uuid, primary_key: true add :name, :text add :price, :decimal, precision: 12, scale: 2 timestamps end end endHal baru di sini adalah bidang desimal yang memungkinkan angka dengan 12 digit, 2 di antaranya untuk bagian desimal dari angka tersebut. Mari kita jalankan migrasi lagi:
mix ecto.migrateSekarang kita telah membuat tabel item dan terakhir mari kita buat tabel invoice_items :
mix ecto.gen.migration create_invoice_itemsEdit migrasi:
defmodule Cart.Repo.Migrations.CreateInvoiceItems do use Ecto.Migration def change do create table(:invoice_items, primary_key: false) do add :id, :uuid, primary_key: true add :invoice_id, references(:invoices, type: :uuid, null: false) add :item_id, references(:items, type: :uuid, null: false) add :price, :decimal, precision: 12, scale: 2 add :quantity, :decimal, precision: 12, scale: 2 add :subtotal, :decimal, precision: 12, scale: 2 timestamps end create index(:invoice_items, [:invoice_id]) create index(:invoice_items, [:item_id]) end end Seperti yang Anda lihat, migrasi ini memiliki beberapa bagian baru. Hal pertama yang akan Anda perhatikan adalah add :invoice_id, references(:invoices, type: :uuid, null: false) . Ini membuat bidang invoice_id dengan batasan dalam database yang mereferensikan tabel faktur . Kami memiliki pola yang sama untuk bidang item_id . Hal lain yang berbeda adalah cara kita membuat index: create index(:invoice_items, [:invoice_id]) membuat index invoice_items_invoice_id_index .
Ecto.Schema dan Ecto.Changeset
Di Ecto, Ecto.Model tidak digunakan lagi karena menggunakan Ecto.Schema , jadi kami akan memanggil skema modul alih-alih model. Mari kita buat set perubahan. Kita akan mulai dengan item changeset paling sederhana dan membuat file lib/cart/item.ex :
defmodule Cart.Item do use Ecto.Schema import Ecto.Changeset alias Cart.InvoiceItem @primary_key {:id, :binary_id, autogenerate: true} schema "items" do field :name, :string field :price, :decimal, precision: 12, scale: 2 has_many :invoice_items, InvoiceItem timestamps end @fields ~w(name price) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:name, :price]) |> validate_number(:price, greater_than_or_equal_to: Decimal.new(0)) end end Di bagian atas, kami menyuntikkan kode ke dalam changeset menggunakan use Ecto.Schema . Kami juga menggunakan import Ecto.Changeset untuk mengimpor fungsionalitas dari Ecto.Changeset . Kita bisa saja menentukan metode spesifik mana yang akan diimpor, tetapi mari kita tetap sederhana. alias Cart.InvoiceItem memungkinkan kita untuk menulis langsung di dalam changeset InvoiceItem , seperti yang akan Anda lihat sebentar lagi.
Ecto.Schema
@primary_key {:id, :binary_id, autogenerate: true} menetapkan bahwa kunci utama kita akan dibuat secara otomatis. Karena kami menggunakan tipe UUID, kami mendefinisikan skema dengan schema "items" do dan di dalam blok kami mendefinisikan setiap bidang dan hubungan. Kami mendefinisikan nama sebagai string dan harga sebagai desimal, sangat mirip dengan migrasi. Selanjutnya, makro has_many :invoice_items, InvoiceItem menunjukkan hubungan antara Item dan InvoiceItem . Karena menurut konvensi kami menamai bidang item_id di tabel invoice_items , kami tidak perlu mengonfigurasi kunci asing. Akhirnya metode timestamps akan menyetel bidang insert_at dan updated_at .
Ecto.Changeset
Fungsi def changeset(data, params \\ %{}) do menerima sebuah struct Elixir dengan params yang akan kita pipa melalui fungsi yang berbeda. cast(params, @fields) memasukkan nilai ke dalam tipe yang benar. Misalnya, Anda hanya dapat meneruskan string di params dan string tersebut akan dikonversi ke jenis yang benar yang ditentukan dalam skema. validate_required([:name, :price]) memvalidasi bahwa nama dan bidang harga ada, validate_number(:price, greater_than_or_equal_to: Decimal.new(0)) memvalidasi bahwa angka lebih besar dari atau sama dengan 0 atau dalam hal ini Decimal.new(0) .
Itu banyak yang harus diperhatikan, jadi mari kita lihat ini di konsol dengan contoh sehingga Anda dapat memahami konsep dengan lebih baik:
iex -S mix Ini akan memuat konsol. -S mix memuat proyek saat ini ke dalam iex REPL.
iex(0)> item = Cart.Item.changeset(%Cart.Item{}, %{name: "Paper", price: "2.5"}) #Ecto.Changeset<action: nil, changes: %{name: "Paper", price: #Decimal<2.5>}, errors: [], data: #Cart.Item<>, valid?: true> Ini mengembalikan struct Ecto.Changeset yang valid tanpa kesalahan. Sekarang mari kita simpan:
iex(1)> item = Cart.Repo.insert!(item) %Cart.Item{__meta__: #Ecto.Schema.Metadata<:loaded, "items">, id: "66ab2ab7-966d-4b11-b359-019a422328d7", inserted_at: #Ecto.DateTime<2016-06-18 16:54:54>, invoice_items: #Ecto.Association.NotLoaded<association :invoice_items is not loaded>, name: "Paper", price: #Decimal<2.5>, updated_at: #Ecto.DateTime<2016-06-18 16:54:54>}Kami tidak menampilkan SQL untuk singkatnya. Dalam hal ini, ia mengembalikan struct Cart.Item dengan semua nilai yang ditetapkan, Anda dapat melihat bahwa insert_at dan updated_at berisi stempel waktu mereka dan bidang id memiliki nilai UUID. Mari kita lihat beberapa kasus lainnya:
iex(3)> item2 = Cart.Item.changeset(%Cart.Item{price: Decimal.new(20)}, %{name: "Scissors"}) #Ecto.Changeset<action: nil, changes: %{name: "Scissors"}, errors: [], data: #Cart.Item<>, valid?: true> iex(4)> Cart.Repo.insert(item2) Sekarang kita telah mengatur item Scissors dengan cara yang berbeda, menetapkan harga secara langsung %Cart.Item{price: Decimal.new(20)} . Kita perlu mengatur jenisnya yang benar, tidak seperti item pertama di mana kita baru saja memberikan string sebagai harga. Kita bisa melewati float dan ini akan dimasukkan ke dalam tipe desimal. Jika kita melewati, misalnya %Cart.Item{price: 12.5} , saat Anda memasukkan item itu akan mengeluarkan pengecualian yang menyatakan bahwa jenisnya tidak cocok.

iex(4)> invalid_item = Cart.Item.changeset(%Cart.Item{}, %{name: "Scissors", price: -1.5}) #Ecto.Changeset<action: nil, changes: %{name: "Scissors", price: #Decimal<-1.5>}, errors: [price: {"must be greater than or equal to %{number}", [number: #Decimal<0>]}], data: #Cart.Item<>, valid?: false> Untuk mengakhiri konsol, tekan Ctrl+C dua kali. Anda dapat melihat bahwa validasi berfungsi dan harga harus lebih besar dari atau sama dengan nol (0). Seperti yang Anda lihat, kami telah mendefinisikan semua skema Ecto.Schema yang merupakan bagian yang terkait dengan bagaimana struktur modul didefinisikan dan changeset Ecto.Changeset yang semuanya validasi dan casting. Mari kita lanjutkan dan buat file lib/cart/invoice_item.ex :
defmodule Cart.InvoiceItem do use Ecto.Schema import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} schema "invoice_items" do belongs_to :invoice, Cart.Invoice, type: :binary_id belongs_to :item, Cart.Item, type: :binary_id field :quantity, :decimal, precision: 12, scale: 2 field :price, :decimal, precision: 12, scale: 2 field :subtotal, :decimal, precision: 12, scale: 2 timestamps end @fields ~w(item_id price quantity) @zero Decimal.new(0) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:item_id, :price, :quantity]) |> validate_number(:price, greater_than_or_equal_to: @zero) |> validate_number(:quantity, greater_than_or_equal_to: @zero) |> foreign_key_constraint(:invoice_id, message: "Select a valid invoice") |> foreign_key_constraint(:item_id, message: "Select a valid item") |> set_subtotal end def set_subtotal(cs) do case {(cs.changes[:price] || cs.data.price), (cs.changes[:quantity] || cs.data.quantity)} do {_price, nil} -> cs {nil, _quantity} -> cs {price, quantity} -> put_change(cs, :subtotal, Decimal.mult(price, quantity)) end end end Changeset ini lebih besar tetapi Anda seharusnya sudah terbiasa dengan sebagian besar. Di sini belongs_to :invoice, Cart.Invoice, type: :binary_id mendefinisikan hubungan "milik" dengan set perubahan Keranjang.Faktur yang akan segera kita buat. belongs_to :item berikutnya membuat hubungan dengan tabel item. Kami telah mendefinisikan @zero Decimal.new(0) . Dalam hal ini, @zero seperti konstanta yang dapat diakses di dalam modul. Fungsi changeset memiliki bagian baru, salah satunya adalah foreign_key_constraint(:invoice_id, message: "Select a valid invoice") . Ini akan memungkinkan pesan kesalahan dihasilkan alih-alih menghasilkan pengecualian ketika batasan tidak terpenuhi. Dan terakhir, metode set_subtotal akan menghitung subtotal. Kami melewati changeset dan mengembalikan changeset baru dengan subtotal dihitung jika kami memiliki harga dan kuantitas.
Sekarang, mari kita buat Cart.Invoice . Jadi buat dan edit file lib/cart/invoice.ex agar berisi yang berikut ini:
defmodule Cart.Invoice do use Ecto.Schema import Ecto.Changeset alias Cart.{Invoice, InvoiceItem, Repo} @primary_key {:id, :binary_id, autogenerate: true} schema "invoices" do field :customer, :string field :amount, :decimal, precision: 12, scale: 2 field :balance, :decimal, precision: 12, scale: 2 field :date, Ecto.Date has_many :invoice_items, InvoiceItem, on_delete: :delete_all timestamps end @fields ~w(customer amount balance date) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:customer, :date]) end def create(params) do cs = changeset(%Invoice{}, params) |> validate_item_count(params) |> put_assoc(:invoice_items, get_items(params)) if cs.valid? do Repo.insert(cs) else cs end end defp get_items(params) do items = params[:invoice_items] || params["invoice_items"] Enum.map(items, fn(item)-> InvoiceItem.changeset(%InvoiceItem{}, item) end) end defp validate_item_count(cs, params) do items = params[:invoice_items] || params["invoice_items"] if Enum.count(items) <= 0 do add_error(cs, :invoice_items, "Invalid number of items") else cs end end end Cart.Invoice changeset memiliki beberapa perbedaan. Yang pertama ada di dalam skema : has_many :invoice_items, InvoiceItem, on_delete: :delete_all artinya ketika kami menghapus sebuah invoice, semua invoice_items yang terkait akan dihapus. Namun, perlu diingat bahwa ini bukan batasan yang ditentukan dalam database.
Mari kita coba metode create di konsol untuk memahami semuanya dengan lebih baik. Anda mungkin telah membuat item ("Kertas", "Gunting") yang akan kami gunakan di sini:
iex(0)> item_ids = Enum.map(Cart.Repo.all(Cart.Item), fn(item)-> item.id end) iex(1)> {id1, id2} = {Enum.at(item_ids, 0), Enum.at(item_ids, 1) } Kami mengambil semua item dengan Cart.Repo.all dan dengan fungsi Enum.map kami hanya mendapatkan item.id dari setiap item. Di baris kedua, kami hanya menetapkan id1 dan id2 dengan item_id pertama dan kedua, masing-masing:
iex(2)> inv_items = [%{item_id: id1, price: 2.5, quantity: 2}, %{item_id: id2, price: 20, quantity: 1}] iex(3)> {:ok, inv} = Cart.Invoice.create(%{customer: "James Brown", date: Ecto.Date.utc, invoice_items: inv_items})Faktur telah dibuat dengan invoice_items dan kami dapat mengambil semua faktur sekarang.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)Anda dapat melihatnya mengembalikan Faktur tetapi kami juga ingin melihat invoice_items :
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items) Dengan fungsi Repo.preload , kita bisa mendapatkan invoice_items . Perhatikan bahwa ini dapat memproses kueri secara bersamaan. Dalam kasus saya, kueri terlihat seperti ini:
iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)Ecto.Query
Sejauh ini, kami telah menunjukkan cara membuat item baru dan faktur baru dengan hubungan. Tapi bagaimana dengan query? Baiklah, izinkan saya memperkenalkan Anda ke Ecto.Query yang akan membantu kami membuat kueri ke database, tetapi pertama-tama kami membutuhkan lebih banyak data untuk menjelaskan dengan lebih baik.
iex(1)> alias Cart.{Repo, Item, Invoice, InvoiceItem} iex(2)> Repo.insert(%Item{name: "Chocolates", price: Decimal.new("5")}) iex(3)> Repo.insert(%Item{name: "Gum", price: Decimal.new("2.5")}) iex(4)> Repo.insert(%Item{name: "Milk", price: Decimal.new("1.5")}) iex(5)> Repo.insert(%Item{name: "Rice", price: Decimal.new("2")}) iex(6)> Repo.insert(%Item{name: "Chocolates", price: Decimal.new("10")})Kita sekarang harus memiliki 8 item dan ada "Cokelat" yang berulang. Kita mungkin ingin tahu item mana yang diulang. Jadi mari kita coba kueri ini:
iex(7)> import Ecto.Query iex(8)> q = from(i in Item, select: %{name: i.name, count: (i.name)}, group_by: i.name) iex(9)> Repo.all(q) 19:12:15.739 [debug] QUERY OK db=2.7ms SELECT i0."name", count(i0."name") FROM "items" AS i0 GROUP BY i0."name" [] [%{count: 1, name: "Scissors"}, %{count: 1, name: "Gum"}, %{count: 2, name: "Chocolates"}, %{count: 1, name: "Paper"}, %{count: 1, name: "Milk"}, %{count: 1, name: "Test"}, %{count: 1, name: "Rice"}] Anda dapat melihat bahwa dalam kueri kami ingin mengembalikan peta dengan nama item dan berapa kali muncul di tabel item. Namun, sebagai alternatif, kami mungkin lebih tertarik untuk melihat produk mana yang paling laris. Jadi untuk itu, mari kita buat beberapa invoice. Pertama, mari kita buat hidup kita lebih mudah dengan membuat peta untuk mengakses item_id :
iex(10)> l = Repo.all(from(i in Item, select: {i.name, i.id})) iex(11)> items = for {k, v} <- l, into: %{}, do: {k, v} %{"Chocolates" => "8fde33d3-6e09-4926-baff-369b6d92013c", "Gum" => "cb1c5a93-ecbf-4e4b-8588-cc40f7d12364", "Milk" => "7f9da795-4d57-4b46-9b57-a40cd09cf67f", "Paper" => "66ab2ab7-966d-4b11-b359-019a422328d7", "Rice" => "ff0b14d2-1918-495e-9817-f3b08b3fa4a4", "Scissors" => "397b0bb4-2b04-46df-84d6-d7b1360b6c72", "Test" => "9f832a81-f477-4912-be2f-eac0ec4f8e8f"}Seperti yang Anda lihat, kami telah membuat peta menggunakan pemahaman
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}] Kita perlu menambahkan harga di parameter invoice_items untuk membuat invoice, tetapi akan lebih baik jika memasukkan id item dan mengisi harga secara otomatis. Kami akan melakukan perubahan pada modul Cart.Invoice untuk melakukannya:
defmodule Cart.Invoice do use Ecto.Schema import Ecto.Changeset import Ecto.Query # We add to query # .... # schema, changeset and create functions don't change # The new function here is items_with_prices defp get_items(params) do items = items_with_prices(params[:invoice_items] || params["invoice_items"]) Enum.map(items, fn(item)-> InvoiceItem.changeset(%InvoiceItem{}, item) end) end # new function to get item prices defp items_with_prices(items) do item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) prices = Repo.all(q) Enum.map(items, fn(item) -> item_id = item[:item_id] || item["item_id"] %{ item_id: item_id, quantity: item[:quantity] || item["quantity"], price: Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 } end) end Hal pertama yang akan Anda perhatikan adalah bahwa kami telah menambahkan Ecto.Query , yang memungkinkan kami untuk menanyakan database. Fungsi barunya adalah defp items_with_prices(items) do pencarian melalui item dan menemukan serta menetapkan harga untuk setiap item.
Pertama, defp items_with_prices(items) do menerima daftar sebagai argumen. Dengan item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) , kami mengulangi semua item dan hanya mendapatkan item_id . Seperti yang Anda lihat, kami mengakses baik dengan atom :item_id atau string “item_id”, karena peta dapat memiliki salah satu dari ini sebagai kunci. Kueri q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) akan menemukan semua item yang ada di item_ids dan akan mengembalikan peta dengan item.id dan item.price . Kami kemudian dapat menjalankan harga kueri prices = Repo.all(q) yang mengembalikan daftar peta. Kami kemudian perlu mengulangi item dan membuat daftar baru yang akan menambahkan harga. Enum.map(items, fn(item) -> iterasi melalui setiap item, menemukan harga Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 , dan membuat daftar baru dengan item_id , quantity, dan price. Dan dengan itu, tidak perlu lagi menambahkan harga di setiap invoice_items .
Memasukkan Lebih Banyak Faktur
Seperti yang Anda ingat, sebelumnya kami membuat item peta yang memungkinkan kami untuk mengakses id menggunakan nama item untuk items["Gum"] "cb1c5a93-ecbf-4e4b-8588-cc40f7d12364". Ini memudahkan pembuatan invoice_items . Mari buat lebih banyak faktur. Mulai konsol lagi dan jalankan:
Iex -S mix iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)Kami menghapus semua invoice_items dan invoice agar kosong:
iex(2)> li = [%{item_id: items["Gum"], quantity: 2}, %{item_id: items["Milk"], quantity: 1}] iex(3)> Invoice.create(%{customer: "Mary Jane", date: Ecto.Date.utc, invoice_items: li}) iex(4)> li2 = [%{item_id: items["Chocolates"], quantity: 2}| li] iex(5)> Invoice.create(%{customer: "Mary Jane", date: Ecto.Date.utc, invoice_items: li2}) iex(5)> li3 = li2 ++ [%{item_id: items["Paper"], quantity: 3 }, %{item_id: items["Rice"], quantity: 1}, %{item_id: items["Scissors"], quantity: 1}] iex(6)> Invoice.create(%{customer: "Juan Perez", date: Ecto.Date.utc, invoice_items: li3})Sekarang kami memiliki 3 faktur; yang pertama dengan 2 item, yang kedua dengan 3 item, dan yang ketiga dengan 6 item. Kami sekarang ingin tahu produk mana yang paling laris? Untuk menjawabnya, kita akan membuat kueri untuk menemukan barang terlaris berdasarkan kuantitas dan subtotal (harga x kuantitas).
defmodule Cart.Item do use Ecto.Schema import Ecto.Changeset import Ecto.Query alias Cart.{InvoiceItem, Item, Repo} # schema and changeset don't change # ... def items_by_quantity, do: Repo.all items_by(:quantity) def items_by_subtotal, do: Repo.all items_by(:subtotal) defp items_by(type) do from i in Item, join: ii in InvoiceItem, on: ii.item_id == i.id, select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}, group_by: i.id, order_by: [desc: sum(field(ii, ^type))] end end Kami mengimpor Ecto.Query dan kemudian kami alias Cart.{InvoiceItem, Item, Repo} sehingga kami tidak perlu menambahkan Cart di awal setiap modul. Fungsi pertama items_by_quantity memanggil fungsi items_by , meneruskan parameter :quantity dan memanggil Repo.all untuk mengeksekusi query. Fungsi items_by_subtotal mirip dengan fungsi sebelumnya tetapi melewati parameter :subtotal . Sekarang mari kita jelaskan items_by :
-
from i in Item, makro ini memilih modul Item -
join: ii in InvoiceItem, on: ii.item_id == i.id, buat join dengan syarat “items.id = invoice_items.item_id” -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}, kami membuat peta dengan semua bidang yang kami inginkan, pertama kami memilih id dan nama dari Item dan kami melakukan penjumlahan operator. Bidang(ii, ^type) menggunakan bidang makro untuk mengakses bidang secara dinamis -
group_by: i.id, Kami mengelompokkan berdasarkan item.id -
order_by: [desc: sum(field(ii, ^type))]dan akhirnya diurutkan berdasarkan jumlah dalam urutan menurun
Sejauh ini kami telah menulis kueri dalam gaya daftar tetapi kami dapat menulis ulang dalam gaya makro:
defp items_by(type) do Item |> join(:inner, [i], ii in InvoiceItem, ii.item_id == i.id) |> select([i, ii], %{id: i.id, name: i.name, total: sum(field(ii, ^type))}) |> group_by([i, _], i.id) |> order_by([_, ii], [desc: sum(field(ii, ^type))]) endSaya lebih suka menulis kueri dalam bentuk daftar karena saya merasa lebih mudah dibaca.
Kesimpulan
Kami telah membahas sebagian besar hal yang dapat Anda lakukan dalam aplikasi dengan Ecto. Tentu saja, masih banyak lagi yang bisa Anda pelajari dari Ecto docs. Dengan Ecto, Anda dapat membuat aplikasi yang toleran terhadap kesalahan secara bersamaan dengan sedikit usaha yang dapat diskalakan dengan mudah berkat mesin virtual Erlang. Ecto menyediakan dasar untuk penyimpanan di aplikasi Elixir Anda dan menyediakan fungsi dan makro untuk mengelola data Anda dengan mudah.
Dalam tutorial ini, kami memeriksa Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query, dan Ecto.Repo. Masing-masing modul ini membantu Anda di berbagai bagian aplikasi Anda dan membuat kode lebih eksplisit dan lebih mudah untuk dipelihara dan dipahami.
Jika Anda ingin melihat kode tutorialnya, Anda dapat menemukannya di sini di GitHub.
Jika Anda menyukai tutorial ini dan tertarik dengan informasi lebih lanjut, saya akan merekomendasikan Phoenix (untuk daftar proyek yang mengagumkan), Awesome Elixir, dan pembicaraan ini yang membandingkan ActiveRecord dengan Ecto.
