ecto 簡介 ecto 相當於 elixir 的 ORM,但是得益於 elixir 語言,和傳統的 ORM 相比,更加簡潔和強大。 ecto 主要分為 4 部分: 1. Repo: 這是和真正資料庫交互的部分 2. Schema: 相當於是資料庫中表的定義,但不僅僅是定義 3. Changeset ...
ecto 簡介
ecto 相當於 elixir 的 ORM,但是得益於 elixir 語言,和傳統的 ORM 相比,更加簡潔和強大。
ecto 主要分為 4 部分:
- Repo: 這是和真正資料庫交互的部分
- Schema: 相當於是資料庫中表的定義,但不僅僅是定義
- Changeset:Schema 到真正資料庫之間的轉換層
- Query:elixir 風格的數據查詢方式
ecto 連接資料庫(以 sqlite 為例)
連接到真實的資料庫,需要相應的驅動,下麵以 sqlite 為例,配置資料庫的連接
創建工程
也就是一般的 elixir 工程
mix new ecto_sample
引入 ecto 和資料庫驅動
引入 ecto 和 sqlite 相關的 packages (mix.exs)
defp deps do
[
{:sqlite_ecto2, "~> 2.2"},
{:ecto, "~> 2.1"}
]
end
配置好後通過命令行安裝:
mix deps.get
創建資料庫
在 lib/ecto_sample 下創建文件 repo.ex,文件內容如下:
defmodule EctoSample.Repo do
use Ecto.Repo,
otp_app: :ecto_sample,
adapter: Sqlite.Ecto2
end
配置資料庫連接,config/config.esx
config :ecto_sample, ecto_repos: [EctoSample.Repo] # 配置 repo
config :ecto_sample, EctoSample.Repo, # 配置驅動和資料庫位置,這裡用的 sqlite 數據,比較簡單
adapter: Sqlite.Ecto2,
database: "ecto_sample.db"
sqlite 資料庫只要配置資料庫文件的位置即可,如果是 postgres 之類的關係資料庫,需要配置主機,用戶名/密碼 等
config :ecto_sample, EctoSample.Repo,
adapter: Ecto.Adapters.Postgres,
database: "ecto_sample_repo",
username: "user",
password: "pass",
hostname: "localhost"
配置完成後,創建資料庫
mix ecto.create
成功執行的話,能看到在工程根目錄下多了個 ecto_sample.db 文件
創建表
創建 schema users, lib/repo.ex 文件中追加
其中 changeset 是在更新數據時用來驗證數據有效性或者轉換數據用的,不是必須的
defmodule EctoSample.User do
use Ecto.Schema
schema "users" do
field :username, :string
field :password, :string
field :email, :string
field :age, :integer
end
def changeset(user, params \\ %{}) do
user
|> cast(params, [:username, :password, :email, :age])
|> validate_required([:username, :password])
end
end
創建建表的 migration
$ mix ecto.gen.migration create_user
Compiling 2 files (.ex)
Generated ecto_sample app
* creating priv/repo/migrations
* creating priv/repo/migrations/20171123012930_create_user.exs
參照 users 的 schema 編輯 priv/repo/migrations/20171123012930_create_user.exs
defmodule EctoSample.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :username, :string
add :password, :string
add :email, :string
add :age, :integer
end
end
end
創建表
$ mix ecto.migrate
09:33:40.257 [info] == Running EctoSample.Repo.Migrations.CreateUser.change/0 forward
09:33:40.257 [info] create table users
09:33:40.259 [info] == Migrated in 0.0s
登入資料庫驗證
用 sqlite3 的客戶端登入資料庫查看情況,下麵使用的是命令行方式
$ sqlite3 ecto_sample.db
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> .fullschema
CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" BIGINT PRIMARY KEY, "inserted_at" NAIVE_DATETIME);
CREATE TABLE IF NOT EXISTS "users" ("id" INTEGER PRIMARY KEY, "username" TEXT, "password" TEXT, "email" TEXT, "age" INTEGER);
/* No STAT tables available */
sqlite> .exit
可以看出:
- 除了創建了 users 表,ecto 還創建了 schema_migrations 用來管理每次的 migration
- 預設創建了 id 主鍵,類型是 INTEGER,如果要改成 uuid 創建時要明確指定
ecto 單表操作
演示示例
通過一個例子來演示對 users 表的 CURD
示例流程: 增加一個記錄 -> 查詢這條記錄 -> 修改這條記錄 -> 查詢新的記錄 -> 刪除這條記錄 -> 再次查詢為空
新增記錄
def add_user(username, password, email \\ "", age \\ 0) do user = EctoSample.User.changeset(%EctoSample.User{}, %{:username => username, :password => password, :email => email, :age => age}) case EctoSample.Repo.insert(user) do {:ok, _} -> Logger.info "insert successfully" {:error, _} -> Logger.error "insert failed" end end
查詢記錄
import Ecto.Query, only: [from: 2] q = from u in EctoSample.User, where: u.username == ^username EctoSample.Repo.all(q) |> Enum.map(fn (u) -> Logger.info "===============================" Logger.info "username: " <> u.username Logger.info "password: " <> u.password if u.email do Logger.info "email: " <> u.email end Logger.info "age: " <> Integer.to_string u.age Logger.info "===============================" end)
修改記錄
def change_user(id, params \\ %{}) do u = EctoSample.Repo.get!(EctoSample.User, id) changeset = EctoSample.User.changeset(u, params) EctoSample.Repo.update(changeset) end
刪除記錄
def delete_user(id) do u = EctoSample.Repo.get!(EctoSample.User, id) EctoSample.Repo.delete(u) |> case do {:ok, _} -> Logger.info "delete successfully" {:error, _} -> Logger.error "delete failed" end end
測試步驟
$ iex -S mix
Erlang/OTP 20 [erts-9.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.5.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> EctoSample.start
{:ok, #PID<0.182.0>}
iex(2)> EctoSample.add_user("a", "b", "c", 10)
22:45:22.570 [info] insert successfully
iex(3)> EctoSample.query_user("a")
22:45:29.370 [info] ===============================
[:ok]
22:45:29.370 [info] username: a
22:45:29.370 [info] password: b
22:45:29.370 [info] email: c
22:45:29.370 [info] age: 10
22:45:29.370 [info] ===============================
iex(4)> EctoSample.change_user(1, %{:username => "change", :age => 20})
{:ok,
%EctoSample.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, age: 20,
email: "c", id: 1, password: "b", username: "change"}}
iex(5)> EctoSample.query_user("change")
22:48:47.644 [info] ===============================
22:48:47.644 [info] username: change
22:48:47.644 [info] password: b
22:48:47.644 [info] email: c
22:48:47.644 [info] age: 20
22:48:47.644 [info] ===============================
iex(6)> EctoSample.delete_user(1)
22:50:17.848 [info] delete successfully
iex(7)> EctoSample.query_user("change")
[]
ecto 表關係操作
表關係只有 3 種,1:1,1:N,M:N。 先在現有的表基礎上增加 3 張表
增加和 User 關聯的表
table 定義
defmodule EctoSample.Schema.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :username, :string field :password, :string field :email, :string field :age, :integer has_one :computer, EctoSample.Schema.Computer belongs_to :company, EctoSample.Schema.Company many_to_many :friends, EctoSample.Schema.Friend, join_through: "users_friends" end def changeset(user, params \\ %{}) do user |> cast(params, [:username, :password, :email, :age]) |> validate_required([:username, :password]) end end # 1 : 1 computer - user defmodule EctoSample.Schema.Computer do use Ecto.Schema import Ecto.Changeset schema "computers" do field :hostname, :string field :ip, :string belongs_to :user, EctoSample.Schema.User end def changeset(computer, params \\ %{}) do computer |> cast(params, [:hostname, :ip]) |> validate_required([:hostname, :ip]) end end # 1 : N company - user defmodule EctoSample.Schema.Company do use Ecto.Schema import Ecto.Changeset schema "companys" do field :comp_name, :string field :addr, :string has_many :users, EctoSample.Schema.User end def changeset(company, params \\ %{}) do company |> cast(params, [:comp_name, :addr]) |> validate_required([:comp_name, :addr]) end end # M : N friend - user defmodule EctoSample.Schema.Friend do use Ecto.Schema import Ecto.Changeset schema "friends" do field :frient_name, :string field :phone, :string many_to_many :users, EctoSample.Schema.User, join_through: "users_friends" end def changeset(friend, params \\ %{}) do friend |> cast(params, [:friend_name, :phone]) |> validate_required([:friend_name, :phone]) end end
原先的 user 表做了一些修改,增加了一些關聯屬性,另外增加了 3 張表,和 user 表的關係分別是:
- 1:1 user : computer
- 1:N company : user
- M:N friend : user
table migration
創建各個表的 migrationmix ecto.gen.migration create_company mix ecto.gen.migration create_computer mix ecto.gen.migration create_friend mix ecto.gen.migration create_users_friends
migration 的代碼參見:https://gitee.com/wangyubin/ecto_sample.git 整個示例工程的代碼都在其中
1:1 示例
def one_to_one() do
import Ecto.Changeset
alias EctoSample.Schema.User
alias EctoSample.Schema.Computer
# insert
computer = %Computer{}
|> Computer.changeset(%{:hostname => "debian", :ip => "192.168.0.100"})
|> EctoSample.Repo.insert!
user = %User{}
|> User.changeset(%{:username => "wyb", :password => "123"})
|> put_assoc(:computer, computer)
|> EctoSample.Repo.insert!
# query
u = EctoSample.Repo.get!(User, user.id) |> EctoSample.Repo.preload(:computer)
Logger.info "==============================="
Logger.info "id: " <> Integer.to_string(u.id)
Logger.info "username: " <> u.username
Logger.info "password: " <> u.password
Logger.info "computer: *********"
Logger.info "hostname: " <> u.computer.hostname
Logger.info "ip: " <> u.computer.ip
Logger.info "==============================="
end
1:N 示例
def one_to_many() do
alias EctoSample.Schema.User
alias EctoSample.Schema.Company
# insert
user1 = EctoSample.Repo.insert!(%User{:username => "wyb001", :password => "123"})
user2 = EctoSample.Repo.insert!(%User{:username => "wyb002", :password => "321"})
company = EctoSample.Repo.insert!(%Company{:comp_name => "yunbim", :addr => "D216", :users => [user1, user2]})
# TODO 這裡是根據 user 來新建 company,也可以 根據已有的 company 來創建 user
# query
c = EctoSample.Repo.get!(Company, company.id) |> EctoSample.Repo.preload(:users)
Logger.info "==============================="
Logger.info "id: " <> Integer.to_string(c.id)
Logger.info "comp_name:" <> c.comp_name
Logger.info "addr : " <> c.addr
Logger.info "users: *********"
c.users |> Enum.map(fn (u) ->
Logger.info "id: " <> Integer.to_string(u.id)
Logger.info "username: " <> u.username
Logger.info "password: " <> u.password
end)
Logger.info "==============================="
end
M:N 示例
def many_to_many() do
alias EctoSample.Schema.User
alias EctoSample.Schema.Friend
import Ecto.Changeset
# insert
user1 = EctoSample.Repo.insert!(%User{:username => "wyb001", :password => "123"})
user2 = EctoSample.Repo.insert!(%User{:username => "wyb002", :password => "321"})
friend1 = EctoSample.Repo.insert!(%Friend{:friend_name => "f001", :phone => "123456789"})
friend2 = EctoSample.Repo.insert!(%Friend{:friend_name => "f002", :phone => "987654321"})
EctoSample.Repo.get!(User, user1.id)
|> EctoSample.Repo.preload(:friends)
|> change
|> put_assoc(:friends, [friend1, friend2])
|> EctoSample.Repo.update!()
EctoSample.Repo.get!(User, user2.id)
|> EctoSample.Repo.preload(:friends)
|> change
|> put_assoc(:friends, [friend1])
|> EctoSample.Repo.update!()
# query
f1 = EctoSample.Repo.get!(Friend, friend1.id) |> EctoSample.Repo.preload(:users)
Logger.info "==============================="
Logger.info "id: " <> Integer.to_string(f1.id)
Logger.info "friend_name:" <> f1.friend_name
Logger.info "phone: " <> f1.phone
Logger.info "users: *********"
f1.users |> Enum.map(fn (u) ->
Logger.info "id: " <> Integer.to_string(u.id)
Logger.info "username: " <> u.username
Logger.info "password: " <> u.password
end)
Logger.info "==============================="
f2 = EctoSample.Repo.get!(Friend, friend2.id) |> EctoSample.Repo.preload(:users)
Logger.info "==============================="
Logger.info "id: " <> Integer.to_string(f2.id)
Logger.info "friend_name:" <> f2.friend_name
Logger.info "phone: " <> f2.phone
Logger.info "users: *********"
f2.users |> Enum.map(fn (u) ->
Logger.info "id: " <> Integer.to_string(u.id)
Logger.info "username: " <> u.username
Logger.info "password: " <> u.password
end)
Logger.info "==============================="
end
運行測試
$ mix ecto.drop
$ mix ecto.create
$ mix ecto.migrate
$ iex -S mix
Erlang/OTP 20 [erts-9.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.5.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> EctoSample.start
{:ok, #PID<0.182.0>}
iex(2)> EctoSample.one_to_one
11:00:27.800 [info] ===============================
11:00:27.800 [info] id: 1
11:00:27.800 [info] username: wyb
11:00:27.800 [info] password: 123
11:00:27.800 [info] computer: *********
11:00:27.800 [info] hostname: debian
11:00:27.800 [info] ip: 192.168.0.100
11:00:27.800 [info] ===============================
iex(3)> EctoSample.one_to_many
11:01:32.327 [info] ===============================
11:01:32.327 [info] id: 1
11:01:32.327 [info] comp_name:yunbim
11:01:32.327 [info] addr : D216
11:01:32.327 [info] users: *********
11:01:32.327 [info] id: 2
11:01:32.327 [info] username: wyb001
11:01:32.327 [info] password: 123
11:01:32.327 [info] id: 3
11:01:32.327 [info] username: wyb002
11:01:32.327 [info] password: 321
11:01:32.327 [info] ===============================
iex(4)> EctoSample.many_to_many
11:02:22.086 [info] ===============================
11:02:22.086 [info] id: 1
11:02:22.086 [info] friend_name:f001
11:02:22.086 [info] phone: 123456789
11:02:22.086 [info] users: *********
11:02:22.086 [info] id: 4
11:02:22.086 [info] username: wyb001
11:02:22.086 [info] password: 123
11:02:22.086 [info] id: 5
11:02:22.086 [info] username: wyb002
11:02:22.086 [info] password: 321
11:02:22.086 [info] ===============================
11:02:22.087 [info] ===============================
11:02:22.087 [info] id: 2
11:02:22.087 [info] friend_name:f002
11:02:22.087 [info] phone: 987654321
11:02:22.087 [info] users: *********
11:02:22.087 [info] id: 4
11:02:22.087 [info] username: wyb001
11:02:22.087 [info] password: 123
11:02:22.087 [info] ===============================
ecto 中的事務
ecto 中的事務,首先通過 Multi 來組裝需要進行的資料庫操作,然後通過 Repo.transaction 來執行
def trans() do
alias EctoSample.Schema.User
alias EctoSample.Schema.Computer
import Ecto.Query, only: [from: 2]
alias Ecto.Multi
# insert user and computer in one transaction, insert all success
Logger.info "=========== before transaction==============="
EctoSample.Repo.one(from u in User, select: count(u.id)) |> Logger.info
EctoSample.Repo.one(from c in Computer, select: count(c.id)) |> Logger.info
Multi.new()
|> Multi.insert(:user, %User{username: "m-user", password: "m-password"})
|> Multi.insert(:computer, %Computer{hostname: "host-name", ip: "0.0.0.0"})
|> EctoSample.Repo.transaction
|> case do
{:ok, _} -> Logger.info "multi success"
{:error, _} -> Logger.error "multi error"
end
Logger.info "=========== after transaction==============="
EctoSample.Repo.one(from u in User, select: count(u.id)) |> Logger.info
EctoSample.Repo.one(from c in Computer, select: count(c.id)) |> Logger.info
end
插入成功之後,User 和 Computer 表的數據都會增加
其他
除了上述內容之外,Ecto 還有其他的 API 輔助查詢和各種數據操作,具體參見 Ecto 文檔