上一篇提到了 Context 及 Schema 的概念,這篇要來談談實作的部份。Phoenix 從最初就是採用 Ecto 這個函式庫來處理資料庫相關的事務。所以你會發現在 mix 指令裡,用 ecto
做為前綴的,都跟資料庫有關。
$ mix --help
...
mix ecto # Prints Ecto help information
mix ecto.create # Creates the repository storage
mix ecto.drop # Drops the repository storage
mix ecto.dump # Dumps the repository database structure
mix ecto.gen.migration # Generates a new migration for the repo
mix ecto.gen.repo # Generates a new repository
mix ecto.load # Loads previously dumped database structure
mix ecto.migrate # Runs the repository migrations
mix ecto.migrations # Displays the repository migration status
mix ecto.rollback # Rolls back the repository migrations
...
而跟 Rails 一樣,Ecto 也支援多種不同的資料庫,包括預設的 PostgreSQL、MySQL (MariaDB)、MSSQL、SQLite,及 Erlang 內建的 Mnesia。
而官方文件第一頁,就說明了 Ecto 可以分為四個主要的組件:
Ecto.Repo
- 每個 Repo 在系統中代表了一個資料庫,每個 Phoenix 可以設定多個 Repo,只要指定好 adapter 與認證方式即可。有了 Repo 之後,我們會用這個模組下的函式來對此資料庫進行增刪改查等動作。
Ecto.Schema
- 這就是我們在上一篇所提到的 schema,用來將資料表的資料對應成 Elixir 裡的 struct 結構。但是在使用 ecto 時並不一定要使用 schema 來產生 struct,而直接將結果用 Map 鍵值對的串列 ([%{}]
) 來表示查詢的結果。
Ecto.Changeset
- 在資料真的寫入資料庫前,我們會用 Changeset 來濾掉不應該有的資料及加上額外欄位,Changeset 也提供了驗證及追蹤改變的內容的機制。
Ecto.Query
- 用 Elixir 的語法來組合查詢條件的模組。避免用純 SQL 會遇到的問題,如 SQL injection 等。由於設計的很符合 function composition 的原則,所以各個 Ecto.Query
條件可以簡單的組合在一起。
Ecto 提供了兩種建構 SQL 查詢語句的語法。我們先來介紹比較好懂的 keyword-base 語法。看起來長得像這樣:
query =
from p in "posts",
where: p.id > 10,
order_by: [desc: :created_at],
select: [p.title, p.content]
看起來跟正統 SQL 相當類似,但其實分解來看,就是一個 from/2
函式呼叫。第一個參數 p in "posts"
是 Ecto.Query.API
的函式。而第二個參數則是一個 keyword list:[where: 條件1, order: 條件2, select: 條件3]
。基本上想得到的 SQL 功能像 join, group_by, union,甚至是 subquery 都可以很直覺的組合出來。
要注意的是方才 query 這個變數的值,還不是對資料庫查詢的結果,而是一個組好的 SQL query 語句,型別是 Ecto.Query
:
#Ecto.Query<from p in HelloPhx.Blog.Post, where: p.id > 10,
order_by: [desc: p.created_at], select: [p.title, p.content]>
要得到查詢的結果,我們要用 Repo.all/1
來真的進行查詢:
iex> Repo.all(query)
[
%HelloPhx.Blog.Post{
content: "Hello Elixir!",
draft: false,
id: 11,
inserted_at: ...,
title: "first post",
updated_at: ...,
},
]
除了 keyword base 的語法之外,你也可以用 function composition 的想法來組合 SQL 查詢字串。由於每個 function call 都是一個 expression,所以稱為 expression base 語法:
"posts"
|> where([p], p.id > 10)
|> order_by(desc: :created_at)
|> select([:title, :content])
而因為這種寫法會大量用到 |>/2
運算子,所以也叫做 pipe-base 語法,在官方文件上這兩個名稱指的是同一種東西。而至於要用哪一種語法,可以依當下的需求選擇。
比起每個查詢都要一次寫完一整句,更棒的是能把查詢的各個部份拆開,就可以重複利用已有的語句來進行各種組合:
post_query = from p in Post, order_by: [desc: :created_at]
title_query = from p in post_query, select: [:title]
實務上會用具名函式的方式來組織各種查詢:
def post_query(id) do
from p in Post, where: p.id > ^id
end
def title_query(query \\ Post) do
from p in query, select: :title
end
query = 10 |> post_query |> title_query
而在 Ecto 中要對資料庫新增及修改,會使用 changesets 這個機制。我們在我們的 Schema 檔案裡已經有一段範例了:
# lib/hello_phx/blog/post.ex
def changeset(%Post{} = post, attrs) do
post
|> cast(attrs, [:title, :content, :draft])
|> validate_required([:title, :content, :draft])
end
這個預設的 changeset 的第一行,表示輸入的 attrs
參數裡,只有 :title
、 :content
及 :draft
會被留下來,其它的參數會被濾掉。
而第二行處理的是驗證,這三個欄位都是必填值,若無法通過所有的驗證,這個 changeset 就會被是為不合格:
iex> changeset = Post.changeset(%Post{}, %{})
iex> changeset.valid?
false
iex> changeset.errors
[title: {"can't be blank", [validation: :required]},
content: {"can't be blank", [validation: :required]},
draft: {"can't be blank", [validation: :required]},]
而若 changeset 不合格,執行 Repo.insert(changeset)
時並不會把資料寫入資料庫,而是回傳這個帶有 error 屬性的 changeset,讓後續的流程得以判別及處理,例如在表單上帶入原先填的值,並在特定欄位顯示錯誤等等。
透過 changeset 的機制,我們可以做出許多不同情況下使用的函式,例如 api_changeset
、admin_changset
、update_changeset
等等,並在 Context 裡,依需求調用它們。
schema.rb
如果在某些情況下,你就是很想念 Rails 裡那張 schema.rb
,可以不用一步步遷移資料庫,而是一次到位變成最終的樣子,該怎麼辦呢?
其實仔細想想,資料庫原本就有提供這個機制了:用 SQL dump 做一張描述怎麼變更資料庫的 SQL script。而 Ecto 也幫你把這件事包裝起來了,只要下一個 mix 指令就可以:
$ mix ecto.dump
The structure for HelloPhx.Repo has been dumped to /hello_phx/priv/repo/structure.sql
這樣就能在提示的路徑裡找到那張 SQL script。我們可以用 mix ecto.load
載入 script。但既然是通用的 SQL script,也能利用各種接受 SQL script 的方式來變動資料庫。
諸如 Ecto.Multi、Ecto.Repo.stream,或是當 schema 有關聯時 (has_many、belongs_to),預設不會載入關聯的 schema,而要用 Repo.preload 來決定哪些要載入 (不會有 N + 1 的問題) 等等,限於篇幅就請大家參考官方文件 Ecto 了。
另外 Ecto 做為一個獨立的函式庫,並不是只能在 Phoenix 專案裡才能使用,由於界面拆分的相當乾淨,所以用在任何要處理資料庫操作的專案上,操作起來都能有一樣好的體驗。
下一篇我們要來談談 View 的部份了。
Happy hacking! 明天見。