模型与数据库
数据模型是应用的核心组件,负责定义数据结构、处理数据逻辑和数据库交互。本指南将详细介绍如何在 Dux PHP Admin 中开发数据模型。
模型基础
模型概念
在 Dux PHP Admin 中,模型遵循 Active Record 模式,每个模型类对应数据库中的一张表:
- 数据表示 - 模型代表数据库表的一行记录
- 业务逻辑 - 封装与数据相关的业务规则
- 数据验证 - 验证数据的完整性和正确性
- 关系映射 - 定义表之间的关联关系
基础模型结构
php
<?php
namespace App\YourModule\Models;
use Dux\Database\Model;
use Dux\Database\Attribute\AutoMigration;
use Illuminate\Database\Eloquent\SoftDeletes;
#[AutoMigration]
class YourModel extends Model
{
use SoftDeletes;
/**
* 数据表名
*/
protected $table = 'your_table';
/**
* 主键字段
*/
protected $primaryKey = 'id';
/**
* 主键类型
*/
protected $keyType = 'int';
/**
* 是否自动递增
*/
public $incrementing = true;
/**
* 是否启用时间戳
*/
public $timestamps = true;
/**
* 可批量赋值字段
*/
protected $fillable = [
'title',
'content',
'status',
'category_id',
'user_id',
'sort',
'views'
];
/**
* 不可批量赋值字段
*/
protected $guarded = [
'id',
'created_at',
'updated_at'
];
/**
* 隐藏字段(序列化时不包含)
*/
protected $hidden = [
'deleted_at',
'password'
];
/**
* 数据类型转换
*/
protected $casts = [
'status' => 'boolean',
'sort' => 'integer',
'views' => 'integer',
'config' => 'array',
'tags' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'published_at' => 'datetime'
];
/**
* 默认值
*/
protected $attributes = [
'status' => true,
'sort' => 0,
'views' => 0
];
}
自动迁移
迁移定义
使用 #[AutoMigration]
属性和 migration()
方法定义表结构:
php
#[AutoMigration]
class Article extends Model
{
/**
* 定义数据库表结构
*/
public function migration(): array
{
return [
// 主键
'id' => ['type' => 'id'],
// 字符串字段
'title' => [
'type' => 'string',
'length' => 255,
'nullable' => false,
'comment' => '文章标题'
],
'slug' => [
'type' => 'string',
'length' => 255,
'unique' => true,
'comment' => 'URL别名'
],
// 文本字段
'content' => [
'type' => 'text',
'nullable' => true,
'comment' => '文章内容'
],
'excerpt' => [
'type' => 'text',
'nullable' => true,
'comment' => '文章摘要'
],
// 数值字段
'views' => [
'type' => 'integer',
'default' => 0,
'unsigned' => true,
'comment' => '浏览次数'
],
'sort' => [
'type' => 'integer',
'default' => 0,
'comment' => '排序值'
],
'price' => [
'type' => 'decimal',
'precision' => 10,
'scale' => 2,
'default' => 0.00,
'comment' => '价格'
],
// 布尔字段
'status' => [
'type' => 'boolean',
'default' => true,
'comment' => '状态'
],
'featured' => [
'type' => 'boolean',
'default' => false,
'comment' => '是否推荐'
],
// JSON字段
'config' => [
'type' => 'json',
'nullable' => true,
'comment' => '配置信息'
],
'tags' => [
'type' => 'json',
'nullable' => true,
'comment' => '标签列表'
],
// 日期时间字段
'published_at' => [
'type' => 'timestamp',
'nullable' => true,
'comment' => '发布时间'
],
// 外键字段
'user_id' => [
'type' => 'integer',
'unsigned' => true,
'nullable' => true,
'foreign' => 'users.id',
'comment' => '用户ID'
],
'category_id' => [
'type' => 'integer',
'unsigned' => true,
'nullable' => true,
'foreign' => 'categories.id',
'comment' => '分类ID'
],
// 时间戳
'created_at' => ['type' => 'timestamp', 'nullable' => true],
'updated_at' => ['type' => 'timestamp', 'nullable' => true],
'deleted_at' => ['type' => 'timestamp', 'nullable' => true],
// 索引定义
'indexes' => [
'idx_status' => ['columns' => ['status']],
'idx_category_status' => ['columns' => ['category_id', 'status']],
'idx_published' => ['columns' => ['published_at']],
'fulltext_search' => ['columns' => ['title', 'content'], 'type' => 'fulltext']
]
];
}
}
字段类型
支持的字段类型及其选项:
php
// 基础类型
'id' => ['type' => 'id'], // 自增主键
'string' => ['type' => 'string', 'length' => 255],
'text' => ['type' => 'text'],
'longtext' => ['type' => 'longtext'],
'integer' => ['type' => 'integer', 'unsigned' => true],
'bigint' => ['type' => 'bigint'],
'decimal' => ['type' => 'decimal', 'precision' => 10, 'scale' => 2],
'float' => ['type' => 'float'],
'double' => ['type' => 'double'],
'boolean' => ['type' => 'boolean'],
'date' => ['type' => 'date'],
'datetime' => ['type' => 'datetime'],
'timestamp' => ['type' => 'timestamp'],
'time' => ['type' => 'time'],
'json' => ['type' => 'json'],
'binary' => ['type' => 'binary'],
// 特殊类型
'enum' => ['type' => 'enum', 'values' => ['active', 'inactive', 'pending']],
'set' => ['type' => 'set', 'values' => ['read', 'write', 'delete']],
// 字段选项
'nullable' => true, // 允许为空
'default' => 'value', // 默认值
'unique' => true, // 唯一约束
'index' => true, // 添加索引
'foreign' => 'table.field', // 外键约束
'comment' => '字段描述', // 字段注释
'unsigned' => true, // 无符号(数值类型)
'auto_increment' => true, // 自动递增
'charset' => 'utf8mb4', // 字符集
'collation' => 'utf8mb4_unicode_ci' // 排序规则
模型关联
一对一关联
php
class User extends Model
{
/**
* 用户资料(一对一)
*/
public function profile()
{
return $this->hasOne(UserProfile::class, 'user_id', 'id');
}
}
class UserProfile extends Model
{
/**
* 所属用户(反向一对一)
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
一对多关联
php
class Category extends Model
{
/**
* 分类下的文章(一对多)
*/
public function articles()
{
return $this->hasMany(Article::class, 'category_id', 'id');
}
/**
* 已发布的文章
*/
public function publishedArticles()
{
return $this->hasMany(Article::class, 'category_id', 'id')
->where('status', true)
->whereNotNull('published_at');
}
}
class Article extends Model
{
/**
* 文章分类(反向一对多)
*/
public function category()
{
return $this->belongsTo(Category::class, 'category_id', 'id');
}
}
多对多关联
php
class Article extends Model
{
/**
* 文章标签(多对多)
*/
public function tags()
{
return $this->belongsToMany(
Tag::class, // 关联模型
'article_tags', // 中间表名
'article_id', // 当前模型在中间表的外键
'tag_id', // 关联模型在中间表的外键
'id', // 当前模型的主键
'id' // 关联模型的主键
)->withTimestamps(); // 包含中间表时间戳
}
/**
* 带中间表数据的关联
*/
public function roles()
{
return $this->belongsToMany(Role::class, 'user_roles')
->withPivot(['status', 'expires_at'])
->withTimestamps();
}
}
class Tag extends Model
{
/**
* 标签文章(反向多对多)
*/
public function articles()
{
return $this->belongsToMany(Article::class, 'article_tags');
}
}
多态关联
php
class Comment extends Model
{
/**
* 可评论的模型(多态关联)
*/
public function commentable()
{
return $this->morphTo();
}
}
class Article extends Model
{
/**
* 文章评论(多态一对多)
*/
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Video extends Model
{
/**
* 视频评论(多态一对多)
*/
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
远程一对多
php
class Country extends Model
{
/**
* 国家的文章(通过用户)
*/
public function articles()
{
return $this->hasManyThrough(
Article::class, // 最终关联的模型
User::class, // 中间模型
'country_id', // 中间模型的外键
'user_id', // 最终模型的外键
'id', // 当前模型的主键
'id' // 中间模型的主键
);
}
}
查询构造器
基础查询
php
// 查找单个记录
$article = Article::find(1);
$article = Article::where('slug', 'hello-world')->first();
$article = Article::findOrFail(1); // 找不到抛异常
// 查找多个记录
$articles = Article::all();
$articles = Article::where('status', true)->get();
$articles = Article::whereIn('id', [1, 2, 3])->get();
// 条件查询
$articles = Article::where('views', '>', 100)
->where('status', true)
->orderBy('created_at', 'desc')
->limit(10)
->get();
// 分页查询
$articles = Article::where('status', true)
->paginate(20);
// 统计查询
$count = Article::where('status', true)->count();
$sum = Article::sum('views');
$avg = Article::avg('views');
$max = Article::max('views');
高级查询
php
// 子查询
$popularArticles = Article::whereIn('id', function ($query) {
$query->select('article_id')
->from('article_views')
->where('views', '>', 1000);
})->get();
// 关联查询
$articles = Article::with(['category', 'user', 'tags'])
->where('status', true)
->get();
// 预加载指定字段
$articles = Article::with([
'user:id,name,avatar',
'category:id,name,color'
])->get();
// 条件预加载
$articles = Article::with([
'comments' => function ($query) {
$query->where('status', 'approved')
->orderBy('created_at', 'desc');
}
])->get();
// 聚合查询
$categories = Category::withCount(['articles', 'publishedArticles'])
->having('articles_count', '>', 0)
->get();
// 原生查询
$articles = Article::selectRaw('*, (views / DATEDIFF(NOW(), created_at)) as daily_views')
->orderByRaw('daily_views DESC')
->get();
查询作用域
本地作用域
php
class Article extends Model
{
/**
* 已发布的文章
*/
public function scopePublished($query)
{
return $query->where('status', true)
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
/**
* 按分类筛选
*/
public function scopeByCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
/**
* 搜索文章
*/
public function scopeSearch($query, $keyword)
{
return $query->where(function ($q) use ($keyword) {
$q->where('title', 'like', "%{$keyword}%")
->orWhere('content', 'like', "%{$keyword}%");
});
}
/**
* 热门文章
*/
public function scopePopular($query, $days = 30)
{
return $query->where('created_at', '>=', now()->subDays($days))
->orderBy('views', 'desc');
}
/**
* 按时间范围
*/
public function scopeDateRange($query, $start, $end)
{
return $query->whereBetween('created_at', [$start, $end]);
}
}
// 使用作用域
$articles = Article::published()
->byCategory(1)
->search('Laravel')
->popular(7)
->get();
全局作用域
php
<?php
namespace App\YourModule\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('status', true)
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}
// 在模型中应用全局作用域
class Article extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new PublishedScope);
// 或使用闭包
static::addGlobalScope('published', function (Builder $builder) {
$builder->where('status', true);
});
}
/**
* 移除全局作用域
*/
public function newQueryWithoutScope($scope = null)
{
return parent::newQuery()->withoutGlobalScope($scope);
}
}
// 查询时移除全局作用域
$allArticles = Article::withoutGlobalScope(PublishedScope::class)->get();
$allArticles = Article::withoutGlobalScopes()->get();
访问器和修改器
访问器(Accessors)
php
class User extends Model
{
/**
* 获取用户全名
*/
public function getFullNameAttribute(): string
{
return $this->first_name . ' ' . $this->last_name;
}
/**
* 获取头像URL
*/
public function getAvatarUrlAttribute(): string
{
return $this->avatar ? asset('uploads/' . $this->avatar) : asset('images/default-avatar.png');
}
/**
* 获取格式化的创建时间
*/
public function getCreatedAtFormatAttribute(): string
{
return $this->created_at?->format('Y-m-d H:i:s') ?? '';
}
/**
* 获取状态文本
*/
public function getStatusTextAttribute(): string
{
return match($this->status) {
1 => '正常',
0 => '禁用',
-1 => '删除',
default => '未知'
};
}
}
// 使用访问器
$user = User::find(1);
echo $user->full_name; // 自动调用 getFullNameAttribute
echo $user->avatar_url; // 自动调用 getAvatarUrlAttribute
echo $user->status_text; // 自动调用 getStatusTextAttribute
修改器(Mutators)
php
class User extends Model
{
/**
* 设置密码(自动加密)
*/
public function setPasswordAttribute($value): void
{
$this->attributes['password'] = bcrypt($value);
}
/**
* 设置邮箱(转小写)
*/
public function setEmailAttribute($value): void
{
$this->attributes['email'] = strtolower($value);
}
/**
* 设置姓名(去除空格)
*/
public function setNameAttribute($value): void
{
$this->attributes['name'] = trim($value);
}
/**
* 设置手机号(去除特殊字符)
*/
public function setPhoneAttribute($value): void
{
$this->attributes['phone'] = preg_replace('/[^0-9]/', '', $value);
}
}
// 使用修改器
$user = new User();
$user->password = 'secret123'; // 自动调用 setPasswordAttribute
$user->email = 'USER@EXAMPLE.COM'; // 自动调用 setEmailAttribute
$user->save();
属性转换
php
class Article extends Model
{
protected $casts = [
// 基础类型转换
'status' => 'boolean',
'views' => 'integer',
'price' => 'decimal:2',
'published_at' => 'datetime',
// 数组和对象转换
'tags' => 'array',
'config' => 'object',
'meta' => 'collection',
// 自定义转换
'settings' => SettingsCast::class,
// 加密转换
'secret' => 'encrypted',
'token' => 'encrypted:array'
];
}
// 自定义类型转换
<?php
namespace App\YourModule\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class SettingsCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): array
{
return json_decode($value, true) ?? [];
}
public function set($model, string $key, $value, array $attributes): string
{
return json_encode($value);
}
}
模型事件
事件监听
php
class Article extends Model
{
protected static function booted(): void
{
// 创建前
static::creating(function ($article) {
if (!$article->user_id) {
$article->user_id = auth()->id();
}
if (!$article->slug) {
$article->slug = \Str::slug($article->title);
}
});
// 创建后
static::created(function ($article) {
// 发送通知
event(new \App\YourModule\Events\ArticleCreated($article));
// 更新统计
Cache::forget('articles_count');
});
// 更新前
static::updating(function ($article) {
if ($article->isDirty('title')) {
$article->slug = \Str::slug($article->title);
}
});
// 更新后
static::updated(function ($article) {
// 清除缓存
Cache::forget("article_{$article->id}");
// 触发事件
event(new \App\YourModule\Events\ArticleUpdated($article));
});
// 删除前
static::deleting(function ($article) {
// 删除关联数据
$article->comments()->delete();
$article->tags()->detach();
});
// 删除后
static::deleted(function ($article) {
// 清理缓存
Cache::forget("article_{$article->id}");
// 记录日志
\Log::info('文章已删除', ['id' => $article->id, 'title' => $article->title]);
});
// 保存前(创建和更新前都会触发)
static::saving(function ($article) {
// 数据验证
if (!$article->title) {
throw new \InvalidArgumentException('标题不能为空');
}
});
// 保存后(创建和更新后都会触发)
static::saved(function ($article) {
// 更新搜索索引
dispatch(new \App\Jobs\UpdateSearchIndex($article));
});
}
}
事件观察者
php
<?php
namespace App\YourModule\Observers;
use App\YourModule\Models\Article;
class ArticleObserver
{
public function creating(Article $article): void
{
$article->user_id = $article->user_id ?: auth()->id();
$article->slug = $article->slug ?: \Str::slug($article->title);
}
public function created(Article $article): void
{
// 发送创建通知
\Notification::send(
$article->user,
new \App\Notifications\ArticleCreated($article)
);
}
public function updating(Article $article): void
{
if ($article->isDirty('status') && $article->status) {
$article->published_at = now();
}
}
public function updated(Article $article): void
{
// 清除相关缓存
\Cache::tags(['articles', "article_{$article->id}"])->flush();
}
public function deleting(Article $article): void
{
// 软删除时保留数据,硬删除时清理关联
if ($article->isForceDeleting()) {
$article->comments()->forceDelete();
$article->tags()->detach();
}
}
public function deleted(Article $article): void
{
\Log::info('文章删除', [
'id' => $article->id,
'title' => $article->title,
'deleted_by' => auth()->id()
]);
}
public function restored(Article $article): void
{
\Log::info('文章恢复', [
'id' => $article->id,
'restored_by' => auth()->id()
]);
}
}
// 注册观察者
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Article::observe(ArticleObserver::class);
}
}
模型序列化
自定义序列化
php
class Article extends Model
{
/**
* 隐藏字段
*/
protected $hidden = [
'deleted_at',
'user_id'
];
/**
* 追加字段
*/
protected $appends = [
'status_text',
'url',
'excerpt'
];
/**
* 可见字段(在特定情况下)
*/
protected $visible = [
'id',
'title',
'content'
];
/**
* 获取状态文本
*/
public function getStatusTextAttribute(): string
{
return $this->status ? '已发布' : '草稿';
}
/**
* 获取文章URL
*/
public function getUrlAttribute(): string
{
return route('articles.show', $this->slug);
}
/**
* 获取摘要
*/
public function getExcerptAttribute(): string
{
return \Str::limit(strip_tags($this->content), 200);
}
/**
* 自定义JSON序列化
*/
public function toArray(): array
{
$array = parent::toArray();
// 添加计算字段
$array['reading_time'] = ceil(str_word_count($this->content) / 200);
// 格式化日期
if ($this->published_at) {
$array['published_at_human'] = $this->published_at->diffForHumans();
}
return $array;
}
}
// 临时修改序列化属性
$article = Article::find(1);
$article->makeVisible(['user_id']);
$article->makeHidden(['content']);
$article->append(['custom_field']);
API 资源转换
php
<?php
namespace App\YourModule\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ArticleResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'content' => $this->when($request->routeIs('articles.show'), $this->content),
'status' => $this->status,
'status_text' => $this->status_text,
'views' => $this->views,
'reading_time' => ceil(str_word_count($this->content) / 200),
// 关联资源
'category' => new CategoryResource($this->whenLoaded('category')),
'author' => new UserResource($this->whenLoaded('user')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
// 条件字段
'edit_url' => $this->when($request->user()?->can('update', $this), route('articles.edit', $this)),
// 时间字段
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
'published_at' => $this->published_at?->toISOString(),
// 人性化时间
'created_at_human' => $this->created_at->diffForHumans(),
'published_at_human' => $this->published_at?->diffForHumans(),
];
}
public function with($request): array
{
return [
'meta' => [
'version' => '1.0',
'generated_at' => now()->toISOString()
]
];
}
}
性能优化
查询优化
php
// 避免 N+1 查询
$articles = Article::with(['category', 'user', 'tags'])->get();
// 只加载需要的字段
$articles = Article::select('id', 'title', 'slug', 'published_at')
->with([
'category:id,name',
'user:id,name,avatar'
])->get();
// 预加载计数
$categories = Category::withCount('articles')->get();
// 分块处理大量数据
Article::chunk(1000, function ($articles) {
foreach ($articles as $article) {
// 处理文章
}
});
// 使用游标分页(大数据集)
$articles = Article::orderBy('id')->cursorPaginate(100);
缓存优化
php
class Article extends Model
{
/**
* 缓存热门文章
*/
public static function getPopular(int $limit = 10): Collection
{
return Cache::remember('articles:popular', 3600, function () use ($limit) {
return static::where('status', true)
->orderBy('views', 'desc')
->limit($limit)
->get();
});
}
/**
* 缓存文章详情
*/
public static function getCached(int $id): ?self
{
return Cache::remember("article:{$id}", 3600, function () use ($id) {
return static::with(['category', 'user', 'tags'])->find($id);
});
}
/**
* 清除相关缓存
*/
protected static function booted(): void
{
static::saved(function ($article) {
Cache::forget("article:{$article->id}");
Cache::forget('articles:popular');
Cache::tags(['articles'])->flush();
});
}
}
最佳实践
1. 模型组织
php
// 按功能分组模型
Models/
├── User/
│ ├── User.php
│ ├── UserProfile.php
│ └── UserSetting.php
├── Content/
│ ├── Article.php
│ ├── Category.php
│ └── Tag.php
└── System/
├── Setting.php
├── Log.php
└── Permission.php
2. 关联优化
php
// 使用预加载避免 N+1 查询
$articles = Article::with([
'category:id,name',
'user:id,name,avatar',
'tags:id,name,color'
])->get();
// 条件预加载
$articles = Article::with([
'comments' => function ($query) {
$query->where('status', 'approved')
->latest()
->limit(5);
}
])->get();
3. 数据验证
php
class Article extends Model
{
/**
* 验证规则
*/
public static function validationRules(): array
{
return [
'title' => 'required|string|max:255',
'content' => 'required|string|min:10',
'category_id' => 'required|integer|exists:categories,id',
'tags' => 'nullable|array|max:10',
'tags.*' => 'string|max:50'
];
}
/**
* 保存前验证
*/
protected static function booted(): void
{
static::saving(function ($article) {
$validator = validator($article->toArray(), static::validationRules());
if ($validator->fails()) {
throw new \Illuminate\Validation\ValidationException($validator);
}
});
}
}
总结
模型开发的关键要点:
- 自动迁移 - 使用注解定义表结构,自动同步
- 关联关系 - 正确定义模型间的关联关系
- 查询优化 - 使用预加载、作用域等优化查询
- 事件处理 - 利用模型事件处理业务逻辑
- 数据转换 - 使用访问器、修改器和类型转换
- 性能优化 - 合理使用缓存和查询优化技巧
- 代码组织 - 保持模型简洁,复杂逻辑移至服务层
遵循这些实践,将帮助你构建高效、可维护的数据模型。