Ecto是一個Elixir的數據庫ORM庫
來自: https://medium.com/@tingwang_de/ecto-fbcdb06eaf2c#.cu3pu7pyy
原文,文中黑體字是我的理解。
Ecto是一個Elixir的數據庫ORM庫
Ecto 的主要組成組件有四個:
- Ecto.Repo?—?Repository 是在數據倉庫上的一層封裝。通過 repository 我們可以創建、更新、銷毀或者查詢已有的數據。 為了和數據庫連接,一個 repository 需要一個適配器和相應的權限配置。
- Ecto.Schema?—?Schema 允許開發人員定義數據結構來映射底層的數據庫數據結構
- Ecto.Changeset?—?Changeset 為開發人員提供了一個方法來過濾(filter)、轉換(cast)外部數據。在提交數據之前,changeset還提供了跟蹤、驗證變化的機制,保證提交的數據是有效的。在不同的上下文中,對數據的有效性的驗證(哪些字段需要驗證、如何驗證)有可能是不一樣的, 所以在Ecto中, 一個Model里會定義多個 changesets 來應對不同的場景。
- Ecto.Query?—?通過Elixir的語法, 把數據從 repository 中取出來。查詢在 Ecto 中是安全的,避免了一般的安全問題, 比如 SQL注入等。查詢也是可以被組合的,(組合是FP最總要的武器),允許開發者一塊一塊的創建查詢再整體組合起來。
Repositories
Repository 是在數據庫外的一層, 我們可以通過下面的代碼創建一個 repository
defmodule Repo do use Ecto.Repo, otp_app: :my_append
Repo必須在應用的配置文件中配置, 通常是在 config/config.exs里:
config: my_app, Repo, adapter: Ecto.Adapter.Postgres, database: "ecto_sample", username: "postgres", password: "postgres", hostname: "localhost"
在Ecto中的每個repository都會定義一個 start_link/0, 使用repository之前這個函數必須被調用。(這個應該是otp里啟動了一個erlang的進程, 之后對repository的調用都是對一個進程的調用) 一般這個函數是不需要手動調用的,而是在application的 supervision tree里。(這個也是erlang運行的方式, 每個進程都需要看護進程)
如果在生成應用的時候我們用了 supervisor (通過傳遞?—?sup 給 mix new),lib/my_app.ex 文件中會包含應用啟動的回調函數, 這個函數內定義并啟動你的監控進程(supervisor)。你只需要編輯 start/2 這個函數來把repository作為一個supervisor啟動,這個supervoir是在應用的監控下的。
def start(_type, _args) do import Supervisor.Spec children = [ supervisor(Repo, [] ] opts = [strategy: one_for_one, name: MyApp.Supervisor] Supervisor.start_link(childern, opts)
Schema
Schema提供了一組函數方便你定義結構化數據,數據字段直接的關系并且把這些改變應用到repository里。
讓我們看個例子:
defmodule Weather do use Ecto.Schema schema "weather" do field :city, :string field :temp_lo, :integer field :temp_hi, :integer field :procp, :float, default: 0.0 endend
定義schema的時候, ecto會自動的定義一個結構體, 這個結構體包含了schema的所有字段。
iex> wheather = %Weather{temp_lo: 30}iex> weather.temp_lo30
Schema還允許我們和repository交互
iex> weather = %Weather{temp_lo: 0, temp_hi: 23}iex> Repo.insert!(weather)%Weather{...}
在持久化 weather 到數據庫庫之后, %Weather{} 的一個新拷貝會被返回, 返回的這個拷貝會有插入后的主鍵只(id)。我們可以通過這個主鍵從repository中獲取結構體。
# 獲得結構體iex> weather = Repo.get Weather, 1%Weather{id: 1, ...}# 刪除數據iex> Repo.delete!(weather)%Weather{id: 1, ...}
注意:使用 Ecto.Schema就會自動添加一個 :id (:integer),這個id會被用作主鍵。如果你想使用其他字段作為主鍵, 你可以通過在schema/2前使用 @primary_key。更多內容可以常看 Ecto.Schema的文檔。
這里我們可以看到數據和數據獲取代碼是怎樣隔離開的。一個在Repository里,一個在schema里。 這樣做的好處是:
- 數據定義在結構體內, 保證了數據是輕量級的而且能夠被序列化。在很多語言中,數據常常被定義為一個肥大的對象,包含了數據狀態變換的方法,這些導致了數據很難被序列化、維護,而且很難被理解。
- 把數據和repository分開, 也保證了repository沒有過多沒必要的代碼,從而可以直接、高效的訪問數據。這也是FP的方式, 就是一個處理數據的鏈條
changeset
雖然在上面的例子里我們直接向repository里插入、刪除結構體,在更新數據的時候我們需要用到changeset來保證Ecto能夠有效的跟蹤變化。
不僅如此,changeset還允許開發人員對修改的內容做過濾,強制轉換或者驗證其有效性,只有通過之后才會把修改應用于數據。例如下面我們定義了一個schema
defmodule User do use Ecto.Schema import Ecto.Changeset schema "users" do field :name field :email field :age, :integer end def changeset(user, params \\ :invalid) do user |> cast(params, [:name, :email, :age]) |> validate_required([:name, :email]) |> validate_format(:email, ~r/@/) |> validate_inclusion(age, 18..100) endend
changeset/2 函數首先調用了Ecto.Changeset.cast/3這個函數,傳入結構體、參數和必選和可選的字段; 這個函數返回一個changeset。 參數是一個map, 鍵是二進制值,這個鍵對應的值會根據其在schema中的定義做強制的轉換。
任何不在必選或者可選參數表里的參數都會被忽略掉。如果一個字段被定義為必選, 但是在結構體里和參數中都沒有傳入, 這個changeset會標記上一個錯誤信息,這個changeset也會被標定為無效。
強制轉換之后, changeset被傳給Ecto.Changeset.validate/2,這個函數只會驗證有改變的字段。也就是說,如果一個字段不是作為參數傳入的, 這個字段壓根就不會被驗證。model轉入的是原本的模型,參數傳入的是修改的字段集。例如,在參數集里只有 “name” 和 “email”鍵,那么“age”的驗證是不會被運行的。
def update(id, params) do changeset = User.changeset Repo.get!(User, id), params["user"] case Repo.update(changeset) do {:ok, user} -> send_resp conn, 200, "Ok" {:error, changeset} _> send_resp conn, 400, "Bad request" endend
user和參數傳入changeset/2, 另一個changeset被返回。如果changeset是有效的, 我們把修改持久化到數據庫, 如果不是有效的我們會返回一個400。
下面的例子是創建一個user
def create(id, params) do changeset = User.changeset %User{}, params["user"] case Repo.insert(changeset) do {:ok, user} -> send_resp conn, 200, "Ok" {:error, changeset} -> send_resp conn, 400, "Bad request" endend
顯示的定義changeset (一個model會有多個changeset) 的好處是我們可以根據不同的情景定義不同的changeset。例如,我們可以分別處理創建和更新的changeset:
def create_changeset(user, params) do # 創建時使用的Changeset enddef update_changeset(user, params) do # 修改時使用的Changesetend
Changeset還可以根據數據庫的約束(constrains)例如唯一索引、外鍵等檢驗是否有效, 如果不通過會產生一個錯誤, 這樣開發人員就可以在保證數據庫的一致性的前提下,還能給終端用戶友好的提示。查看Ecto.Changeset.unique_contraint/3的文檔查看跟多的例子并且了解_constraint函數的用法
Query
最后,Ecto允許你用Elixir的語法寫查詢語句,并把這些查詢發送給repository,repository會把這些elixir寫的查詢翻譯成為底層數據庫的查詢。讓我們看個例子:
import Ecto.Query, only[from: 2]query = from w in Weather, where: w.prcp > 0 or is_nil(w.prcp), select: w# 返回符合查詢條件的 %Weather{} Repo.all(query)
查詢是在from宏中定義擴展的。支持的關鍵字包括:
- :distinct
- :where
- :order_by
- :offset
- :limit
- :lock
- :group_by
- :having
- :join
- :select
- preload
在Ecto.Query模塊中你可以找到這些關鍵字的解釋還有具體的例子。所有支持在查詢語句里使用的函數都在Ecto.Query.API里。
當我們在寫查詢語句的時候, 我們其實在使用查詢語句的DSL, 如果在這個DSL里使用Elixir的函數或者嵌入參數,我們需要在函數或者參數的前面加上^:
def min_prcp(min) do from w in Weather, where: w.prcp > ^min or is_nil(w.prcp)end
除了Repo.all/1會返回所有符合條件的結果之外, 我們還可以用Repo.first/1只返回一個結果或者nil,Repo.first!/1會返回一個結果或者在沒有結果的時候報錯,Repo.get/2根據主機獲得結果。
如果你想生寫一些SQL代碼的話, Ecto提供了片段(fragments)參看Ecto.Query.API.fragment/1。初次之外,很多的適配器還提供了直接查詢的接口,例如Ecto.Adapters.SQL.query/4。