Eşzamanlı Elixir Uygulamaları için Ödün Vermeyen Veritabanı Paketleyici Ecto ile tanışın

Yayınlanan: 2022-03-11

Ecto, 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.

Tüm SQL lehçelerinden bıktınız mı? Ecto aracılığıyla veritabanınızla konuşun.
Cıvıldamak

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) .

Elixir'de Decimal işlemler bir struct olarak uygulandıkları için farklı şekilde yapılır.

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.