ecto

georgeguimaraes2026-10-24查看 267复制 1550

使用 Ecto 设计和调试 Elixir 持久性。用于架构、变更集、查询、预加载、事务、迁移、多租户和通过应用程序上下文进行数据访问。涵盖数据库查询性能和连接池。

Download (.md)

📘如何使用这个技能

  1. 1
    复制技能内容·点击上方「Copy SKILL.md」复制全部内容,或 Download 下载到本地
  2. 2
    打开你的 AI 客户端·支持 Claude Code、Trae、Cursor、ChatGPT、DeepSeek、Kimi 等任意 AI 助手
  3. 3
    粘贴为系统提示/知识文件·Claude Code:放入 .claude/skills/ 目录;普通对话:作为首条消息发送并说「请严格按这份技能文档执行」
  4. 4
    直接提出你的需求·AI 会按技能定义的工作流、格式和约束来处理,无需再解释背景

💡 提示:技能生效后,每次对话只需一句需求,AI 会按 SKILL.md 的流程执行。

SKILL.md4652 chars
---
name: ecto
description: Design and debug Elixir persistence with Ecto. Use for schemas, changesets, queries, preloads, transactions, migrations, multi-tenancy, and data access through application contexts. Covers database query performance and connection pools.
---

# Ecto

Model persistence, compose queries, and keep data access within application boundaries.

## Context = Setting That Changes Meaning

Context isn't just a namespace—it changes what words mean. "Product" means different things in Checkout (SKU, name), Billing (SKU, cost), and Fulfillment (SKU, warehouse). Each bounded context may have its OWN Product schema/table.

**Think top-down:** Subdomain → Context → Entity. Not "What context does Product belong to?" but "What is a Product in this business domain?"

## Cross-Context References: IDs, Not Associations

```elixir
schema "cart_items" do
  field :product_id, :integer  # Reference by ID
  # NOT: belongs_to :product, Catalog.Product
end
```

Query through the context, not across associations. Keeps contexts independent and testable.

## DDD Patterns as Pipelines

```elixir
def create_product(params) do
  params
  |> Products.build()       # Factory: unstructured → domain
  |> Products.validate()    # Aggregate: enforce invariants
  |> Products.insert()      # Repository: persist
end
```

Use events (as data structs) to compose bounded contexts with minimal coupling.

## Schema ≠ Database Table

| Use Case | Approach |
|----------|----------|
| Database table | Standard `schema/2` |
| Form validation only | `embedded_schema/1` |
| API request/response | Embedded schema or schemaless |

## Multiple Changesets per Schema

```elixir
def registration_changeset(user, attrs)  # Full validation + password
def profile_changeset(user, attrs)       # Name, bio only
def admin_changeset(user, attrs)         # Role, verified_at
```

Different operations = different changesets.

## Multi-Tenancy: Composite Foreign Keys

```elixir
add :post_id, references(:posts, with: [org_id: :org_id], match: :full)
```

Use `prepare_query/3` for automatic scoping. Raise if `org_id` missing.

## Preload vs Join Trade-offs

| Approach | Best For |
|----------|----------|
| Separate preloads | Has-many with many records (less memory) |
| Join preloads | Belongs-to, has-one (single query) |

Join preloads can use 10x more memory for has-many.

## CRUD Contexts Are Fine

> "If you have a CRUD bounded context, go for it. No need to add complexity."

Use generators for simple cases. Add DDD patterns only when business logic demands it.

## Gotchas from Core Team

### CTE Queries Don't Inherit Schema Prefix

In multi-tenant apps, CTEs don't get the parent query's prefix.

**Fix:** Explicitly set prefix: `%{recursive_query | prefix: "tenant"}`

### Parameterized Queries ≠ Prepared Statements

- **Parameterized queries:** `WHERE id = $1` — always used by Ecto
- **Prepared statements:** Query plan cached by name — can be disabled

**pgbouncer:** Use `prepare: :unnamed` (disables prepared statements, keeps parameterized queries).

### pool_count vs pool_size

More pools with fewer connections = better for benchmarks. **But** with mixed fast/slow queries, a single larger pool gives better latency.

**Rule:** `pool_count` for uniform workloads, larger `pool_size` for real apps.

### Sandbox Mode Doesn't Work With External Processes

Cachex, separate GenServers, or anything outside the test process won't share the sandbox transaction.

**Fix:** Make the external service use the test process, or accept it's not in the same transaction.

### Null Bytes Crash Postgres

PostgreSQL rejects null bytes even though they're valid UTF-8.

**Fix:** Sanitize at boundaries: `String.replace(string, "\x00", "")`

### preload_order for Association Sorting

```elixir
has_many :comments, Comment, preload_order: [desc: :inserted_at]
```

Note: Doesn't work for `through` associations.

### Runtime Migrations Use List API

```elixir
Ecto.Migrator.run(Repo, [{0, Migration1}, {1, Migration2}], :up, opts)
```

## Idioms

- Prefer `Repo.insert/1` over `Repo.insert!/1`—handle `{:ok, _}` / `{:error, _}` explicitly
- Use `Repo.transact/1` (Ecto 3.12+) for simple transactions instead of `Ecto.Multi`

## Red Flags - STOP and Reconsider

- belongs_to pointing to another context's schema
- Single changeset for all operations
- Preloading has-many with join
- CTEs in multi-tenant apps without explicit prefix
- Using pgbouncer without `prepare: :unnamed`
- Testing with Cachex/GenServers assuming sandbox shares transactions
- Accepting user input without null byte sanitization

**Any of these? Re-read the Gotchas section.**
来源:skillsdirectory

评论0

登录后参与评论

还没有评论,来抢沙发吧