Ecto 是 RDB 的 wrapper。他有個稱為 changesets 的功能,可保存所有對 database 的修改內容,他會封裝在寫入 RDB 前的 receiving external data, casting 及 validating 這些程序。
設定 Ecto
修改 /lib/rumbl/repo.ex
defmodule Rumbl.Repo do
use Ecto.Repo, otp_app: :rumbl
end
修改 /lib/rumbl.ex,enable Ecto supervisor
defmodule Rumbl do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(Rumbl.Repo, []),
# Start the endpoint when the application starts
supervisor(Rumbl.Endpoint, []),
# Start your own worker by calling: Rumbl.Worker.start_link(arg1, arg2, arg3)
# worker(Rumbl.Worker, [arg1, arg2, arg3]),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Rumbl.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
Rumbl.Endpoint.config_change(changed, removed)
:ok
end
end
設定 DB /config/dev.exs
# Configure your database
config :rumbl, Rumbl.Repo,
adapter: Ecto.Adapters.MySQL,
username: "root",
password: "password",
database: "rumbl",
hostname: "localhost",
pool_size: 10
產生資料庫
$ mix ecto.create
DB Schema and Migration
修改 /web/models/user.ex 的內容
defmodule Rumbl.User do
use Rumbl.Web, :model
schema "users" do
field :name, :string
field :username, :string
field :password, :string, virtual: true
field :password_hash, :string
timestamps
end
end
DSL 是用 Elixir macro 實作的,schema 及 field 讓我們指定 DB table 及 Elixir struc,每個 field 會關聯到 DB table 的一個欄位,以及 User struct 的 field。
另外有增加一個 virtual 欄位,這是 password 的 hash 前結果,virtual field 不需要對應到 DB table 的欄位,因為 DB 只要紀錄 hash 後的密碼,Ecto 會自動定義 Elixir struct,所以可以直接使用 %Rumbl.User{}。
在 /web/web.ex 裡面 model 定義為
def model do
quote do
use Ecto.Schema
import Ecto
import Ecto.Changeset
# 增加 , only: [from: 1, from: 2]
import Ecto.Query, only: [from: 1, from: 2]
end
end
現在我們需要用 "migrations" 的功能,將 DB schema 的定義反應到實際的 DB 裡面,migration 會修改資料庫以符合 application 的需要。
$ mix ecto.gen.migration create_user
* creating priv/repo/migrations
* creating priv/repo/migrations/20170903125816_create_user.exs
打開 priv/repo/migrations/20170903125816createuser.exs ,填入 change 的 function 內容(create table: users)
defmodule Rumbl.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string
add :username, :string, null: false
add :password_hash, :string
timestamps()
end
create unique_index(:users, [:username])
end
end
Ecto.Migration API 提供了 create, remove, and change database tables, fields, and indexes 這些 functions。
$ mix ecto.migrate
[info] == Running Rumbl.Repo.Migrations.CreateUser.change/0 forward
[info] create table users
[info] create index users_username_index
[info] == Migrated in 0.0s
要注意,目前是針對 dev 環境的處理,如果要換別的環境,就要設定 MIX_ENV 這個環境變數。
利用 Repository 新增 data
利用 IEX 產生測試資料
$ iex -S mix
iex(1)> alias Rumbl.Repo
Rumbl.Repo
iex(2)> alias Rumbl.User
Rumbl.User
iex(3)> Repo.insert(%User{name: "Jose", username: "josie", password_hash: "<3<3elixir"})
[debug] QUERY OK db=2.7ms
INSERT INTO `users` (`name`,`password_hash`,`username`,`inserted_at`,`updated_at`) VALUES (?,?,?,?,?) ["Jose", "<3<3elixir", "josie", {{2017, 9, 3}, {13, 9, 55, 939011}}, {{2017, 9, 3}, {13, 9, 55, 941294}}]
{:ok,
%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1,
inserted_at: ~N[2017-09-03 13:09:55.939011], name: "Jose", password: nil,
password_hash: "<3<3elixir", updated_at: ~N[2017-09-03 13:09:55.941294],
username: "josie"}}
iex(4)> Repo.insert(%User{name: "Bruce", username: "bruce", password_hash: "7langs"})
[debug] QUERY OK db=1.8ms
INSERT INTO `users` (`name`,`password_hash`,`username`,`inserted_at`,`updated_at`) VALUES (?,?,?,?,?) ["Bruce", "7langs", "bruce", {{2017, 9, 3}, {13, 10, 3, 333839}}, {{2017, 9, 3}, {13, 10, 3, 333851}}]
{:ok,
%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 2,
inserted_at: ~N[2017-09-03 13:10:03.333839], name: "Bruce", password: nil,
password_hash: "7langs", updated_at: ~N[2017-09-03 13:10:03.333851],
username: "bruce"}}
iex(5)> Repo.insert(%User{name: "Chris", username: "chris", password_hash: "phoenix"})
[debug] QUERY OK db=182.8ms
INSERT INTO `users` (`name`,`password_hash`,`username`,`inserted_at`,`updated_at`) VALUES (?,?,?,?,?) ["Chris", "phoenix", "chris", {{2017, 9, 3}, {13, 10, 13, 487813}}, {{2017, 9, 3}, {13, 10, 13, 487825}}]
{:ok,
%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 3,
inserted_at: ~N[2017-09-03 13:10:13.487813], name: "Chris", password: nil,
password_hash: "phoenix", updated_at: ~N[2017-09-03 13:10:13.487825],
username: "chris"}}
iex(6)> Repo.all(User)
[debug] QUERY OK source="users" db=2.6ms
SELECT u0.`id`, u0.`name`, u0.`username`, u0.`password_hash`, u0.`inserted_at`, u0.`updated_at` FROM `users` AS u0 []
[%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1,
inserted_at: ~N[2017-09-03 13:09:55.000000], name: "Jose", password: nil,
password_hash: "<3<3elixir", updated_at: ~N[2017-09-03 13:09:55.000000],
username: "josie"},
%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 2,
inserted_at: ~N[2017-09-03 13:10:03.000000], name: "Bruce", password: nil,
password_hash: "7langs", updated_at: ~N[2017-09-03 13:10:03.000000],
username: "bruce"},
%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 3,
inserted_at: ~N[2017-09-03 13:10:13.000000], name: "Chris", password: nil,
password_hash: "phoenix", updated_at: ~N[2017-09-03 13:10:13.000000],
username: "chris"}]
iex(7)> Repo.get(User, 1)
[debug] QUERY OK source="users" db=1.9ms
SELECT u0.`id`, u0.`name`, u0.`username`, u0.`password_hash`, u0.`inserted_at`, u0.`updated_at` FROM `users` AS u0 WHERE (u0.`id` = ?) [1]
%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1,
inserted_at: ~N[2017-09-03 13:09:55.000000], name: "Jose", password: nil,
password_hash: "<3<3elixir", updated_at: ~N[2017-09-03 13:09:55.000000],
username: "josie"}
從 mix phoenix.server 的 console log 也能發現,查詢的語法已經改用 SQL 連接 DB,而不是 memory DB。
$ mix phoenix.server
[info] GET /users
[debug] Processing by Rumbl.UserController.index/2
Parameters: %{}
Pipelines: [:browser]
[debug] QUERY OK source="users" db=2.2ms decode=4.6ms
SELECT u0.`id`, u0.`name`, u0.`username`, u0.`password_hash`, u0.`inserted_at`, u0.`updated_at` FROM `users` AS u0 []
[info] Sent 200 in 82ms
Building Forms
Phoenix form builder
修改 /web/controllers/user_controller.ex,增加 new 新增 User 的 function
defmodule Rumbl.UserController do
use Rumbl.Web, :controller
# 取得所有 users
def index(conn, _params) do
users = Repo.all(Rumbl.User)
render conn, "index.html", users: users
end
# 顯示某個 id 的 user
def show(conn, %{"id" => id}) do
user = Repo.get(Rumbl.User, id)
render conn, "show.html", user: user
end
alias Rumbl.User
# 利用 changeset 功能新增 user
def new(conn, _params) do
changeset = User.changeset(%User{})
render conn, "new.html", changeset: changeset
end
end
修改 /web/models/user.ex
defmodule Rumbl.User do
use Rumbl.Web, :model
schema "users" do
field :name, :string
field :username, :string
field :password, :string, virtual: true
field :password_hash, :string
timestamps()
end
# 參數: a User struct and parameters
# User struct 傳入 cast,限制 name 及 username 都是必要欄位
# 並將 requied & optional values 轉成 schema types
# 接下來傳入 validate_length,檢查 username 資料長度
# 如果沒有任何 parameters,必須傳入 empty map %{}
def changeset(model, params \\ :empty) do
model
|> cast(params, ~w(name username), [])
|> validate_length(:username, min: 1, max: 20)
end
end
注意 User.changeset 的部分,他會接收一個 struct 及 controller 的參數,回傳 Ecto.Changeset。
Changeset 可讓 Ecto 管理 record changes, cast parameters, perform validations。可使用 changeset 製作 customized strategy 處理各種資料修改的功能,例如新增使用者或更新資訊。
注意: 在舊版 Ecto 要用 :empty 當作 empty map,但在 Ecto 2.0 後,要改寫為 %{}
ref: Empty atom in Ecto changeset
接下來修改 /web/router.ex 的 scope
scope "/", Rumbl do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/users", UserController, only: [:index, :show, :new, :create]
end
resources 是一個簡化的一組 REST action implementations,包含 create, read, update, delete
resources "/users", UserController
等同以下這些網址的定義
get "/users", UserController, :index
get "/users/new", UserController, :new
get "/users/:id", UserController, :show
get "/users/:id/edit", UserController, :edit
post "/users", UserController, :create
patch "/users/:id", UserController, :update
put "/users/:id", UserController, :update
delete "/users/:id", UserController, :delete
剛剛的定義用 only: 限制只使用某幾個 route 定義
resources "/users", UserController, only: [:index, :show, :new, :create]
可在 console 直接用這個指令查看整個 application 的 routes
$ mix phoenix.routes
page_path GET / Rumbl.PageController :index
user_path GET /users Rumbl.UserController :index
user_path GET /users/new Rumbl.UserController :new
user_path GET /users/:id Rumbl.UserController :show
user_path POST /users Rumbl.UserController :create
新增網頁
<h1>New User</h1>
<%= form_for @changeset, user_path(@conn, :create), fn f -> %>
<div class="form-group">
<%= text_input f, :name, placeholder: "Name", class: "form-control" %>
</div>
<div class="form-group">
<%= text_input f, :username, placeholder: "Username", class: "form-control" %>
</div>
<div class="form-group">
<%= password_input f, :password, placeholder: "Password", class: "form-control" %>
</div>
<%= submit "Create User", class: "btn btn-primary" %>
<% end %>
http://localhost:4000/users/new
<h1>New User</h1>
<form accept-charset="UTF-8" action="/users" method="post"><input name="_csrf_token" type="hidden" value="VQAbE0ACaEdYJglWOgIbMioeICosAAAA60MX/k8+nSByckvzFyogeg=="><input name="_utf8" type="hidden" value="✓"><div class="form-group">
<input class="form-control" id="user_name" name="user[name]" placeholder="Name" type="text"></div>
<div class="form-group">
<input class="form-control" id="user_username" name="user[username]" placeholder="Username" type="text"></div>
<div class="form-group">
<input class="form-control" id="user_password" name="user[password]" placeholder="Password" type="password"></div>
<button class="btn btn-primary" type="submit">Create User</button></form>
另外有個 protocol: Phoenix.HTML.FormData,可分離畫面以及後端實作,Ecto.Changeset 也實作了這個 Protocol,並把 Table 的 data strcuture 直接轉換到 Phoenix form。
Creating Resources
def create(conn, %{"user" => user_params}) do
changeset = User.changeset(%User{}, user_params)
# Repo.insert 的結果有兩種,正確時,會 redirect 到 users index 首頁
# 錯誤時,會停留在 new.html 的畫面
case Repo.insert(changeset) do
{:ok, user} ->
conn
|> put_flash(:info, "#{user.name} created!")
|> redirect(to: user_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
修改 /web/templates/user/new.html.eex ,加上 error message
<h1>New User</h1>
<%= form_for @changeset, user_path(@conn, :create), fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="form-group">
<%= text_input f, :name, placeholder: "Name", class: "form-control" %>
<%= error_tag f, :name %>
</div>
<div class="form-group">
<%= text_input f, :username, placeholder: "Username", class: "form-control" %>
<%= error_tag f, :username %>
</div>
<div class="form-group">
<%= password_input f, :password, placeholder: "Password", class: "form-control" %>
<%= error_tag f, :password %>
</div>
<%= submit "Create User", class: "btn btn-primary" %>
<% end %>
error_tag helper 定義在 web/views/error_helpers.ex
可顯示錯誤訊息。
$ iex -S mix
iex(1)> changeset = Rumbl.User.changeset(%Rumbl.User{username: "eric"})
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Rumbl.User<>,
valid?: true>
iex(2)> import Ecto.Changeset
Ecto.Changeset
iex(3)> changeset = put_change(changeset, :username, "eric")
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Rumbl.User<>,
valid?: true>
iex(4)> changeset.changes
%{}
iex(5)> changeset = put_change(changeset, :username, "ericname")
#Ecto.Changeset<action: nil, changes: %{username: "ericname"}, errors: [],
data: #Rumbl.User<>, valid?: true>
iex(6)> changeset.changes
%{username: "ericname"}
iex(7)> get_change(changeset, :username)
"ericname"
Ecto 使用 changesets 儲存 persistence 前後異動的資料。