동시성 Elixir 앱을 위한 타협 없는 데이터베이스 래퍼 Ecto를 만나보세요
게시 됨: 2022-03-11Ecto는 Elixir 언어로 쿼리를 작성하고 데이터베이스와 상호 작용하기 위한 도메인 특정 언어입니다. 최신 버전(2.0)은 PostgreSQL 및 MySQL을 지원합니다. (MSSQL, SQLite 및 MongoDB에 대한 지원은 향후 제공될 예정입니다.) Elixir를 처음 사용하거나 경험이 거의 없는 경우 Kleber Virgilio Correia의 Getting Started with Elixir Programming Language를 읽는 것이 좋습니다.
Ecto는 네 가지 주요 구성 요소로 구성됩니다.
- 엑토.레포. 데이터 저장소 주변의 래퍼인 리포지토리를 정의합니다. 이를 사용하여 리포지토리를 삽입, 생성, 삭제 및 쿼리할 수 있습니다. 데이터베이스와 통신하려면 어댑터와 자격 증명이 필요합니다.
- 엑토.스키마. 스키마는 모든 데이터 소스를 Elixir 구조체에 매핑하는 데 사용됩니다.
- 엑토.체인지셋. 변경 세트는 개발자가 외부 매개변수를 필터링 및 캐스팅하는 방법과 변경 사항을 데이터에 적용하기 전에 추적하고 유효성을 검사하는 메커니즘을 제공합니다.
- 엑토.쿼리. 리포지토리에서 정보를 검색하기 위한 DSL과 유사한 SQL 쿼리를 제공합니다. Ecto의 쿼리는 SQL 주입과 같은 일반적인 문제를 피하면서 보안이 유지되는 동시에 구성 가능하므로 개발자가 쿼리를 한 번에 모두 작성하는 대신 조각별로 작성할 수 있습니다.
이 튜토리얼에서는 다음이 필요합니다.
- Elixir 설치(1.2 이상 설치 가이드)
- PostgreSQL 설치
- 데이터베이스를 생성할 수 있는 권한이 정의된 사용자(참고: 이 자습서 전체에서 암호가 "postgres"인 사용자 "postgres"를 예로 사용합니다.)
설치 및 구성
우선 Mix를 사용하여 감독자가 있는 새 앱을 만들어 보겠습니다. Mix는 애플리케이션 생성, 컴파일, 테스트, 종속성 관리 등을 위한 작업을 제공하는 Elixir와 함께 제공되는 빌드 도구입니다.
mix new cart --sup
그러면 초기 프로젝트 파일이 포함된 디렉토리 카트가 생성됩니다.
* 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
데이터베이스에 대한 연결을 유지할 감독자 트리가 필요하기 때문에 --sup
옵션을 사용하고 있습니다. 다음으로 cd cart
가 있는 cart
디렉터리로 이동하여 mix.exs
파일을 열고 내용을 바꿉니다.
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
에서 우리는 application :postgrex, :ecto
를 추가해야 우리 애플리케이션 내에서 사용할 수 있습니다. 또한 defp deps do
postgrex (데이터베이스 어댑터) 및 ecto 를 추가하여 종속성을 추가해야 합니다. 파일을 편집했으면 콘솔에서 다음을 실행합니다.
mix deps.get
이것은 모든 종속성을 설치하고 설치된 패키지의 모든 종속성과 하위 종속성을 저장하는 mix.lock
파일을 생성합니다(번들러의 Gemfile.lock
과 유사).
엑토 레포
이제 애플리케이션에서 repo를 정의하는 방법을 살펴보겠습니다. 하나 이상의 리포지토리를 가질 수 있습니다. 즉, 둘 이상의 데이터베이스에 연결할 수 있습니다. config/config.exs
파일에서 데이터베이스를 구성해야 합니다.
use Mix.Config config :cart, ecto_repos: [Cart.Repo]
다음 명령을 실행할 수 있도록 최소값을 설정했을 뿐입니다. :cart, cart_repos: [Cart.Repo]
줄을 사용하여 Ecto에 사용 중인 저장소를 알려줍니다. 이것은 우리가 많은 저장소를 가질 수 있도록 하기 때문에, 즉 우리가 여러 데이터베이스에 연결할 수 있도록 하는 멋진 기능입니다.
이제 다음 명령을 실행합니다.
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]
이 명령은 리포지토리를 생성합니다. 출력을 읽으면 앱에 감독자와 리포지토리를 추가하라는 메시지가 표시됩니다. 감독부터 시작합시다. 우리는 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
이 파일에서 우리는 supervisor(Cart.Repo, [])
를 정의하고 그것을 자식 목록에 추가합니다(Elixir에서 목록은 배열과 유사합니다). 우리는 전략 strategy: :one_for_one
이는 감독되는 프로세스 중 하나가 실패하면 감독자가 해당 프로세스만 기본 상태로 다시 시작한다는 의미입니다. 여기에서 감독자에 대해 자세히 알아볼 수 있습니다. lib/cart/repo.ex
를 보면 이 파일이 이미 생성되었음을 알 수 있습니다. 이는 애플리케이션에 대한 Repo 가 있음을 의미합니다.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end
이제 구성 파일 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"
데이터베이스에 대한 모든 구성을 정의했으면 이제 다음을 실행하여 생성할 수 있습니다.
mix ecto.create
이 명령은 데이터베이스를 생성하고 기본적으로 구성을 완료했습니다. 이제 코딩을 시작할 준비가 되었지만 먼저 앱의 범위를 정의하겠습니다.
인라인 항목으로 송장 작성
데모 애플리케이션을 위해 간단한 인보이스 도구를 만들 것입니다. 변경 세트(모델)의 경우 Invoice , Item 및 InvoiceItem 이 있습니다. InvoiceItem 은 Invoice 및 Item 에 속합니다. 이 다이어그램은 모델이 서로 어떻게 관련되는지를 나타냅니다.
다이어그램은 매우 간단합니다. 모든 세부 정보를 저장하는 많은 invoke_items 가 있는 테이블 송장 과 많은 invoke_items 가 있는 테이블 항목 이 있습니다. invoke_items 테이블에 있는 invoke_id 와 item_id 의 타입이 UUID임을 알 수 있다. API를 통해 앱을 노출하려는 경우 경로를 난독화하는 데 도움이 되므로 UUID를 사용하고 있으며 순차 번호에 의존하지 않기 때문에 동기화가 더 간단합니다. 이제 Mix 작업을 사용하여 테이블을 생성해 보겠습니다.
Ecto.Migration
마이그레이션은 데이터베이스 스키마를 수정하는 데 사용되는 파일입니다. Ecto.Migration은 테이블 생성, 인덱스 추가, 제약 조건 생성 및 기타 스키마 관련 작업을 위한 일련의 메서드를 제공합니다. 마이그레이션은 애플리케이션이 데이터베이스와 동기화된 상태를 유지하는 데 실제로 도움이 됩니다. 첫 번째 테이블에 대한 마이그레이션 스크립트를 생성해 보겠습니다.
mix ecto.gen.migration create_invoices
그러면 마이그레이션을 정의할 priv/repo/migrations/20160614115844_create_invoices.exs
와 유사한 파일이 생성됩니다. 생성된 파일을 열고 내용을 다음과 같이 수정합니다.
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
내부 메서드 def change do
에서 데이터베이스에 대한 SQL을 생성할 스키마를 정의합니다. create table(:invoices, primary_key: false) do
는 테이블 송장 을 생성합니다. primary_key: false
를 설정했지만 UUID 유형의 ID 필드, 텍스트 유형의 고객 필드, 날짜 유형의 날짜 필드를 추가합니다. timestamps
메소드는 Ecto가 레코드가 inserted_at
된 시간과 업데이트된 시간으로 각각 자동으로 채우는 insert_at 및 updated_at
필드를 생성합니다. 이제 콘솔로 이동하여 마이그레이션을 실행합니다.
mix ecto.migrate
정의된 모든 필드가 있는 테이블 invoice
을 만들었습니다. 항목 테이블을 생성해 보겠습니다.
mix ecto.gen.migration create_items
이제 생성된 마이그레이션 스크립트를 편집합니다.
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
여기서 새로운 것은 12자리 숫자를 허용하는 소수 필드이며, 그 중 2자리는 숫자의 소수 부분입니다. 마이그레이션을 다시 실행해 보겠습니다.
mix ecto.migrate
이제 항목 테이블을 생성했으며 마지막으로 송장 _항목 테이블을 생성해 보겠습니다.
mix ecto.gen.migration create_invoice_items
마이그레이션 수정:
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
보시다시피 이 마이그레이션에는 몇 가지 새로운 부분이 있습니다. 가장 먼저 알 수 있는 것은 add :invoice_id, references(:invoices, type: :uuid, null: false)
입니다. 이렇게 하면 송장 테이블을 참조하는 데이터베이스의 제약 조건이 있는 invoke_id 필드가 생성됩니다. item_id 필드에 대해 동일한 패턴이 있습니다. 또 다른 다른 점은 create index(:invoice_items, [:invoice_id])
를 생성하는 방식 입니다 .
Ecto.Schema 및 Ecto.Changeset
Ecto에서 Ecto.Model
은 Ecto.Schema
를 사용하기 위해 더 이상 사용되지 않으므로 모델 대신 모듈 스키마를 호출합니다. 변경 집합을 만들어 보겠습니다. 가장 간단한 변경 세트 항목으로 시작하여 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
상단에서 use Ecto.Schema
를 사용하여 변경 집합에 코드를 삽입합니다. 또한 Ecto.Changeset 에서 기능을 가져오기 위해 import Ecto.Changeset
을 사용하고 있습니다. 가져올 특정 메서드를 지정할 수도 있지만 간단하게 유지하겠습니다. alias Cart.InvoiceItem
을 사용하면 잠시 후에 보게 될 InvoiceItem 변경 집합 내부에 직접 작성할 수 있습니다.
엑토스키마
@primary_key {:id, :binary_id, autogenerate: true}
는 기본 키가 자동 생성되도록 지정합니다. UUID 유형을 사용하기 때문에 schema "items" do
로 스키마를 정의하고 블록 내에서 각 필드와 관계를 정의합니다. 마이그레이션과 매우 유사하게 이름 을 문자열로, 가격 을 10진수로 정의했습니다. 다음으로 매크로 has_many :invoice_items, InvoiceItem
은 Item 과 InvoiceItem 간의 관계를 나타냅니다. 규칙에 따라 invoke_items 테이블에서 필드 이름을 item_id 로 지정했으므로 외래 키를 구성할 필요가 없습니다. 마지막으로 타임스탬프 메서드는 insert_at 및 updated_at 필드를 설정합니다.
Ecto.Changeset
def changeset(data, params \\ %{}) do
function은 다른 함수를 통해 파이프할 params가 있는 Elixir 구조체를 받습니다. cast(params, @fields)
는 값을 올바른 유형으로 캐스팅합니다. 예를 들어 params에 문자열만 전달할 수 있으며 해당 문자열은 스키마에 정의된 올바른 유형으로 변환됩니다. validate_required([:name, :price])
는 이름 과 가격 필드가 있는지 확인하고, validate_number(:price, greater_than_or_equal_to: Decimal.new(0))
는 숫자가 0보다 크거나 같은지 확인합니다. 이 경우 Decimal.new(0)
.
많은 내용을 다루었으므로 개념을 더 잘 이해할 수 있도록 콘솔에서 예제와 함께 살펴보겠습니다.
iex -S mix
그러면 콘솔이 로드됩니다. -S mix
는 현재 프로젝트를 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>
이것은 오류 없이 유효한 Ecto.Changeset
구조체를 반환합니다. 이제 저장해 보겠습니다.
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을 표시하지 않습니다. 이 경우 모든 값이 설정된 Cart.Item 구조체를 반환합니다. insert_at 및 updated_at 에 타임스탬프가 포함되어 있고 id 필드에 UUID 값이 있는 것을 볼 수 있습니다. 몇 가지 다른 경우를 살펴보겠습니다.
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)
이제 가격을 직접 %Cart.Item{price: Decimal.new(20)}
설정하여 Scissors
항목을 다른 방식으로 설정했습니다. 문자열을 가격으로 전달한 첫 번째 항목과 달리 올바른 유형을 설정해야 합니다. 우리는 float를 전달할 수 있었고 이것은 10진수 유형으로 캐스트되었을 것입니다. 예를 들어 %Cart.Item{price: 12.5}
를 전달하면 항목을 삽입할 때 유형이 일치하지 않는다는 예외가 발생합니다.

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>
콘솔을 종료하려면 Ctrl+C를 두 번 누릅니다. 유효성 검사가 작동하고 가격이 영(0)보다 크거나 같아야 함을 알 수 있습니다. 보시다시피 모듈의 구조를 정의하는 방법과 관련된 부분인 Ecto.Schema 스키마와 모든 유효성 검사 및 캐스팅인 변경 집합 Ecto.Changeset 을 모두 정의했습니다. 계속해서 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
이 변경 집합은 더 크지만 대부분은 이미 익숙할 것입니다. 다음은 다음과 같습니다 belongs_to :invoice, Cart.Invoice, type: :binary_id
는 곧 생성할 Cart.Invoice 변경 집합과의 "belongs to" 관계를 정의합니다. 다음 belongs_to :item
은 항목 테이블과의 관계를 생성합니다. @zero Decimal.new(0)
을 정의했습니다. 이 경우 @zero 는 모듈 내부에서 액세스할 수 있는 상수와 같습니다. 변경 집합 함수에는 새로운 부분이 있으며 그 중 하나는 foreign_key_constraint(:invoice_id, message: "Select a valid invoice")
입니다. 이렇게 하면 제약 조건이 충족되지 않을 때 예외를 생성하는 대신 오류 메시지가 생성됩니다. 마지막으로 set_subtotal 메서드는 부분합을 계산합니다. 가격과 수량 모두가 있는 경우 변경 집합을 전달하고 소계를 계산하여 새 변경 집합을 반환합니다.
이제 Cart.Invoice 를 생성해 보겠습니다. 따라서 다음을 포함하도록 lib/cart/invoice.ex
파일을 만들고 편집하십시오.
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 변경 집합에는 몇 가지 차이점이 있습니다. 첫 번째는 스키마 내부에 있습니다. has_many : invoice_items has_many :invoice_items, InvoiceItem, on_delete: :delete_all
은 송장을 삭제할 때 연결된 모든 송장 항목이 삭제됨을 의미합니다. 그러나 이것은 데이터베이스에 정의된 제약 조건이 아님을 명심하십시오.
더 잘 이해하기 위해 콘솔에서 create 메소드를 사용해 보겠습니다. 여기에서 사용할 항목("종이", "가위")을 생성했을 수 있습니다.
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) }
Cart.Repo.all 을 사용하여 모든 항목을 가져오고 Enum.map 함수를 사용하여 각 항목의 item.id
를 가져옵니다. 두 번째 줄에서는 id1
과 id2
에 각각 첫 번째와 두 번째 item_ids를 할당합니다.
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})
인보이스가 invoke_items로 생성되었으며 이제 모든 인보이스를 가져올 수 있습니다.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
Invoice 를 반환하는 것을 볼 수 있지만 invoke_items 도 보고 싶습니다.
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)
Repo.preload 함수를 사용하여 invoice_items
를 얻을 수 있습니다. 쿼리를 동시에 처리할 수 있습니다. 제 경우 쿼리는 다음과 같습니다.
iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)
엑토.쿼리
지금까지 관계가 있는 새 항목과 새 송장을 만드는 방법을 보여 주었습니다. 그러나 쿼리는 어떻습니까? 음, 데이터베이스에 대한 쿼리를 만드는 데 도움이 될 Ecto.Query 를 소개하겠습니다. 하지만 먼저 더 잘 설명하려면 더 많은 데이터가 필요합니다.
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")})
이제 8개의 항목이 있어야 하며 "초콜릿"이 반복됩니다. 어떤 항목이 반복되는지 알고 싶을 수 있습니다. 다음 쿼리를 시도해 보겠습니다.
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"}]
쿼리에서 항목 이름과 항목 테이블에 항목이 나타나는 횟수가 포함된 맵을 반환하려는 것을 알 수 있습니다. 또는 가장 잘 팔리는 제품을 확인하는 데 더 관심이 있을 수 있습니다. 이를 위해 송장을 생성해 보겠습니다. 먼저 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"}
보시다시피 우리는 이해력을 사용하여 지도를 만들었습니다.
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]
송장을 생성하려면 invoice_items
_항목 매개변수에 가격을 추가해야 하지만 항목의 ID를 전달하고 가격이 자동으로 채워지도록 하는 것이 좋습니다. 이를 수행하기 위해 Cart.Invoice 모듈을 변경합니다.
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
가장 먼저 알 수 있는 것은 데이터베이스를 쿼리할 수 있는 Ecto.Query 가 추가되었다는 것입니다. 새로운 함수는 항목을 검색하고 각 항목의 가격을 찾아 설정하는 defp items_with_prices(items) do
입니다.
먼저 defp items_with_prices(items) do
는 목록을 인수로 받습니다. item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end)
를 사용하여 모든 항목을 반복하고 item_id 만 얻습니다. 보시다시피, 맵은 이들 중 하나를 키로 가질 수 있기 때문에 atom :item_id
또는 문자열 "item_id"로 액세스합니다. 쿼리 q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids)
는 item_ids
에 있는 모든 항목을 찾고 다음을 포함하는 지도를 반환합니다. item.id
및 item.price
. 그런 다음 쿼리 prices = Repo.all(q)
을 실행하여 지도 목록을 반환할 수 있습니다. 그런 다음 항목을 반복하고 가격을 추가할 새 목록을 만들어야 합니다. Enum.map(items, fn(item) ->
각 항목을 반복하고 가격을 찾습니다. Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0
, item_id
, 수량 및 가격을 사용하여 새 목록을 생성하고 이를 통해 더 이상 각 invoice_items
항목에 가격을 추가할 필요가 없습니다.
더 많은 인보이스 삽입
기억하시겠지만 이전에 items["Gum"]
이름을 사용하여 id 에 액세스할 수 있는 맵 항목 을 만들었습니다. 이렇게 하면 간편하게 invoke_items 를 생성할 수 있습니다. 더 많은 인보이스를 생성해 보겠습니다. 콘솔을 다시 시작하고 다음을 실행합니다.
Iex -S mix
iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)
모든 invoke_items 및 송장을 삭제하여 백지 상태로 만듭니다.
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})
이제 3개의 송장이 있습니다. 첫 번째는 2개 항목, 두 번째 항목은 3개 항목, 세 번째 항목은 6개 항목입니다. 이제 가장 많이 팔린 상품이 무엇인지 알아볼까요? 이에 답하기 위해 수량 및 소계(가격 x 수량)별로 베스트 셀러 항목을 찾는 쿼리를 만들 것입니다.
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 를 가져온 다음 alias Cart.{InvoiceItem, Item, Repo}
을 지정하므로 각 모듈의 시작 부분에 Cart를 추가할 필요가 없습니다. 첫 번째 함수 items_by_quantity 는 items_by
함수를 호출하여 :quantity
매개변수를 전달하고 Repo.all 을 호출하여 쿼리를 실행합니다. items_by_subtotal 함수는 이전 함수와 유사하지만 :subtotal
매개변수를 전달합니다. 이제 items_by 를 설명하겠습니다.
-
from i in Item
이 매크로는 항목 모듈을 선택합니다. -
join: ii in InvoiceItem, on: ii.item_id == i.id
는 "items.id = invoke_items.item_id" 조건에 대한 조인을 생성합니다. -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}
, 먼저 원하는 모든 필드가 있는 맵을 생성합니다. 먼저 Item에서 id와 이름을 선택합니다. 그리고 우리는 연산자 합계를 합니다. field(ii, ^type)는 매크로 필드를 사용하여 필드에 동적으로 액세스합니다. -
group_by: i.id
, items.id별로 그룹화합니다. -
order_by: [desc: sum(field(ii, ^type))]
그리고 마지막으로 합계를 내림차순으로 정렬합니다.
지금까지 쿼리를 목록 스타일로 작성했지만 매크로 스타일로 다시 작성할 수 있습니다.
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
목록 형식으로 쿼리를 작성하는 것이 더 읽기 쉬워서 선호합니다.
결론
Ecto를 사용하여 앱에서 할 수 있는 작업의 상당 부분을 다루었습니다. 물론 Ecto 문서에서 더 많은 것을 배울 수 있습니다. Ecto를 사용하면 Erlang 가상 머신 덕분에 쉽게 확장할 수 있는 적은 노력으로 동시 내결함성 응용 프로그램을 만들 수 있습니다. Ecto는 Elixir 애플리케이션의 저장을 위한 기반을 제공하고 데이터를 쉽게 관리할 수 있는 기능과 매크로를 제공합니다.
이 자습서에서는 Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query 및 Ecto.Repo를 조사했습니다. 이러한 각 모듈은 애플리케이션의 다른 부분에서 도움이 되며 코드를 보다 명확하고 유지 관리하고 이해하기 쉽게 만듭니다.
튜토리얼의 코드를 확인하고 싶다면 여기 GitHub에서 찾을 수 있습니다.
이 튜토리얼이 마음에 들었고 더 많은 정보에 관심이 있다면 Phoenix(멋진 프로젝트 목록), Awesome Elixir 및 ActiveRecord와 Ecto를 비교하는 이 강연을 추천합니다.