Ecto简介 #

一、Ecto概述 #

1.1 什么是Ecto #

Ecto是Elixir的官方数据库抽象层和查询语言。它提供了:

  • Repository: 数据库连接和操作
  • Schema: 数据模型定义
  • Changeset: 数据验证和转换
  • Query: 类型安全的查询语言

1.2 Ecto架构 #

text
Ecto架构
├── Repository
│   ├── 数据库连接
│   ├── 事务管理
│   └── 基础CRUD
├── Schema
│   ├── 数据模型
│   ├── 字段定义
│   └── 关联关系
├── Changeset
│   ├── 数据验证
│   ├── 类型转换
│   └── 错误处理
└── Query
    ├── 查询构建
    ├── 条件过滤
    └── 关联预加载

二、Repository #

2.1 定义Repository #

elixir
defmodule Hello.Repo do
  use Ecto.Repo,
    otp_app: :hello,
    adapter: Ecto.Adapters.Postgres
end

2.2 配置连接 #

elixir
config :hello, Hello.Repo,
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  database: "hello_dev",
  pool_size: 10

2.3 启动Repository #

elixir
defmodule Hello.Application do
  use Application

  def start(_type, _args) do
    children = [
      Hello.Repo
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

三、基础操作 #

3.1 插入数据 #

elixir
def create_user(attrs \\ %{}) do
  %User{}
  |> User.changeset(attrs)
  |> Repo.insert()
end

3.2 查询数据 #

elixir
def list_users do
  Repo.all(User)
end

def get_user!(id), do: Repo.get!(User, id)

def get_user(id), do: Repo.get(User, id)

def get_user_by_email(email) do
  Repo.get_by(User, email: email)
end

3.3 更新数据 #

elixir
def update_user(%User{} = user, attrs) do
  user
  |> User.changeset(attrs)
  |> Repo.update()
end

3.4 删除数据 #

elixir
def delete_user(%User{} = user) do
  Repo.delete(user)
end

四、Schema #

4.1 定义Schema #

elixir
defmodule Hello.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
    field :name, :string
    field :age, :integer
    field :active, :boolean, default: true
    field :bio, :string

    timestamps()
  end
end

4.2 字段类型 #

类型 说明
:integer 整数
:float 浮点数
:boolean 布尔值
:string 字符串
:binary 二进制
:date 日期
:time 时间
:naive_datetime 日期时间(无时区)
:utc_datetime 日期时间(UTC)
:map 映射
数组

4.3 主键 #

elixir
schema "users" do
  field :id, :binary_id, primary_key: true
  field :email, :string
end

4.4 自定义主键 #

elixir
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id

schema "users" do
  field :email, :string
end

五、Changeset #

5.1 定义Changeset #

elixir
defmodule Hello.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string, virtual: true
    field :hashed_password, :string

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :name])
    |> validate_required([:email, :name])
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
  end

  def registration_changeset(user, attrs) do
    user
    |> changeset(attrs)
    |> cast(attrs, [:password])
    |> validate_required([:password])
    |> validate_length(:password, min: 8)
    |> put_hashed_password()
  end

  defp put_hashed_password(changeset) do
    case get_change(changeset, :password) do
      nil -> changeset
      password -> put_change(changeset, :hashed_password, hash_password(password))
    end
  end
end

5.2 cast函数 #

elixir
cast(struct, params, allowed_fields)
  • struct: Schema结构体
  • params: 参数映射
  • allowed_fields: 允许更新的字段列表

5.3 验证函数 #

elixir
changeset
|> validate_required([:email, :name])
|> validate_format(:email, ~r/@/)
|> validate_length(:name, min: 2, max: 100)
|> validate_number(:age, greater_than: 0)
|> validate_inclusion(:status, ["active", "inactive"])
|> validate_exclusion(:role, ["admin"])
|> unique_constraint(:email)
|> foreign_key_constraint(:post_id)

六、Query基础 #

6.1 基本查询 #

elixir
import Ecto.Query

def list_active_users do
  from(u in User, where: u.active == true)
  |> Repo.all()
end

def list_users_ordered do
  from(u in User, order_by: [desc: u.inserted_at])
  |> Repo.all()
end

6.2 条件查询 #

elixir
def search_users(name) do
  from(u in User, where: ilike(u.name, ^"%#{name}%"))
  |> Repo.all()
end

def get_users_by_age(min, max) do
  from(u in User, where: u.age >= ^min and u.age <= ^max)
  |> Repo.all()
end

6.3 选择字段 #

elixir
def list_user_names do
  from(u in User, select: u.name)
  |> Repo.all()
end

def list_user_summaries do
  from(u in User, select: %{id: u.id, name: u.name, email: u.email})
  |> Repo.all()
end

6.4 分页 #

elixir
def list_users_paginated(page, per_page) do
  offset = (page - 1) * per_page

  from(u in User,
    order_by: [desc: u.inserted_at],
    limit: ^per_page,
    offset: ^offset
  )
  |> Repo.all()
end

七、事务 #

7.1 基本事务 #

elixir
def transfer_money(from_id, to_id, amount) do
  Repo.transaction(fn ->
    from_account = Repo.get!(Account, from_id)
    to_account = Repo.get!(Account, to_id)

    {:ok, _} = update_balance(from_account, from_account.balance - amount)
    {:ok, _} = update_balance(to_account, to_account.balance + amount)
  end)
end

7.2 with事务 #

elixir
def create_user_with_profile(attrs) do
  Ecto.Multi.new()
  |> Ecto.Multi.insert(:user, User.changeset(%User{}, attrs))
  |> Ecto.Multi.insert(:profile, fn %{user: user} ->
    Profile.changeset(%Profile{user_id: user.id}, attrs)
  end)
  |> Repo.transaction()
end

7.3 Ecto.Multi #

elixir
def complex_operation(attrs) do
  Ecto.Multi.new()
  |> Ecto.Multi.insert(:user, User.changeset(%User{}, attrs))
  |> Ecto.Multi.run(:account, fn repo, %{user: user} ->
    create_account(repo, user)
  end)
  |> Ecto.Multi.update(:stats, fn %{user: user} ->
    update_user_stats(user)
  end)
  |> Repo.transaction()
  |> case do
    {:ok, result} -> {:ok, result.user}
    {:error, :user, changeset, _} -> {:error, changeset}
    {:error, :account, error, _} -> {:error, error}
  end
end

八、关联关系 #

8.1 一对多 #

elixir
defmodule Hello.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :body, :string
    has_many :comments, Hello.Blog.Comment
  end
end

defmodule Hello.Blog.Comment do
  use Ecto.Schema

  schema "comments" do
    field :body, :string
    belongs_to :post, Hello.Blog.Post
  end
end

8.2 多对多 #

elixir
defmodule Hello.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    many_to_many :tags, Hello.Blog.Tag, join_through: "posts_tags"
  end
end

defmodule Hello.Blog.Tag do
  use Ecto.Schema

  schema "tags" do
    field :name, :string
    many_to_many :posts, Hello.Blog.Post, join_through: "posts_tags"
  end
end

8.3 预加载 #

elixir
def list_posts_with_comments do
  from(p in Post, preload: [:comments])
  |> Repo.all()
end

def get_post_with_comments!(id) do
  Repo.get!(Post, id) |> Repo.preload(:comments)
end

九、聚合查询 #

9.1 计数 #

elixir
def count_users do
  from(u in User, select: count(u.id))
  |> Repo.one()
end

def count_active_users do
  from(u in User, where: u.active == true, select: count(u.id))
  |> Repo.one()
end

9.2 求和与平均 #

elixir
def average_age do
  from(u in User, select: avg(u.age))
  |> Repo.one()
end

def total_posts do
  from(p in Post, select: sum(p.view_count))
  |> Repo.one()
end

9.3 分组 #

elixir
def posts_count_by_author do
  from(p in Post,
    group_by: p.author_id,
    select: {p.author_id, count(p.id)}
  )
  |> Repo.all()
end

十、总结 #

10.1 核心概念 #

概念 说明
Repository 数据库连接和操作
Schema 数据模型定义
Changeset 数据验证和转换
Query 类型安全的查询

10.2 常用操作 #

操作 函数
插入 Repo.insert/1
查询 Repo.all/1, Repo.get/2
更新 Repo.update/1
删除 Repo.delete/1

10.3 下一步 #

现在你已经了解了Ecto基础,接下来让我们学习 Schema与迁移,深入了解数据模型定义!

最后更新:2026-03-28