iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 23
1

上一篇提到了 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 可以分為四個主要的組件:

  • 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 查詢

Keyword base 語法

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: ...,
  },
]

Expression base 語法

除了 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 語法,在官方文件上這兩個名稱指的是同一種東西。而至於要用哪一種語法,可以依當下的需求選擇。

組合 Query

比起每個查詢都要一次寫完一整句,更棒的是能把查詢的各個部份拆開,就可以重複利用已有的語句來進行各種組合:

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

用 Changeset 來驗證與寫入

而在 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_changesetadmin_changsetupdate_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 專案裡才能使用,由於界面拆分的相當乾淨,所以用在任何要處理資料庫操作的專案上,操作起來都能有一樣好的體驗。

重點回顧

  • Ecto 是 Phoenix 內建的資料庫處理模組
  • Ecto.Repo 代表資料庫,用來實際執行增刪改查行為
  • Ecto.Query 用來組織 SQL 查詢語句
  • Ecto.Changeset 是異動資料時的寫入及驗證機制
  • Ecto 也可以在非 Phoenix 的專案裡單獨使用

下一篇我們要來談談 View 的部份了。
Happy hacking! 明天見。


上一篇
再會, model
下一篇
View 與 Template
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

1 則留言

0
taiansu
iT邦新手 5 級 ‧ 2018-01-12 04:57:33

已更新

我要留言

立即登入留言