上下文模式 #
一、Context概述 #
1.1 什么是Context #
Context是Phoenix中组织业务逻辑的设计模式,它将相关的功能组织在一起,提供清晰的API边界。
1.2 Context结构 #
text
lib/hello/
├── accounts/ # Accounts Context
│ ├── accounts.ex # 公共API
│ ├── user.ex # User Schema
│ └── user_token.ex # UserToken Schema
├── blog/ # Blog Context
│ ├── blog.ex # 公共API
│ ├── post.ex # Post Schema
│ └── comment.ex # Comment Schema
└── shop/ # Shop Context
├── shop.ex # 公共API
├── product.ex # Product Schema
└── order.ex # Order Schema
二、定义Context #
2.1 Context模块 #
elixir
defmodule Hello.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.Query, warn: false
alias Hello.Repo
alias Hello.Accounts.User
def list_users, do: Repo.all(User)
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
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
def delete_user(%User{} = user) do
Repo.delete(user)
end
def change_user(%User{} = user, attrs \\ %{}) do
User.changeset(user, attrs)
end
end
2.2 Schema模块 #
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])
|> unique_constraint(:email)
end
def registration_changeset(user, attrs) do
user
|> changeset(attrs)
|> cast(attrs, [:password])
|> validate_required([:password])
|> 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))
end
end
end
三、Context设计原则 #
3.1 单一职责 #
elixir
defmodule Hello.Accounts do
alias Hello.Accounts.User
alias Hello.Accounts.UserToken
def list_users, do: Repo.all(User)
def get_user!(id), do: Repo.get!(User, id)
def create_user(attrs), do: ...
def update_user(user, attrs), do: ...
def delete_user(user), do: ...
def generate_user_token(user), do: ...
def verify_user_token(token), do: ...
end
3.2 清晰的API边界 #
elixir
defmodule Hello.Blog do
alias Hello.Blog.Post
alias Hello.Blog.Comment
def list_posts(opts \\ []), do: ...
def get_post!(id), do: ...
def create_post(attrs), do: ...
def update_post(post, attrs), do: ...
def delete_post(post), do: ...
def list_comments(post_id), do: ...
def create_comment(post_id, attrs), do: ...
def delete_comment(comment), do: ...
end
3.3 避免跨Context直接访问 #
elixir
defmodule Hello.Blog do
alias Hello.Accounts
def create_post(user_id, attrs) do
user = Accounts.get_user!(user_id)
%Post{author_id: user.id}
|> Post.changeset(attrs)
|> Repo.insert()
end
end
四、Context关联 #
4.1 跨Context数据访问 #
elixir
defmodule Hello.Blog do
alias Hello.Accounts
def list_posts_by_user(user_id) do
from(p in Post, where: p.author_id == ^user_id)
|> Repo.all()
end
def get_post_with_author!(id) do
post = Repo.get!(Post, id)
author = Accounts.get_user!(post.author_id)
%{post | author: author}
end
end
4.2 跨Context事务 #
elixir
defmodule Hello.Shop do
alias Hello.Accounts
alias Hello.Shop.Order
def create_order(user_id, attrs) do
Ecto.Multi.new()
|> Ecto.Multi.run(:user, fn _repo, _changes ->
case Accounts.get_user(user_id) do
nil -> {:error, :user_not_found}
user -> {:ok, user}
end
end)
|> Ecto.Multi.insert(:order, fn %{user: user} ->
Order.changeset(%Order{user_id: user.id}, attrs)
end)
|> Repo.transaction()
end
end
五、Context测试 #
5.1 测试Context #
elixir
defmodule Hello.AccountsTest do
use Hello.DataCase
alias Hello.Accounts
describe "users" do
alias Hello.Accounts.User
test "list_users/0 returns all users" do
user = user_fixture()
assert Accounts.list_users() == [user]
end
test "get_user!/1 returns the user with given id" do
user = user_fixture()
assert Accounts.get_user!(user.id) == user
end
test "create_user/1 with valid data creates a user" do
attrs = %{email: "test@example.com", name: "Test User"}
assert {:ok, %User{} = user} = Accounts.create_user(attrs)
assert user.email == "test@example.com"
assert user.name == "Test User"
end
test "create_user/1 with invalid data returns error changeset" do
attrs = %{email: nil, name: nil}
assert {:error, %Ecto.Changeset{}} = Accounts.create_user(attrs)
end
end
defp user_fixture(attrs \\ %{}) do
attrs = Enum.into(attrs, %{email: "test@example.com", name: "Test User"})
{:ok, user} = Accounts.create_user(attrs)
user
end
end
5.2 测试辅助 #
elixir
defmodule Hello.DataCase do
use ExUnit.CaseTemplate
using do
quote do
alias Hello.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Hello.DataCase
end
end
setup tags do
Hello.DataCase.setup_sandbox(tags)
:ok
end
def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Hello.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
end
六、Context生成器 #
6.1 使用生成器 #
bash
mix phx.gen.context Accounts User users email:string name:string
mix phx.gen.context Blog Post posts title:string body:text
mix phx.gen.context Shop Product products name:string price:decimal
6.2 生成器输出 #
text
* creating lib/hello/accounts/user.ex
* creating priv/repo/migrations/20240101000000_create_users.exs
* creating lib/hello/accounts.ex
* creating test/hello/accounts_test.exs
七、Context最佳实践 #
7.1 命名约定 #
elixir
defmodule Hello.Accounts do
def list_users, do: ...
def get_user!(id), do: ...
def get_user(id), do: ...
def create_user(attrs), do: ...
def update_user(user, attrs), do: ...
def delete_user(user), do: ...
def change_user(user, attrs), do: ...
end
7.2 文档注释 #
elixir
defmodule Hello.Accounts do
@moduledoc """
The Accounts context handles user management.
## Examples
iex> Accounts.create_user(%{email: "test@example.com"})
{:ok, %User{}}
iex> Accounts.create_user(%{email: nil})
{:error, %Ecto.Changeset{}}
"""
@doc """
Creates a new user.
## Examples
iex> create_user(%{email: "test@example.com", name: "Test"})
{:ok, %User{}}
iex> create_user(%{email: nil})
{:error, %Ecto.Changeset{}}
"""
def create_user(attrs), do: ...
end
7.3 错误处理 #
elixir
defmodule Hello.Accounts do
def authenticate_user(email, password) do
with %User{} = user <- get_user_by_email(email),
true <- verify_password(user, password) do
{:ok, user}
else
nil -> {:error, :not_found}
false -> {:error, :invalid_password}
end
end
end
八、总结 #
8.1 核心概念 #
| 概念 | 说明 |
|---|---|
| Context | 业务逻辑模块 |
| Schema | 数据模型 |
| 公共API | Context暴露的函数 |
| 私有函数 | 内部实现 |
8.2 设计原则 #
- 单一职责
- 清晰的API边界
- 避免跨Context直接访问
- 使用Ecto.Multi处理跨Context事务
8.3 下一步 #
现在你已经了解了上下文模式,接下来让我们学习 认证系统,深入了解用户认证!
最后更新:2026-03-28