# LibEcto
LibEcto 是一个为 [Ecto](https://hexdocs.pm/ecto/Ecto.html) 提供的简单封装库,让日常数据库操作变得更加便捷。
## 为什么选择 LibEcto
Ecto 是一个很棒的库,但在日常使用中可能显得有些冗长。例如,假设你有一个这样的 Schema:
```elixir
defmodule Sample.Schema do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "test" do
field :name, :string
field :value, :string
timestamps()
end
def changeset(m, params) do
m
|> cast(params, [:name, :value])
end
end
```
对于大多数常见用例,你需要编写大量样板代码来进行简单的 CRUD 操作:
```elixir
defmodule Sample.DB do
alias Sample.Schema
alias Sample.Repo
import Ecto.Changeset
def insert_one(params) do
Schema.changeset(%Schema{}, params)
|> Repo.insert()
end
def update_one(m, params) do
m
|> change(params)
|> Repo.update()
end
def delete_one(m) do
Repo.delete(m)
end
def get_by_id(id) do
Repo.get(Schema, id, select: [:id, :name, :value])
end
def get_by_id_array(id_array) do
Repo.all(from m in Schema, where: m.id in ^id_array, select: [:id, :name, :value])
end
def get_by_name(name) do
Repo.get_by(Schema, name: name, select: [:id, :name, :value])
end
#... 更多样板代码
# - get by name array
# - get by page
# - get by prefix
end
```
但是!使用 LibEcto,你可以这样写:
```elixir
defmodule Sample.DB do
use LibEctoV2
@repo Sample.Repo
@schema Sample.Schema
@columns [:id, :name, :value]
@filters [:id, :name]
def filter(:id, dynamic, %{"id" => value}) when is_bitstring(value),
do: {:ok, dynamic([m], ^dynamic and m.id == ^value)}
def filter(:id, dynamic, %{"id" => value}) when is_list(value),
do: {:ok, dynamic([m], ^dynamic and m.id in ^value)}
def filter(:name, dynamic, %{"name" => value}) when is_bitstring(value),
do: {:ok, dynamic([m], ^dynamic and m.name == ^value)}
def filter(:name, dynamic, %{"name" => {"like", value}}) when is_bitstring(value),
do: {:ok, dynamic([m], ^dynamic and like(m.name, ^value))}
def filter(:name, dynamic, %{"name" => value}) when is_list(value),
do: {:ok, dynamic([m], ^dynamic and m.name in ^value)}
def filter(_, dynamic, _), do: {:ok, dynamic}
def init_filter, do: dynamic([m], true)
# 如果 GenericDB 无法满足你的需求,你仍然可以使用 ecto 的能力来构建复杂查询、更新或事务
def other_complicated_query_or_update() do
# 执行一些复杂操作
end
end
```
LibEcto 会为你生成所有样板代码,让你可以专注于业务逻辑:
```elixir
iex> Sample.DB.create_one(%{name: "test", value: "testv"})
{:ok, %Sample.Schema{id: "2JIebKci1ZgKenvhllJa3PMbydB", name: "test", value: "testv"}}
iex> Sample.DB.get_one(%{"name" => "test"})
{:ok, %Sample.Schema{id: "2JIebKci1ZgKenvhllJa3PMbydB", name: "test", value: "testv"}}
iex> Sample.DB.get_one(%{"name" => "not-exists"})
{:ok, nil}
iex> Sample.DB.get_one!(%{"name" => "not-exists"})
** (LibEcto.Exception) not found: %{"name" => "not-exists"}
iex> {:ok, m} = Sample.DB.get_one(%{"name" => "test"})
iex> Sample.DB.update_one(m, %{name: "test2"})
{:ok, %Sample.Schema{id: "2JIebKci1ZgKenvhllJa3PMbydB", name: "test2", value: "testv"}}
```
## 支持的函数
### 写入操作
- `create_one/1` - 创建单条记录
- `update_one/2` - 更新单条记录
- `delete_one/1` - 删除单条记录
- `delete_all/1` - 删除所有匹配的记录
### 读取操作
- `get_one/2` - 获取单条记录(支持指定列)
- `fetch_one/2` - 获取单条记录,不存在时返回错误
- `get_one!/2` - 获取单条记录,不存在时抛出异常
- `get_all/2` - 获取所有匹配的记录
- `get_limit/4` - 获取有限数量的记录(支持排序)
- `get_by_page/5` - 分页获取记录
- `exists?/1` - 检查记录是否存在
- `count/1` - 计算匹配记录的数量
## V2 版本特性
V2 是 LibEcto 的完全重写版本,它更加强大和灵活。它将原来复杂的宏分解为更合理的结构。主要改进包括:
### 更清晰的架构
- 模块化的设计,职责分离
- 更好的类型安全性和错误处理
- 支持自定义过滤器和验证器
### 更灵活的配置
- 支持动态查询条件构建
- 可配置的列选择和过滤器
- 支持复杂的查询逻辑
### 更好的开发体验
- 自动生成类型定义
- 统一的错误处理机制
- 详细的文档和示例
## 安装
在 `mix.exs` 中添加 `lib_ecto` 到依赖列表:
```elixir
def deps do
[
{:lib_ecto, "~> 0.3"}
]
end
```
## 使用示例
### 基本用法
```elixir
defmodule MyApp.UserDB do
use LibEctoV2
@repo MyApp.Repo
@schema MyApp.User
@columns [:id, :name, :email, :age]
@filters [:id, :name, :email, :age]
# 定义过滤器
def filter(:id, dynamic, %{"id" => id}) when is_binary(id),
do: {:ok, dynamic([u], ^dynamic and u.id == ^id)}
def filter(:name, dynamic, %{"name" => name}) when is_binary(name),
do: {:ok, dynamic([u], ^dynamic and u.name == ^name)}
def filter(:email, dynamic, %{"email" => email}) when is_binary(email),
do: {:ok, dynamic([u], ^dynamic and u.email == ^email)}
def filter(:age, dynamic, %{"age" => age}) when is_integer(age),
do: {:ok, dynamic([u], ^dynamic and u.age == ^age)}
def filter(:age, dynamic, %{"age" => {"gt", min}}) when is_integer(min),
do: {:ok, dynamic([u], ^dynamic and u.age > ^min)}
def filter(:age, dynamic, %{"age" => {"lt", max}}) when is_integer(max),
do: {:ok, dynamic([u], ^dynamic and u.age < ^max)}
def filter(_, dynamic, _), do: {:ok, dynamic}
def init_filter, do: dynamic([u], is_nil(u.deleted_at))
end
```
### 高级用法
```elixir
# 创建用户
{:ok, user} = MyApp.UserDB.create_one(%{
name: "John Doe",
email: "john@example.com",
age: 30
})
# 获取用户
{:ok, user} = MyApp.UserDB.get_one(%{"email" => "john@example.com"})
# 分页获取用户
{:ok, %{list: users, total: total}} = MyApp.UserDB.get_by_page(
%{"age" => {"gt", 25}}, # 条件:年龄大于25
1, # 第1页
10, # 每页10条
[:id, :name, :email], # 只选择这些列
[desc: :created_at] # 按创建时间降序
)
# 检查用户是否存在
exists = MyApp.UserDB.exists?(%{"name" => "John Doe"})
# 统计用户数量
{:ok, count} = MyApp.UserDB.count(%{"age" => {"lt", 40}})
# 更新用户
{:ok, updated_user} = MyApp.UserDB.update_one(user, %{age: 31})
# 删除用户
{:ok, _} = MyApp.UserDB.delete_one(user)
```
## 测试
测试用例使用 [ecto_sqlite3](https://github.com/elixir-sqlite/ecto_sqlite3) 作为数据库。
运行测试:
```bash
mix test
```
测试覆盖率:
```bash
mix coveralls
```
## 贡献
欢迎提交 Issue 和 Pull Request!
## 许可证
MIT License