在Elixir的Phoenix框架开发中,数据库操作是核心需求之一,而SQL语言作为关系型数据库的标准操作语言,和Elixir的交互主要通过Ecto库实现。Ecto提供了一套领域特定语言,让开发者可以用Elixir的语法编写数据库操作逻辑,无需直接拼接原生SQL,同时也支持在需要时执行原生SQL语句。

Ecto的核心组件
Ecto主要包含四个核心模块,分别承担不同的职责:
- Repo:数据库仓库,负责和数据库建立连接,执行所有的数据库操作命令,是Ecto和数据库交互的入口。
- Schema:用于映射数据库表结构,定义表的字段、类型以及字段的校验规则,是Elixir代码和数据库表的对应桥梁。
- Changeset:用于处理数据变更,包含数据校验、脏数据过滤、关联数据处理等逻辑,保证写入数据库的数据符合预期。
- Query:用于构建查询语句,支持用Elixir的管道语法组合查询条件,最终会被Ecto转换成对应的SQL语句执行。
基础CRUD操作的Ecto实现
下面通过一个用户表的例子,展示如何通过Ecto完成基础的增删改查操作,这些操作会被Ecto转换成对应的SQL语句执行。
定义Schema
首先定义用户表的Schema,映射数据库中的users表:
# lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
timestamps()
end
# 定义数据变更校验规则
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :age])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> validate_number(:age, greater_than_or_equal_to: 0)
|> unique_constraint(:email)
end
end配置Repo
在config/dev.exs中配置数据库连接信息,Repo会读取这些配置建立数据库连接:
# config/dev.exs config :my_app, MyApp.Repo, adapter: Ecto.Adapters.Postgres, database: "my_app_dev", username: "postgres", password: "postgres", hostname: "localhost", port: 5432
执行CRUD操作
通过Repo执行对应的操作,Ecto会自动生成SQL语句:
# 插入数据
alias MyApp.Accounts.User
alias MyApp.Repo
# 创建用户,Ecto会生成INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING id, name, email, age, inserted_at, updated_at的SQL
user_attrs = %{name: "张三", email: "zhangsan@ipipp.com", age: 25}
changeset = User.changeset(%User{}, user_attrs)
case Repo.insert(changeset) do
{:ok, user} -> IO.inspect(user, label: "插入成功")
{:error, changeset} -> IO.inspect(changeset.errors, label: "插入失败")
end
# 查询数据,Ecto会生成SELECT u0.id, u0.name, u0.email, u0.age, u0.inserted_at, u0.updated_at FROM users AS u0 WHERE (u0.email = $1)的SQL
user = Repo.get_by(User, email: "zhangsan@ipipp.com")
IO.inspect(user, label: "查询结果")
# 更新数据
changeset = User.changeset(user, %{age: 26})
case Repo.update(changeset) do
{:ok, user} -> IO.inspect(user, label: "更新成功")
{:error, changeset} -> IO.inspect(changeset.errors, label: "更新失败")
end
# 删除数据,Ecto会生成DELETE FROM users WHERE id = $1的SQL
Repo.delete(user)执行原生SQL语句
如果遇到Ecto的Query无法覆盖的复杂SQL场景,也可以直接通过Repo执行原生SQL语句:
# 执行查询类原生SQL,返回结果是列表,每个元素是元组
result = Repo.query!("SELECT id, name, email FROM users WHERE age > $1", [20])
Enum.each(result.rows, fn row ->
IO.inspect(row, label: "原生SQL查询结果")
end)
# 执行修改类原生SQL,比如批量更新
Repo.query!("UPDATE users SET age = age + 1 WHERE age < $1", [30])Ecto和原生SQL的选择建议
日常开发中,优先使用Ecto提供的CRUD和Query API,这样不仅能减少SQL注入风险,还能利用Ecto的校验、关联处理等能力。只有在遇到非常复杂的查询、数据库特有的函数调用等场景时,再考虑使用原生SQL,同时要注意原生SQL的参数化,避免直接拼接字符串导致安全问题。