数据库与查询构建器
Anon Framework Next 提供 PDO 驱动的 SQL 数据库层,以及独立的 MongoDB 数据库层。
- SQL:
Connection + QueryBuilder + Model - Mongo:
Mongo\Connection + Mongo\QueryBuilder + Mongo\ModelQueryBuilder - 迁移:当前以原生 SQL 和 Mongo schema helper 为主,不提供统一字段级 Schema Builder
数据库驱动支持范围
当前 Connection 层可识别这些数据库类型:
mysqlpgsqlsqlitesqlsrvoracleocimongomongodbmysql/pgsql/sqlite/sqlsrv/oracle/oci:走PDO + SQL QueryBuildermongo/mongodb:走独立的 Mongo 连接与非 SQL QueryBuilder“支持”表示已接入连接层或查询层,不表示所有驱动语义完全等价
Mongo 相关类已拆到独立目录:
Anon\Core\Database\Mongo\ConnectionAnon\Core\Database\Mongo\QueryBuilderAnon\Core\Database\Mongo\ModelQueryBuilderAnon\Core\Database\Model\QueryBuilder
支持状态建议这样理解:
mysql/sqlite:当前最稳pgsql/sqlsrv/oracle/oci:已接入,但要关注方言差异mongo/mongodb:已接入,但不是 SQL 等价实现
SQL 驱动当前需要特别注意:
- 标识符包装符不同
LIKE/ILIKE/ 分页 /UPSERT已做驱动分支,但仍建议实库验证- JSON 查询已补路径取值比较,
contains只在mysql/pgsql开放 - 迁移 SQL 需要按目标数据库自己写原生语法
- 迁移系统只保证迁移记录表初始化,业务迁移内容不做跨库字段抽象
Mongo 当前能力:
- 独立连接、CRUD、排序、分页、正则、
upsert、模型查询、软删除、基础聚合管道 _id、嵌套 BSON 条件、常见*_id外键会做基础归一化- 默认主键
id会自动映射为_id - 默认外键命名仍保持
user_id
Mongo 当前不支持:
join()having()selectRaw()orderByRaw()
Mongo 事务依赖会话能力,通常要求副本集或分片集群。
字段类型说明
当前框架没有提供统一的 Schema Builder 字段抽象层:
- 迁移里的字段类型请直接写目标数据库自己的原生 SQL 类型
VARCHAR,TEXT,INT,BIGINT,TIMESTAMP,JSON,UUID等写法是否可用,取决于你当前使用的数据库- 如果你需要跨 MySQL / PostgreSQL / SQLServer / Oracle 共用同一套迁移 SQL,需要自己处理方言差异
例如:
- MySQL 常见写法:
INT AUTO_INCREMENT PRIMARY KEY - SQLite 常见写法:
INTEGER PRIMARY KEY AUTOINCREMENT - PostgreSQL 常见写法:
GENERATED ... AS IDENTITY或SERIAL - SQL Server 常见写法:
INT IDENTITY(1,1) PRIMARY KEY - Oracle 常见写法:
NUMBER、VARCHAR2,以及序列或 identity 语法
配置
通过项目根目录的 anon.config.php 配置数据库连接,通过 .env* 保存敏感值。配置文件建议统一使用 Env::get():
use Anon\Core\Facade\Env;
return [
'database' => [
'type' => Env::get('DATABASE_TYPE', 'mysql'),
'host' => Env::get('DATABASE_URL', '127.0.0.1'),
'port' => (int) Env::get('DATABASE_PORT', 3306),
'database' => Env::get('DATABASE_NAME', 'anon_test'),
'username' => Env::get('DATABASE_USER', 'root'),
'password' => Env::get('DATABASE_PASSWORD', ''),
'charset' => Env::get('DATABASE_CHARSET', 'utf8mb4'),
'prefix' => Env::get('DATABASE_PREFIX', ''),
],
];配合 .env 文件保存敏感值:
[DATABASE]
DATABASE_TYPE=mysql
DATABASE_URL=127.0.0.1
DATABASE_PORT=3306
DATABASE_USER=root
DATABASE_PASSWORD=root
DATABASE_NAME=anon_test
DATABASE_PREFIX=Mongo 配置示例:
use Anon\Core\Facade\Env;
return [
'database' => [
'type' => Env::get('DATABASE_TYPE', 'mongodb'),
'host' => Env::get('DATABASE_URL', '127.0.0.1'),
'port' => (int) Env::get('DATABASE_PORT', 27017),
'database' => Env::get('DATABASE_NAME', 'anon'),
'username' => Env::get('DATABASE_USER', ''),
'password' => Env::get('DATABASE_PASSWORD', ''),
'auth_source' => Env::get('DATABASE_AUTH_SOURCE', 'admin'),
'prefix' => '',
],
];对应 .env:
[DATABASE]
DATABASE_TYPE=mongodb
DATABASE_URL=127.0.0.1
DATABASE_PORT=27017
DATABASE_USER=
DATABASE_PASSWORD=
DATABASE_NAME=anon
DATABASE_AUTH_SOURCE=adminMongo 扩展查询
Mongo 额外提供两类非 SQL 风格接口:
use Anon\Core\Facade\DB;
// 直接追加文档过滤条件
$rows = DB::table('users')
->where('status', 1)
->whereDocument([
'$or' => [
['profile.city' => 'Tokyo'],
['tags' => ['$in' => ['vip']]],
],
])
->get();use Anon\Core\Facade\DB;
// 直接执行聚合管道
$rows = DB::table('orders')
->where('status', 'paid')
->aggregatePipeline([
[
'$group' => [
'_id' => '$user_id',
'total_amount' => ['$sum' => '$amount'],
],
],
[
'$sort' => ['total_amount' => -1],
],
]);基础统计仍然可以直接用:
DB::table('orders')->where('status', 'paid')->count();
DB::table('orders')->where('status', 'paid')->aggregate('SUM', 'amount');
DB::table('orders')->where('status', 'paid')->sum('amount');
DB::table('orders')->avg('score');
DB::table('orders')->min('amount');
DB::table('orders')->max('amount');结果提取和时间排序快捷方法:
DB::table('users')->latest()->pluck('email');
DB::table('users')->oldest('created_at')->pluck('name', 'id');
DB::table('users')->where('id', 1)->value('email');Mongo 查询构建器已补常用链式方法:
DB::table('users')
->where('status', 1)
->orWhere('role', 'admin')
->orWhereIn('user_id', ['6863d9a6f58e8a7e7d6f1234'])
->whereNotBetween('score', [0, 59])
->orWhereNull('deleted_at')
->get();Mongo 模型聚合结果也可以直接 hydrate 或分页:
$models = User::query()->aggregateModels([
['$match' => ['status' => 1]],
]);$result = User::query()->aggregatePaginate(
[
['$match' => ['status' => 1]],
],
20,
1,
true
);aggregatePaginate(..., true) 的最后一个参数表示是否 hydrate 成模型实例。
Mongo 文档型更新操作:
DB::table('users')->where('_id', $id)->increment('login_count');
DB::table('users')->where('_id', $id)->decrement('credits', 5);
DB::table('users')->where('_id', $id)->push('tags', 'vip', true);
DB::table('users')->where('_id', $id)->pull('tags', 'banned');
DB::table('users')->where('_id', $id)->unset(['legacy_field']);
DB::table('users')->where('_id', $id)->renameField('nickname', 'display_name');Mongo 集合和索引也可以直接走连接对象:
$mongo = app('db');
$mongo->createCollection('audit_logs');
$mongo->createIndex('audit_logs', ['user_id' => 1, 'created_at' => -1], ['unique' => false]);
$mongo->listIndexes('audit_logs');
$mongo->dropIndex('audit_logs', ['user_id' => 1, 'created_at' => -1]);也可以在查询构建器上直接操作当前 collection 的索引:
DB::table('audit_logs')->createIndex(['user_id' => 1]);
DB::table('audit_logs')->listIndexes();
DB::table('audit_logs')->dropIndex(['user_id' => 1]);Mongo 连接还提供了轻量 schema helper:
use Anon\Core\Facade\DB;
DB::schema()->create('audit_logs', function ($collection) {
$collection->index(['user_id' => 1, 'created_at' => -1]);
$collection->ttl('expired_at', 0);
$collection->validator([
'bsonType' => 'object',
'required' => ['user_id', 'action'],
'properties' => [
'user_id' => ['bsonType' => 'objectId'],
'action' => ['bsonType' => 'string'],
],
]);
});也可以直接拿 collection helper:
$users = DB::schema()->table('users');
$users->unique(['email' => 1]);
$users->text(['nickname', 'bio']);
$users->hashed('tenant_id');
$users->indexes();DB::schema() 当前是 Mongo 连接专用 helper,SQL 驱动下不会返回通用 SchemaBuilder。
Mongo 迁移类也可以直接使用基类里的 helper:
use Anon\Core\Database\Migration\Migration;
class CreateAuditLogs extends Migration
{
public function up(): void
{
$this->schema()->create('audit_logs', function ($collection) {
$collection->index(['user_id' => 1, 'created_at' => -1]);
});
}
public function down(): void
{
$this->schema()->dropIfExists('audit_logs');
}
}迁移运行器本身已兼容 Mongo,不再强依赖 PDO 记录迁移状态。
模型关系字段如果明确就是 ObjectId 语义,可以直接用显式 helper:
public function user()
{
return $this->objectIdBelongsTo(User::class);
}
public function posts()
{
return $this->objectIdHasMany(Post::class);
}这样会直接按 Mongo 常见的 _id / user_id 规则建关系,减少手动传键名。
如果不是 mysql,建议显式配置这些值:
DATABASE_TYPEDATABASE_CHARSETDATABASE_PORT
默认值本身更偏向 MySQL 场景。
数据库事务
查询构建器提供了事务支持:
try {
DB::beginTransaction();
DB::table('users')->where('id', 1)->update(['balance' => 100]);
DB::table('orders')->insert(['user_id' => 1, 'amount' => 50]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}也支持事务回调写法:
DB::transaction(function () {
DB::table('users')->where('id', 1)->update(['balance' => 100]);
DB::table('orders')->insert(['user_id' => 1, 'amount' => 50]);
});执行原生 SQL
use Anon\Core\Facade\DB;
// 执行查询
$users = DB::select('SELECT * FROM users WHERE status = ?', [1]);
// 执行写入/更新/删除等操作
$affected = DB::statement('UPDATE users SET status = ? WHERE id = ?', [0, 1]);什么时候应该优先写原生 SQL
下面这些情况优先直接写原生 SQL:
- 需要跨数据库兼容地建表
- 需要用到数据库方言能力
- 需要复杂聚合、窗口函数、CTE
- 需要数据库特有的 JSON / ARRAY / 高级 UPSERT 语法
查询构建器 (Query Builder)
查询数据
// 获取所有数据
$users = DB::table('users')->get();
// 指定查询字段
$users = DB::table('users')->select(['id', 'name', 'email'])->get();
// 获取单条数据
$user = DB::table('users')->where('id', 1)->first();
// 获取数据总数
$count = DB::table('users')->where('status', 1)->count();
// 判断记录是否存在
$exists = DB::table('users')->where('email', 'admin@example.com')->exists();条件查询
// AND 条件
DB::table('users')->where('status', 1)->where('role_id', 2)->get();
// OR 条件
DB::table('users')->where('status', 1)->orWhere('role_id', 2)->get();
// IN 查询
DB::table('users')->whereIn('id', [1, 2, 3])->get();
DB::table('users')->whereNotIn('id', [4, 5, 6])->get();
// BETWEEN 查询
DB::table('users')->whereBetween('created_at', ['2026-01-01', '2026-12-31'])->get();
DB::table('users')->whereNotBetween('score', [60, 100])->get();
// NULL 查询
DB::table('users')->whereNotNull('email')->get();
// LIKE 查询
DB::table('users')->whereLike('name', '%anon%')->get();
DB::table('users')->whereNotLike('email', '%spam%')->get();
// JSON 路径取值查询
DB::table('users')->whereJsonValue('profile', 'address.city', 'Tokyo')->get();
DB::table('users')->whereJsonNotValue('profile', 'settings.lang', 'zh-CN')->get();whereLike() / whereNotLike() 会按驱动做最小兼容处理:
pgsql会优先使用ILIKEmysql在大小写敏感模式下会使用LIKE BINARY- 其他 SQL 驱动会退回到
LOWER(column) LIKE LOWER(?)这一类通用写法
正则条件
DB::table('users')->whereRegex('name', '^anon', false)->get();
DB::table('users')->whereNotRegex('email', '@spam\\.com$', false)->get();当前正则条件按驱动分为:
pgsql:使用~/~*/!~/!~*mysql:使用REGEXP/NOT REGEXPoracle/oci:使用REGEXP_LIKEsqlsrv:当前不会假装支持,会直接抛出异常sqlite:当前依赖自定义REGEXP函数,框架默认没有注册,也会直接抛出异常
JSON 路径值条件
DB::table('users')->whereJsonValue('profile', 'address.city', 'Tokyo')->get();
DB::table('users')->orWhereJsonValue('profile', '$.settings.theme', 'dark')->get();
DB::table('users')->whereJsonValue('profile', 'flags.0', true)->get();
DB::table('users')->whereJsonNotValue('profile', 'meta.deleted_at', null)->get();
DB::table('users')->whereJsonLike('profile', 'bio', '%anon%')->get();
DB::table('users')->whereJsonIn('profile', 'status', ['active', 'pending'])->get();
DB::table('users')->whereJsonContains('profile', '$', ['tags' => ['vip']])->get();whereJsonValue() 当前按驱动转成:
mysql:JSON_EXTRACT/JSON_UNQUOTEpgsql:#>>sqlite:json_extractsqlsrv/oracle/oci:JSON_VALUE
当前限制:
- 只支持标量值比较
LIKE/IN这类 JSON 条件也是基于路径取值做比较contains当前只支持mysql/pgsql- 路径只支持点号路径和数字索引
- JSON 对象、数组包含判断仍建议直接写原生 SQL
聚合与 HAVING
$rows = DB::table('orders')
->select(['user_id'])
->selectRaw('COUNT(*) as total_orders')
->groupBy('user_id')
->having('total_orders', '>', 10)
->get();
$rows = DB::table('orders')
->select(['user_id'])
->selectRaw('SUM(amount) as total_amount')
->groupBy('user_id')
->havingRaw('SUM(amount) > ?', [1000])
->get();having() 适合常规字段比较;涉及聚合表达式或别名兼容时优先用 havingRaw()。
排序与分页
注意: 分页已按
mysql / pgsql / sqlite / sqlsrv / oracle / oci做驱动分支,但sqlsrv、oracle / oci仍建议先在目标数据库验证。
分页和条数限制支持这几种写法:
// 方式一:独立使用 limit() 和 offset()
$users = DB::table('users')
->orderBy('created_at', 'DESC')
->limit(10)
->offset(20)
->get();
// 方式二:直接在 limit() 中传入 limit 和 offset
$users = DB::table('users')
->orderBy('created_at', 'DESC')
->limit(10, 20)
->get();
// 方式三:使用 paginate()
$result = DB::table('users')->paginate(15);连表查询 (Join)
$users = DB::table('users')
->select(['users.id', 'users.name', 'roles.role_name'])
->leftJoin('roles', 'users.role_id', '=', 'roles.id')
->get();Raw 表达式
$rows = DB::table('orders')
->select(['user_id'])
->selectRaw('SUM(amount) as total_amount')
->groupBy('user_id')
->orderByRaw('total_amount DESC')
->get();当前提供的 raw 入口有:
selectRaw()havingRaw()orderByRaw()
这些方法只适合你自己明确可控的 SQL 片段,不能把不可信用户输入直接拼进去。
插入数据
// 插入单条数据
$insertId = DB::table('users')->insert([
'name' => 'Anon',
'email' => 'anon@example.com'
]);
// UPSERT
DB::table('users')->upsert(
[
'email' => 'anon@example.com',
'name' => 'Anon',
'status' => 1,
],
['email'],
['name', 'status']
);upsert() 当前分为两类实现:
mysql:ON DUPLICATE KEY UPDATEpgsql/sqlite:ON CONFLICTsqlsrv/oracle/oci:事务包裹的 update-or-insert 回退
insert() / ORM save() 的主键回填当前规则:
pgsql会优先走RETURNINGsqlsrv会优先走OUTPUT INSERTEDoracle/oci会优先走RETURNING ... INTO- 其他驱动先取
lastInsertId() - 取不到时,模型会回退到已显式写入的数据主键
- SQL 模型如果依赖 sequence,可在模型里声明
protected ?string $sequence
批量插入数据
对于大量数据的插入,使用 insertAll() 可将所有数据合并为一条原生 SQL 执行。
$data = [
['name' => 'User1', 'email' => 'user1@example.com'],
['name' => 'User2', 'email' => 'user2@example.com'],
['name' => 'User3', 'email' => 'user3@example.com']
];
// 批量插入
$affectedRows = DB::table('users')->insertAll($data);更新与删除数据
// 更新数据
DB::table('users')->where('id', 1)->update(['status' => 0]);
// 删除数据
DB::table('users')->where('status', 0)->delete();批量数据处理
1. 分块处理 (Chunk)
对于大量数据集的处理,可以使用 chunk() 方法。它会自动对数据进行分页查询。
DB::table('users')->where('status', 1)->chunk(500, function ($users, $page) {
foreach ($users as $user) {
// 执行逻辑
}
});2. 游标迭代 (Cursor)
如果是用于数据导出且只需要顺序遍历,可以使用 cursor()。它利用 PHP 的 Generator 特性和 PDO 游标,减少内存使用。
foreach (DB::table('logs')->cursor() as $log) {
echo $log['message'];
}ORM 模型 (Model)
Anon Framework Next 提供了基于 ActiveRecord 模式的 ORM 模型。
定义模型类,继承 Anon\Core\Database\Model:
namespace Anon\Model;
use Anon\Core\Database\Model;
class User extends Model
{
protected string $table = 'users';
protected string $primaryKey = 'id';
}如果主键不是默认 id,或者目标数据库依赖 sequence,可以直接在模型里声明:
class Invoice extends Model
{
protected string $table = 'invoices';
protected string $primaryKey = 'invoice_id';
protected ?string $sequence = 'invoices_invoice_id_seq';
}使用模型进行数据操作:
use Anon\Model\User;
// 查询所有记录
$users = User::all();
// 根据主键查询单条记录
$user = User::find(1);
// 结合查询构建器进行条件查询
$user = User::where('status', 1)->first();
// 创建新数据
$user = User::create(['name' => 'Anon', 'email' => 'anon@example.com']);
// 更新数据
$user = User::find(1);
if ($user) {
$user->name = 'New Name';
$user->save();
}
// 批量删除数据
User::destroy(1);
User::destroy([1, 2, 3]);模型事件
ORM 支持在模型的生命周期中触发事件,这对于解耦业务逻辑非常有用。例如在创建用户后自动初始化配置,或在更新时记录日志。
支持的事件列表:
creating/createdupdating/updatedsaving/saveddeleting/deleted
在任意前置事件(如 creating)中返回 false 将会中断随后的保存或删除操作。
namespace Anon\Model;
use Anon\Core\Database\Model;
class User extends Model
{
protected static function boot()
{
// 注册事件
static::creating(function (User $user) {
// 在插入数据库前执行
if (empty($user->uuid)) {
$user->uuid = \Anon\Core\Support\Str::uuid();
}
});
static::deleted(function (User $user) {
// 用户删除后,级联删除他的帖子
Post::query()->where('user_id', $user->id)->delete();
});
}
}注意: 框架启动时需要在合适的生命周期(如服务提供者或全局 Hook
app_init中)主动调用一次模型的boot方法以注册闭包。或者你也可以在项目自己的模型基类里,例如Anon\Model\BaseModel的构造函数中做静态检测。
关系模型
当前 ORM 已支持常见的三种基础关系:
hasOnehasManybelongsTo
一对一
namespace Anon\Model;
use Anon\Core\Database\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class, 'user_id', 'id');
}
}一对多
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class, 'user_id', 'id');
}
}反向关联
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}预加载
为了避免列表接口中频繁触发 N+1 查询,可以在查询时使用 with() 进行预加载:
use Anon\Model\User;
$users = User::query()->with(['profile', 'posts'])->get();
foreach ($users as $user) {
echo $user->profile?->nickname;
echo count($user->posts);
}单条查询同样支持:
$user = User::query()->with('profile')->where('id', 1)->first();当前能力边界
如果你准备在生产里使用多数据库,建议先按下面理解当前边界:
- 连接层:已支持多 PDO 驱动
- Query Builder:基础 CRUD、条件、Join、排序、分组可用
- ORM:基础 ActiveRecord、关系、预加载、软删除可用
- 迁移:当前是原生 SQL 方案,不是 Laravel 风格的跨库字段抽象
- 跨库一致性:分页、正则、主键回填、迁移 SQL 仍建议实库验证
软删除
在模型中开启软删除后,普通查询会自动排除已删除数据,delete() 会改为写入删除时间:
class Post extends Model
{
protected bool $softDelete = true;
}基础用法
$post = Post::find(1);
$post?->delete(); // 写入 deleted_at
$all = Post::withTrashed()->get(); // 包含已删除
$trashed = Post::onlyTrashed()->get(); // 仅已删除
$post?->restore(); // 恢复
$post?->forceDelete(); // 物理删除