Eşzamanlı Elixir Uygulamaları için Ödün Vermeyen Veritabanı Paketleyici Ecto ile tanışın
Yayınlanan: 2022-03-11Ecto, Elixir dilinde sorgu yazmak ve veritabanlarıyla etkileşim kurmak için alana özgü bir dildir. En son sürüm (2.0) PostgreSQL ve MySQL'i destekler. (MSSQL, SQLite ve MongoDB desteği gelecekte sunulacaktır). Elixir'de yeniyseniz veya çok az deneyiminiz varsa, Kleber Virgilio Correia'nın Elixir Programlama Dili ile Başlarken kitabını okumanızı tavsiye ederim.
Ecto dört ana bileşenden oluşur:
- Ecto.Repo. Bir veri deposunun etrafındaki sarmalayıcılar olan depoları tanımlar. Bunu kullanarak bir repo ekleyebilir, oluşturabilir, silebilir ve sorgulayabiliriz. Veritabanıyla iletişim kurmak için bir bağdaştırıcı ve kimlik bilgileri gerekir.
- Ecto.Şema. Şemalar, herhangi bir veri kaynağını bir Elixir yapısına eşlemek için kullanılır.
- Ecto.Changeset. Değişiklik kümeleri, geliştiricilerin harici parametreleri filtrelemesi ve yayınlaması için bir yolun yanı sıra değişiklikleri verilere uygulanmadan önce izlemek ve doğrulamak için bir mekanizma sağlar.
- Ekto.Sorgu. Bir havuzdan bilgi almak için DSL benzeri bir SQL sorgusu sağlar. Ecto'daki sorgular güvenlidir, SQL Injection gibi yaygın sorunlardan kaçınır ve yine de birleştirilebilir olmakla birlikte geliştiricilerin tek seferde değil parça parça sorgular oluşturmasına olanak tanır.
Bu eğitim için ihtiyacınız olacak:
- İksir yüklü (1.2 veya üstü için kurulum kılavuzu)
- PostgreSQL yüklü
- Veritabanı oluşturma izniyle tanımlanmış bir kullanıcı (Not: Bu eğitim boyunca örnek olarak “postgres” kullanıcısını “postgres” parolasıyla kullanacağız.)
Kurulum ve Yapılandırma
Yeni başlayanlar için, Mix'i kullanan bir süpervizörle yeni bir uygulama oluşturalım. Mix, uygulamanızı oluşturmak, derlemek, test etmek, bağımlılıklarını yönetmek ve çok daha fazlası için görevler sağlayan Elixir ile birlikte gelen bir derleme aracıdır.
mix new cart --sup
Bu, ilk proje dosyalarıyla bir dizin arabası oluşturacaktır:
* 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
Veritabanına bağlantıyı sürdürecek bir süpervizör ağacına ihtiyacımız olduğu için --sup
seçeneğini kullanıyoruz. Ardından cd cart
ile cart
dizinine gidiyoruz ve mix.exs
dosyasını açıyoruz ve içeriğini değiştiriyoruz:
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
def application do
, uygulamamız içinde kullanılabilmesi için uygulamalar :postgrex, :ecto
olarak eklememiz gerekiyor mu? Bunları ayrıca defp deps do
postgrex (veritabanı bağdaştırıcısıdır) ve ecto ekleyerek bağımlılıklar olarak eklemeliyiz. Dosyayı düzenledikten sonra konsolda çalıştırın:
mix deps.get
Bu, tüm bağımlılıkları kuracak ve kurulu paketlerin tüm bağımlılıklarını ve alt bağımlılıklarını depolayan bir mix.lock
dosyası oluşturacaktır (paketleyicideki Gemfile.lock
benzer).
Ecto.Repo
Şimdi uygulamamızda nasıl repo tanımlayacağımıza bakacağız. Birden fazla depomuz olabilir, yani birden fazla veritabanına bağlanabiliriz. config/config.exs
dosyasındaki veritabanını yapılandırmamız gerekiyor:
use Mix.Config config :cart, ecto_repos: [Cart.Repo]
Biz sadece minimumu ayarlıyoruz, böylece bir sonraki komutu çalıştırabiliriz. :cart, cart_repos: [Cart.Repo]
satırı ile Ecto'ya hangi depoları kullandığımızı söylüyoruz. Bu harika bir özellik çünkü birçok depoya sahip olmamızı sağlıyor, yani birden fazla veritabanına bağlanabiliyoruz.
Şimdi aşağıdaki komutu çalıştırın:
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]
Bu komut depoyu oluşturur. Çıktıyı okursanız, uygulamanıza bir süpervizör ve repo eklemenizi söyler. Süpervizörle başlayalım. lib/cart.ex
düzenleyeceğiz:
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
Bu dosyada süpervizör supervisor(Cart.Repo, [])
ve alt listeye ekliyoruz (Elixir'de listeler dizilere benzer). Denetlenen çocukları strateji strategy: :one_for_one
, bu, denetlenen süreçlerden biri başarısız olursa, süpervizörün yalnızca bu süreci varsayılan durumuna yeniden başlatacağı anlamına gelir. Denetçiler hakkında daha fazla bilgiyi buradan edinebilirsiniz. lib/cart/repo.ex
bakarsanız, bu dosyanın zaten oluşturulduğunu göreceksiniz, yani uygulamamız için bir Repo'muz var.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end
Şimdi config/config.exs
yapılandırma dosyasını düzenleyelim:
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"
Veritabanımız için tüm konfigürasyonu tanımladıktan sonra, şimdi şunu çalıştırarak oluşturabiliriz:
mix ecto.create
Bu komut veritabanını oluşturur ve bununla birlikte yapılandırmayı esasen tamamladık. Artık kodlamaya başlamaya hazırız, ancak önce uygulamamızın kapsamını tanımlayalım.
Satır İçi Öğelerle Fatura Oluşturma
Demo uygulamamız için basit bir faturalandırma aracı oluşturacağız. Changeset (modeller) için Invoice , Item ve InvoiceItem olacak . InvoiceItem , Invoice ve Item'e aittir. Bu şema, modellerimizin birbiriyle nasıl ilişkilendirileceğini gösterir:
Diyagram oldukça basit. Tüm ayrıntıları sakladığımız birçok fatura_öğesine sahip bir tablo faturalarımız ve ayrıca birçok fatura_öğesine sahip bir tablo öğelerine sahibiz. bill_items tablosundaki bill_id ve item_id tipinin UUID olduğunu görebilirsiniz. Uygulamayı bir API üzerinden göstermek istemeniz durumunda yolları karartmaya yardımcı olduğu için UUID kullanıyoruz ve sıralı bir sayıya bağlı olmadığınız için senkronizasyonu kolaylaştırıyor. Şimdi Mix görevlerini kullanarak tabloları oluşturalım.
Ecto.Migration
Geçişler, veritabanı şemasını değiştirmek için kullanılan dosyalardır. Ecto.Migration size tablolar oluşturmak, dizinler eklemek, kısıtlamalar oluşturmak ve şemayla ilgili diğer şeyler için bir dizi yöntem sunar. Geçişler, uygulamanın veritabanıyla senkronize olmasına gerçekten yardımcı olur. İlk tablomuz için bir taşıma betiği oluşturalım:
mix ecto.gen.migration create_invoices
Bu, geçişimizi tanımlayacağımız priv/repo/migrations/20160614115844_create_invoices.exs
dosyasına benzer bir dosya oluşturacaktır. Oluşturulan dosyayı açın ve içeriğini aşağıdaki gibi değiştirin:
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
Yöntem def change do
içinde, veritabanı için SQL'i oluşturacak şemayı tanımlarız. create table(:invoices, primary_key: false) do
tablo faturalarını oluşturur. primary_key: false
olarak ayarladık ancak UUID türünde bir kimlik alanı, metin türünde müşteri alanı, tarih türünde tarih alanı ekleyeceğiz. Zaman timestamps
yöntemi, updated_at
sırasıyla kaydın eklendiği ve güncellendiği zamanla otomatik olarak doldurduğu inserted_at
ve güncellenen_at alanlarını oluşturur. Şimdi konsola gidin ve taşıma işlemini çalıştırın:
mix ecto.migrate
Tüm tanımlanmış alanları ile tablo invoice
oluşturduk. Öğeler tablosunu oluşturalım:
mix ecto.gen.migration create_items
Şimdi oluşturulan taşıma komut dosyasını düzenleyin:
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 end
Buradaki yeni şey, 2'si sayının ondalık kısmı için olan 12 basamaklı sayılara izin veren ondalık alandır. Taşıma işlemini tekrar çalıştıralım:
mix ecto.migrate
Şimdi item tablosunu oluşturduk ve son olarak fatura_items tablosunu oluşturalım:
mix ecto.gen.migration create_invoice_items
Taşımayı düzenleyin:
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
Gördüğünüz gibi, bu göçün bazı yeni bölümleri var. Fark edeceğiniz ilk şey add :invoice_id, references(:invoices, type: :uuid, null: false)
olacaktır. Bu, veritabanında faturalar tablosuna başvuran bir kısıtlamayla fatura_kimliği alanını oluşturur. item_id alanı için aynı kalıba sahibiz. Farklı olan başka bir şey de dizin oluşturma yöntemimizdir: create index(:invoice_items, [:invoice_id])
fatura_items_invoice_id_index dizinini oluşturur.
Ecto.Schema ve Ecto.Changeset
Ecto'da Ecto.Schema
, Ecto.Model
kullanımı lehine kullanımdan kaldırılmıştır, bu nedenle modeller yerine modül şemaları diyeceğiz. Değişiklik kümelerini oluşturalım. En basit değişiklik kümesi Öğesi ile başlayacağız ve lib/cart/item.ex
dosyasını oluşturacağız:
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
En üstte, use Ecto.Schema
kullanarak değişiklik setine kod enjekte ediyoruz. Ayrıca Ecto.Changeset'ten işlevselliği içe aktarmak için içe import Ecto.Changeset
kullanıyoruz . Hangi belirli yöntemlerin içe aktarılacağını belirleyebilirdik, ancak bunu basit tutalım. alias Cart.InvoiceItem
, birazdan göreceğiniz gibi, doğrudan InvoiceItem değişiklik kümesinin içine yazmamıza izin verir.
Ecto.Şema
@primary_key {:id, :binary_id, autogenerate: true}
, birincil anahtarımızın otomatik olarak oluşturulacağını belirtir. UUID tipi kullandığımız için schema "items" do
şeması ile tanımlıyoruz ve blok içinde her alanı ve ilişkiyi tanımlıyoruz. Adı string ve fiyatı ondalık olarak tanımladık, geçişe çok benzer. Ardından, has_many :invoice_items, InvoiceItem
, Item ile InvoiceItem arasındaki ilişkiyi gösterir. Kural olarak, fatura_öğeleri tablosunda item_id alanını adlandırdığımız için, yabancı anahtarı yapılandırmamız gerekmez. Son olarak, zaman damgaları yöntemi, eklenen_at ve güncellenen_at alanlarını ayarlayacaktır.
Ecto.Değişiklik Kümesi
def changeset(data, params \\ %{}) do
işlevi, farklı işlevler arasında ileteceğimiz paramları olan bir İksir yapısı alır. cast(params, @fields)
değerleri doğru türe dönüştürür. Örneğin, paragraflarda yalnızca dizeleri iletebilirsiniz ve bunlar şemada tanımlanan doğru türe dönüştürülür. validate_required([:name, :price])
ad ve fiyat alanlarının mevcut olduğunu doğrular, validate_number(:price, greater_than_or_equal_to: Decimal.new(0))
sayının 0'dan büyük veya 0'a eşit olduğunu veya bu durumda Decimal.new(0)
.
Bu, anlaşılması gereken çok şeydi, bu yüzden kavramları daha iyi kavrayabilmeniz için buna konsolda örneklerle bakalım:
iex -S mix
Bu konsolu yükleyecektir. -S mix
, mevcut projeyi iex REPL'ye yükler.
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>
Bu, hatasız geçerli bir Ecto.Changeset
yapısı döndürür. Şimdi kaydedelim:
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>}
SQL'i kısalık için göstermiyoruz. Bu durumda, Cart.Item yapısını tüm değerler ayarlanmış olarak döndürür, Insert_at ve updated_at'in zaman damgalarını içerdiğini ve id alanının bir UUID değerine sahip olduğunu görebilirsiniz. Diğer bazı durumlara bakalım:
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)
Şimdi, Scissors
öğesini farklı bir şekilde belirledik, fiyatı doğrudan %Cart.Item{price: Decimal.new(20)}
olarak belirledik. Fiyat olarak bir dize geçtiğimiz ilk öğenin aksine, doğru türünü ayarlamamız gerekiyor. Bir kayan noktayı geçebilirdik ve bu ondalık bir türe dönüştürülebilirdi. Örneğin %Cart.Item{price: 12.5}
, öğeyi eklediğinizde türün eşleşmediğini belirten bir istisna atar.

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>
Konsolu sonlandırmak için Ctrl+C'ye iki kez basın. Doğrulamaların çalıştığını ve fiyatın sıfıra (0) eşit veya daha büyük olması gerektiğini görebilirsiniz. Gördüğünüz gibi, modülün yapısının nasıl tanımlandığı ile ilgili kısım olan Ecto.Schema'nın tamamını ve tüm validasyonlar ve döküm olan changeset Ecto.Changeset'i tanımladık. Devam edelim ve lib/cart/invoice_item.ex
dosyasını oluşturalım:
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
Bu değişiklik seti daha büyük, ancak çoğuna zaten aşina olmalısınız. Buraya belongs_to :invoice, Cart.Invoice, type: :binary_id
, yakında oluşturacağımız Cart.Invoice değişiklik seti ile "ait" ilişkisini tanımlar. Bir sonraki belongs_to :item
ait olan öğe, öğeler tablosuyla bir ilişki oluşturur. @zero Decimal.new(0)
tanımladık. Bu durumda @zero , modül içinde erişilebilen bir sabit gibidir. changeset işlevinin yeni bölümleri var, bunlardan biri foreign_key_constraint(:invoice_id, message: "Select a valid invoice")
. Bu, kısıtlama yerine getirilmediğinde bir istisna oluşturmak yerine bir hata mesajının oluşturulmasına izin verecektir. Ve son olarak, set_subtotal yöntemi ara toplamı hesaplayacaktır. Değişiklik kümesini geçeriz ve hem fiyat hem de miktara sahipsek, hesaplanan ara toplamla yeni bir değişiklik kümesi döndürürüz.
Şimdi Cart.Invoice'i oluşturalım. Bu nedenle, aşağıdakileri içerecek şekilde lib/cart/invoice.ex
dosyasını oluşturun ve düzenleyin:
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 değişiklik kümesinin bazı farklılıkları vardır. İlki şemaların içindedir: has_many :invoice_items, InvoiceItem, on_delete: :delete_all
bir faturayı sildiğimizde, ilişkili tüm fatura_itemlerinin silineceği anlamına gelir. Ancak bunun veritabanında tanımlanmış bir kısıtlama olmadığını unutmayın.
İşleri daha iyi anlamak için konsoldaki create yöntemini deneyelim. Burada kullanacağımız öğeleri (“Kağıt”, “Makas”) oluşturmuş olabilirsiniz:
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) }
Tüm öğeleri Cart.Repo.all ile getirdik ve Enum.map işleviyle her öğenin item.id
. İkinci satırda, id1
ve id2
sırasıyla birinci ve ikinci item_ids ile atarız:
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})
Fatura, fatura_itemleri ile oluşturuldu ve şimdi tüm faturaları getirebiliyoruz.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
Faturayı iade ettiğini görebilirsiniz, ancak fatura_öğelerini de görmek isteriz:
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)
Repo.preload fonksiyonu ile invoice_items
alabiliriz. Bunun sorguları aynı anda işleyebileceğini unutmayın. Benim durumumda sorgu şöyle görünüyordu:
iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)
Ecto.Query
Şimdiye kadar, ilişkilerle yeni kalemlerin ve yeni faturaların nasıl oluşturulacağını gösterdik. Ama sorgulamak ne olacak? Pekala, sizi veritabanına sorgulama yapmamıza yardımcı olacak Ecto.Query ile tanıştırayım, ama önce daha iyi açıklamak için daha fazla veriye ihtiyacımız var.
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")})
Şimdi 8 parçamız olmalı ve tekrarlanan bir “Çikolata” var. Hangi öğelerin tekrarlandığını bilmek isteyebiliriz. O halde şu sorguyu deneyelim:
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"}]
Sorguda, öğenin adı ve öğeler tablosunda kaç kez göründüğü ile bir harita döndürmek istediğimizi görebilirsiniz. Alternatif olarak, en çok satan ürünlerin hangileri olduğunu görmekle daha fazla ilgilenebiliriz. Bunun için bazı faturalar oluşturalım. İlk önce, item_id
erişmek için bir harita oluşturarak hayatımızı kolaylaştıralım:
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"}
Gördüğünüz gibi bir kavrama kullanarak bir harita oluşturduk.
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]
Bir fatura oluşturmak için invoice_items
fiyatı eklememiz gerekiyor, ancak sadece öğenin kimliğini geçip fiyatın otomatik olarak doldurulması daha iyi olur. Bunu gerçekleştirmek için Cart.Invoice modülünde değişiklikler yapacağız:
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
İlk fark edeceğiniz şey, veritabanını sorgulamamıza izin verecek olan Ecto.Query'yi eklemiş olmamız. Yeni işlev, öğeler arasında arama yapan ve her öğe için fiyatı bulan ve belirleyen defp items_with_prices(items) do
.
İlk olarak, defp items_with_prices(items) do
argüman olarak bir liste alır. item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end)
ile, tüm öğeler boyunca yinelenir ve yalnızca item_id değerini alırız . Gördüğünüz gibi, haritalar anahtar olarak bunlardan herhangi birine sahip olabileceğinden, ya atom :item_id
ya da “item_id” dizesi ile erişiyoruz. q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids)
tüm öğeleri item_ids
ve bir harita döndürür item.id
ve item.price
. Ardından, bir harita listesi döndüren prices = Repo.all(q)
sorgusunu çalıştırabiliriz. Daha sonra öğeleri yinelememiz ve fiyatı ekleyecek yeni bir liste oluşturmamız gerekiyor. Enum.map(items, fn(item) ->
her öğeyi yineler, Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0
, ve item_id
, miktar ve fiyat ile yeni bir liste oluşturur Ve bununla birlikte, artık her invoice_items
fiyat eklemek gerekli değildir.
Daha Fazla Fatura Ekleme
Hatırladığınız gibi, daha önce öğeler için öğe adını kullanarak kimliğe erişmemizi sağlayan bir harita öğeleri oluşturmuştuk, yani items["Gum"]
“cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”. Bu, bill_items oluşturmayı kolaylaştırır. Daha fazla fatura oluşturalım. Konsolu yeniden başlatın ve çalıştırın:
Iex -S mix
iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)
Boş bir sayfa listesi olması için tüm fatura_öğelerini ve faturaları sileriz:
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})
Şimdi 3 faturamız var; ilki 2 maddeli, ikincisi 3 maddeli ve üçüncüsü 6 maddeli. Şimdi hangi ürünlerin en çok satan ürünler olduğunu bilmek istiyoruz? Bunu cevaplamak için, miktar ve ara toplam (fiyat x miktar) ile en çok satan ürünleri bulmak için bir sorgu oluşturacağız.
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
Ecto.Query'yi içe aktarıyoruz ve ardından alias Cart.{InvoiceItem, Item, Repo}
böylece her modülün başına Cart eklememize gerek kalmıyor. İlk öğe items_by
işlevi, :quantity
parametresini ileterek ve sorguyu yürütmek için Repo.all'ı çağırarak items_by işlevini çağırır. item_by_subtotal işlevi, önceki işleve benzer ancak :subtotal
parametresini geçer. Şimdi item_by'yi açıklayalım:
- Öğedeki
from i in Item
, bu makro Öğe modülünü seçer -
join: ii in InvoiceItem, on: ii.item_id == i.id
, "items.id = fatura_items.item_id" koşulunda bir birleştirme oluşturur -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}
, önce tüm alanları içeren bir harita oluşturuyoruz, önce Item'den id ve name seçiyoruz ve bir operatör toplamı yaparız. field(ii, ^type), bir alana dinamik olarak erişmek için makro alanını kullanır -
group_by: i.id
, item.id'ye göre gruplandırıyoruz -
order_by: [desc: sum(field(ii, ^type))]
ve son olarak toplama göre azalan sırada sıralayın
Şimdiye kadar sorguyu liste stilinde yazdık ancak makro stilinde yeniden yazabiliriz:
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))]) end
Sorguları daha okunaklı bulduğum için liste şeklinde yazmayı tercih ediyorum.
Çözüm
Ecto ile bir uygulamada yapabileceklerinizin önemli bir kısmını ele aldık. Tabii ki, Ecto dokümanlarından öğrenebileceğiniz daha çok şey var. Ecto ile, Erlang sanal makinesi sayesinde kolayca ölçeklenebilen, az çabayla eşzamanlı, hataya dayanıklı uygulamalar oluşturabilirsiniz. Ecto, Elixir uygulamalarınızdaki depolama için temel sağlar ve verilerinizi kolayca yönetmeniz için işlevler ve makrolar sağlar.
Bu derste Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query ve Ecto.Repo'yu inceledik. Bu modüllerin her biri, uygulamanızın farklı bölümlerinde size yardımcı olur ve kodu daha açık, bakımı ve anlaşılması daha kolay hale getirir.
Eğiticinin kodunu kontrol etmek isterseniz, onu GitHub'da burada bulabilirsiniz.
Bu öğreticiyi beğendiyseniz ve daha fazla bilgiyle ilgileniyorsanız, Phoenix'i (harika projelerin bir listesi için), Müthiş İksir'i ve ActiveRecord ile Ecto'yu karşılaştıran bu konuşmayı öneririm.