# pgbatis
> pgbaits 编写规则纯属个人习惯与风格,不喜勿用。
轻量级异步 PostgreSQL CRUD 辅助库,基于 `tokio-postgres` + `deadpool-postgres`。
通过派生宏 `#[derive(PGCRUD)]` 快速为结构体生成增删改查能力,提供链式条件构造器(`Wrapper`)和可选逻辑删除支持。
## 特性
- **派生宏驱动** — `#[derive(PGCRUD)]` 自动生成 INSERT/UPDATE/SELECT SQL 及 `Parameters` trait 实现
- **链式条件构造** — `Wrapper` 提供 `eq`、`like`、`between`、`gt`、`set_pages` 等链式方法,统一使用 `$n` 参数化查询
- **逻辑删除** — 默认启用,读操作自动过滤已删除数据,`remove()` 默认软删除,支持按表排除
- **分页查询** — 单表 `fetch_page` 和原生 SQL `query_page` 均自动计算 total、pages
- **事务支持** — `transaction_delete_and_save`、`transaction_batch` 支持跨表批量操作
- **灵活查询** — 类型化查询(`fetch`/`fetch_one`)与 HashMap 查询(`query`/`query_one`)双模式
## 快速开始
### 1. 添加依赖
```toml
[dependencies]
pgbatis = "0.1"
serde = { version = "1.0", features = ["derive"] }
```
### 2. 定义实体
```rust
use pgbatis::pgmacro::PGCRUD;
use serde::{Deserialize, Serialize};
#[derive(PGCRUD, Serialize, Deserialize, Clone, Debug, Default)]
pub struct User {
pub id: Option<String>,
pub name: Option<String>,
pub age: Option<i32>,
}
```
`#[derive(PGCRUD)]` 还会自动生成对应的 `UserColumn` 结构体(如 `UserColumn::Id`、`UserColumn::Name`),
用于在 `Wrapper` 中引用列名,避免硬编码字符串。
### 3. 初始化连接
```rust,ignore
// 必须在任何数据库操作之前调用
pgbatis::link("127.0.0.1", 5432, "user", "password", "mydb", 10).await?;
```
### 4. CRUD 操作
```rust,ignore
use pgbatis::Wrapper;
// ---- 保存 ----
let user = User { id: Some("1".into()), name: Some("Alice".into()), age: Some(30) };
pgbatis::save(&user, None, None).await?;
// ---- 条件更新 ----
let update = User { name: Some("Bob".into()), ..Default::default() };
let wrapper = Wrapper::new().eq(&UserColumn::Id, &"1");
pgbatis::update(update, wrapper, None, None).await?;
// ---- 查询列表 ----
let users: Vec<User> = pgbatis::fetch(Wrapper::new().gt(&UserColumn::Age, &18), None, None).await?;
// ---- 单条查询 ----
let user: User = pgbatis::fetch_one(Wrapper::new().eq(&UserColumn::Id, &"1"), None, None).await?;
// ---- 分页查询 ----
let page = pgbatis::fetch_page::<User>(
Wrapper::new().set_pages(1, 10).set_order_by_column_ext(&UserColumn::Id, false),
None, None,
).await?;
println!("第{}/{}页,共{}条", page.current_page, page.pages, page.total);
// ---- 删除(默认软删除) ----
pgbatis::remove::<User>(Wrapper::new().eq(&UserColumn::Id, &"1"), None, None).await?;
// ---- 强制物理删除 ----
pgbatis::permanent_deletion::<User>(Wrapper::new().eq(&UserColumn::Id, &"1"), None, None).await?;
```
## API 总览
### 连接管理
| `link(host, port, user, password, dbname, max_size)` | 初始化连接池(必须最先调用) |
| `ping()` | 连接探活(执行 `SELECT 1`) |
### CRUD 操作
| `save(entity, prefix, suffix)` | INSERT,SQL 由 `Parameters::gen_save` 自动生成 |
| `update(entity, wrapper, prefix, suffix)` | UPDATE,结合 Wrapper 条件 |
| `remove::<T>(wrapper, prefix, suffix)` | 删除(默认软删除,排除表则物理删除) |
| `permanent_deletion::<T>(wrapper, prefix, suffix)` | 强制物理 DELETE |
### 类型化查询(返回实体)
| `fetch::<T>(wrapper, prefix, suffix)` | 单表查询,返回 `Vec<T>` |
| `fetch_one::<T>(wrapper, prefix, suffix)` | 单表查询单条,返回 `T` |
| `fetch_page::<T>(wrapper, prefix, suffix)` | 单表分页,返回 `PageT<T>` |
| `fetch_with_recoder_field::<T>(…)` | 单表查询,返回 `Vec<HashMap<String, Value>>` |
### 原生 SQL 查询(返回 HashMap)
| `query(sql, params)` | 查询列表 → `Vec<HashMap<String, Value>>` |
| `query_one(sql, params)` | 查询单条 → `HashMap<String, Value>` |
| `query_page(sql, params, order_by, desc, page, size)` | 分页查询 → `PageHash` |
| `query_t::<T>(sql, params)` | 查询列表 → `Vec<T>`(SELECT 字段顺序须与实体一致) |
| `execute(sql, params)` | 执行非查询 SQL → 影响行数 |
### 存在性检查
| `check_row_by_column(table, column, value)` | 按列值检查是否存在 |
| `check_row_wrap::<T>(wrapper, prefix, suffix)` | 按 Wrapper 条件检查是否存在 |
### 事务
| `transaction_delete_and_save(…)` | 先删后批量插(事务内) |
| `transaction_batch(params)` | 批量执行多条 SQL(可跨表) |
### 工具函数
| `format_like(s)` | `"hello"` → `Some("%hello%")` |
| `format_like_left(s)` | `"hello"` → `Some("%hello")` |
| `format_like_right(s)` | `"hello"` → `Some("hello%")` |
| `set_unlogic_delete_table(table)` | 将表加入逻辑删除排除名单 |
## Wrapper 条件构造器
```rust,ignore
use pgbatis::Wrapper;
// 假设实体 Order 有字段 status, price, stock, created_at 等
// PGCRUD 会生成对应的 OrderColumn
let wrapper = Wrapper::new()
// 比较
.eq(&UserColumn::Name, &"Alice")
.not_eq(&OrderColumn::Status, &0)
.gt(&UserColumn::Age, &18)
.ge(&OrderColumn::Score, &60)
.lt(&OrderColumn::Price, &100)
.le(&OrderColumn::Stock, &50)
// 范围
.between(&OrderColumn::CreatedAt, &start, &end)
.not_between(&OrderColumn::Price, &min, &max)
// 模糊
.like(&UserColumn::Name, &format!("%{}%", keyword))
// 集合
.set_in(&UserColumn::Id, &some_value)
.in_array_string(&OrderColumn::Status, &vec!["active".into(), "pending".into()])
// 空值
.is_null::<String>(&OrderColumn::DeletedAt)
.is_not_null::<String>(&OrderColumn::Email)
// 指定返回字段(不设置则返回全部字段)
.set_recoder_field(&UserColumn::Id)
.set_recoder_field(&UserColumn::Name)
// 排序
.set_order_by_column_ext(&UserColumn::Id, false) // 类型安全
// .set_order_by("id", false)? // 字符串形式,含注入校验
// 分页
.set_pages(1, 20);
// 构建 WHERE 子句
let (where_clause, args) = wrapper.build(1).unwrap();
```
## 逻辑删除
### 默认行为
- 读操作(`fetch`/`fetch_one`/`fetch_page`/`check_row_*`)自动追加 `is_deleted = 0` 过滤
- `remove()` 执行软删除(`UPDATE SET is_deleted = 1`)
- `permanent_deletion()` 始终执行物理删除
### 排除指定表
```rust,ignore
// 对日志表执行物理删除,查询时也不过滤 is_deleted
pgbatis::set_unlogic_delete_table("sys_log");
```
## 原生 SQL 示例
```rust,ignore
// 联表查询
let sql = "SELECT u.name, o.amount FROM users u \
JOIN orders o ON u.id = o.user_id WHERE o.amount > $1";
let rows = pgbatis::query(sql, &[&100]).await?;
// 联表分页
let page = pgbatis::query_page(
"SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id",
&[], "amount", true, 1, 10,
).await?;
// 聚合查询
let row = pgbatis::query_one("SELECT count(*) as cnt FROM users WHERE age > $1", &[&18]).await?;
```
## 安全注意事项
- 始终通过 `Wrapper` 方法构造条件(参数化查询 `$n`),避免手写 SQL 拼接用户输入
- `in_array_string` 使用字符串拼接,仅在输入可控时使用
- `set_order_by` 会校验 `--` 和 `;` 字符,防止注入
## 许可证
MIT