在我学习 Elixir 的过程中,我发现了 Ecto ,这是一个强大的数据库封装器和查询生成器,能够无缝对接 SQL 数据库。尽管常与 Entity Framework Core 或 Ruby on Rails 的 ActiveRecord 等 ORM 框架对比,但 Ecto 的设计不同:它不自动跟踪状态,要求开发者显式管理数据变化。本文将探讨 Ecto 的核心概念,包括仓库(Repository)、模式(Schema)、迁移(Migration)和基础增删改查(CRUD)操作。高级功能请参考 Ecto 官方文档 。
Ecto 是 Elixir 的数据库封装器和查询生成器,专为 PostgreSQL、MySQL 等关系型数据库设计。它提供以下功能:
Ecto 通过以下方式避免传统 ORM 的陷阱:
mix.exs
中的依赖:{:ecto_sql, "~> 3.0"}, # Ecto
{:postgrex, ">= 0.0.0"} # PostgreSQL驱动
仓库(或 Repo)是 Ecto 与数据库交互的接口,类似于 Entity Framework 的 DbContext
。
1. 定义仓库模块:
defmodule Friends.Repo do
use Ecto.Repo,
otp_app: :friend,
adapter: Ecto.Adapters.Postgres
end
2. 在 config/config.exs
中配置:
config :friends, Friends.Repo,
database: "friends",
username: "user",
password: "pass",
hostname: "localhost"
运行:
mix ecto.gen.repo -r Friends.Repo
在 lib/
的应用监督者中添加仓库:
def start(_type, _args) do
children = [Friends.Repo]
...
end
然后更新 config/config.exs
:
config :friends, ecto_repos: [Friends.Repo]
模式将数据库表映射为 Elixir 结构体。例如,定义一个 Person
模式:
defmodule Friends.Person do
use Ecto.Schema
import Ecto.Changeset
schema "people" do
field :first_name, :string
field :last_name, :string
field :age, :integer
end
def changeset(person, params \\ %{}) do
person
|> cast(params, [:first_name, :last_name, :age])
|> validate_required([:first_name, :last_name])
end
end
迁移以增量方式定义数据库模式变更。
在 priv/repo/migrations/
创建文件:
defmodule Friends.Repo.Migrations.CreatePeople do
use Ecto.Migration
def change do
create table(:people) do
add :first_name, :string
add :last_name, :string
add :age, :integer
end
end
end
运行:
mix ecto.gen.migration create_people
该命令会创建一个空迁移文件:
defmodule Friends.Repo.Migrations.CreatePeople do
use Ecto.Migration
def change do
end
end
运行:
mix ecto.create # 创建数据库
mix ecto.migrate # 执行迁移
插入新记录:
person = %Friends.Person{first_name: "Alice", last_name: "Smith", age: 30}
{:ok, inserted_person} = Friends.Repo.insert(person)
带验证的插入:
changeset = Friends.Person.changeset(%Friends.Person{}, %{first_name: "Alice"})
case Friends.Repo.insert(changeset) do
{:ok, person} -> # 成功
{:error, changeset} -> # 处理错误
end
获取记录:
# 通过 ID 获取
Friends.Repo.get(Friends.Person, 1)
# 获取第一条记录
Friends.Repo.one(from p in Friends.Person, order_by: [asc: p.id], limit: 1)
# 获取符合条件的所有记录
Friends.Repo.all(from p in Friends.Person, where: like(p.first_name, "A%"))
更新现有记录:
person = Friends.Repo.get!(Friends.Person, 1)
changeset = Friends.Person.changeset(person, %{age: 31})
Friends.Repo.update(changeset)
删除记录:
person = Friends.Repo.get!(Friends.Person, 1)
Friends.Repo.delete(person)
Ecto 在抽象与控制之间取得平衡,提供以下优势:
尽管不是传统 ORM,Ecto 的函数式设计避免了常见的对象-关系映射抽象漏洞。对于高级模式(如关联关系),可探索其对 has_many
、belongs_to
和 many_to_many
的支持。