名称: elixir-ecto-patterns 用户可调用: false 描述: 在需要使用Elixir Ecto模式时使用,包括模式、变化集、查询和事务。在构建数据库驱动的Elixir应用程序时使用。 允许工具:
- Bash
- Read
Elixir Ecto 模式
掌握Ecto,Elixir的数据库包装器和查询生成器。这个技能涵盖模式、变化集、查询、关联和事务,用于构建健壮的数据库应用程序。
模式定义
defmodule MyApp.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
field :is_active, :boolean, default: true
field :role, Ecto.Enum, values: [:user, :admin, :moderator]
has_many :posts, MyApp.Post
belongs_to :organization, MyApp.Organization
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :age, :is_active, :role])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> validate_number(:age, greater_than: 0, less_than: 150)
|> unique_constraint(:email)
end
end
变化集验证
defmodule MyApp.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
field :body, :text
field :published, :boolean, default: false
field :tags, {:array, :string}, default: []
belongs_to :user, MyApp.User
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body, :published, :tags, :user_id])
|> validate_required([:title, :body, :user_id])
|> validate_length(:title, min: 3, max: 100)
|> validate_length(:body, min: 10)
|> foreign_key_constraint(:user_id)
end
def publish_changeset(post) do
post
|> change(published: true)
end
end
基础查询
import Ecto.Query
# 获取所有用户
Repo.all(User)
# 通过ID获取用户
Repo.get(User, 1)
Repo.get!(User, 1) # 如果未找到则抛出异常
# 通过特定字段获取
Repo.get_by(User, email: "user@example.com")
# 使用where子句过滤
query = from u in User, where: u.age > 18
Repo.all(query)
# 选择特定字段
query = from u in User, select: {u.id, u.name}
Repo.all(query)
# 排序结果
query = from u in User, order_by: [desc: u.inserted_at]
Repo.all(query)
# 限制和偏移
query = from u in User, limit: 10, offset: 20
Repo.all(query)
复杂查询
# 结合多个条件
query =
from u in User,
where: u.is_active == true,
where: u.age >= 18,
order_by: [desc: u.inserted_at],
limit: 10
Repo.all(query)
# 使用管道语法
User
|> where([u], u.is_active == true)
|> where([u], u.age >= 18)
|> order_by([u], desc: u.inserted_at)
|> limit(10)
|> Repo.all()
# 动态查询
def filter_users(params) do
User
|> filter_by_name(params["name"])
|> filter_by_age(params["min_age"])
|> Repo.all()
end
defp filter_by_name(query, nil), do: query
defp filter_by_name(query, name) do
where(query, [u], ilike(u.name, ^"%#{name}%"))
end
defp filter_by_age(query, nil), do: query
defp filter_by_age(query, min_age) do
where(query, [u], u.age >= ^min_age)
end
关联和预加载
# 预加载关联
user = Repo.get(User, 1) |> Repo.preload(:posts)
# 预加载嵌套关联
user = Repo.get(User, 1) |> Repo.preload([posts: :comments])
# 查询时预加载
query = from u in User, preload: [:posts, :organization]
Repo.all(query)
# 自定义预加载查询
posts_query = from p in Post, where: p.published == true
query = from u in User, preload: [posts: ^posts_query]
Repo.all(query)
# 连接和预加载
query =
from u in User,
join: p in assoc(u, :posts),
where: p.published == true,
preload: [posts: p]
Repo.all(query)
聚合和分组
# 计数记录
Repo.aggregate(User, :count)
# 带条件计数
query = from u in User, where: u.is_active == true
Repo.aggregate(query, :count)
# 其他聚合
Repo.aggregate(User, :avg, :age)
Repo.aggregate(User, :sum, :age)
Repo.aggregate(User, :max, :age)
# 分组
query =
from u in User,
group_by: u.role,
select: {u.role, count(u.id)}
Repo.all(query)
# 分组带having子句
query =
from u in User,
group_by: u.role,
having: count(u.id) > 5,
select: {u.role, count(u.id)}
Repo.all(query)
插入和更新
# 使用变化集插入
attrs = %{name: "John", email: "john@example.com", age: 30}
%User{}
|> User.changeset(attrs)
|> Repo.insert()
# 不使用变化集插入
Repo.insert(%User{name: "Jane", email: "jane@example.com"})
# 更新
user = Repo.get(User, 1)
user
|> User.changeset(%{age: 31})
|> Repo.update()
# 更新所有
query = from u in User, where: u.is_active == false
Repo.update_all(query, set: [is_active: true])
# 删除
user = Repo.get(User, 1)
Repo.delete(user)
# 删除所有
query = from u in User, where: u.is_active == false
Repo.delete_all(query)
事务
# 基本事务
Repo.transaction(fn ->
user = Repo.insert!(%User{name: "Alice"})
Repo.insert!(%Post{title: "First post", user_id: user.id})
end)
# Multi用于复杂事务
alias Ecto.Multi
Multi.new()
|> Multi.insert(:user, User.changeset(%User{}, user_attrs))
|> Multi.insert(:post, fn %{user: user} ->
Post.changeset(%Post{}, Map.put(post_attrs, :user_id, user.id))
end)
|> Multi.run(:send_email, fn _repo, %{user: user} ->
send_welcome_email(user)
end)
|> Repo.transaction()
嵌入模式
defmodule MyApp.Address do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :street, :string
field :city, :string
field :state, :string
field :zip, :string
end
def changeset(address, attrs) do
address
|> cast(attrs, [:street, :city, :state, :zip])
|> validate_required([:city, :state])
end
end
defmodule MyApp.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
embeds_one :address, MyApp.Address
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name])
|> cast_embed(:address, required: true)
end
end
自定义Ecto类型
defmodule MyApp.Encrypted do
use Ecto.Type
def type, do: :binary
def cast(value) when is_binary(value), do: {:ok, value}
def cast(_), do: :error
def dump(value) when is_binary(value) do
{:ok, encrypt(value)}
end
def load(value) when is_binary(value) do
{:ok, decrypt(value)}
end
defp encrypt(value) do
# 加密逻辑
value
end
defp decrypt(value) do
# 解密逻辑
value
end
end
# 在模式中使用
schema "users" do
field :secret, MyApp.Encrypted
end
何时使用此技能
使用elixir-ecto-patterns当您需要:
- 构建数据库支持的Elixir应用程序
- 定义带有验证的模式和数据模型
- 使用Ecto的DSL编写复杂数据库查询
- 管理数据库关系和关联
- 使用变化集处理数据转换
- 实现事务以确保数据一致性
- 处理PostgreSQL、MySQL或其他数据库
- 构建带有数据库访问的Phoenix应用程序
- 创建健壮的数据验证层
最佳实践
- 始终使用变化集进行数据验证
- 预加载关联以避免N+1查询
- 使用事务处理多步数据库操作
- 利用Ecto.Multi处理复杂事务逻辑
- 保持模式聚焦,避免上帝对象
- 使用虚拟字段处理计算或临时数据
- 索引外键和频繁查询的字段
- 在需要时使用片段处理复杂SQL
- 编写可组合的查询函数
- 测试数据库约束和验证
常见陷阱
- 未预加载关联(N+1查询问题)
- 忘记验证必需字段
- 未使用事务处理相关操作
- 硬编码查询而非组合它们
- 忽略模式中的数据库约束
- 未正确处理变化集错误
- 过度使用嵌入模式处理关系数据
- 外键缺失索引
- 未使用Repo.transaction处理多步操作
- 在业务逻辑中暴露原始Ecto查询