Yii是PHP生态里一款高性能的MVC框架,它内置的Active Record实现一直被认为是同类框架中做得比较完善的。与此同时,网站前端性能越来越依赖静态资源的分发速度,CDN的接入几乎成了标配。这篇文章把这两块内容放在一起讲:一方面梳理Yii ORM的日常用法和进阶技巧,另一方面说说在Yii项目里接入CDN的几种常见方式,以及二者配合时需要注意的细节。

一、Yii ORM基础:Active Record模型怎么用
Active Record是Yii提供的ORM核心,它的思路是让数据库表中的每一行对应一个PHP对象,操作对象就等于操作数据。要使用它,首先需要定义一个继承自yii\db\ActiveRecord的模型类,并通过tableName()方法指定关联的数据表。
<?php
namespace app\models;
use yii\db\ActiveRecord;
class User extends ActiveRecord
{
// 对应数据表 user
public static function tableName()
{
return '{{%user}}';
}
// 字段验证规则
public function rules()
{
return [
[['username', 'email'], 'required'],
['email', 'email'],
['username', 'string', 'length' => [3, 20]],
];
}
}上面代码中的{{%user}}写法会自动拼接表前缀,这是Yii推荐的方式,多项目共用数据库时尤其有用。rules()方法定义的验证规则会在调用save()时自动触发,验证不通过会直接返回false,数据不会落库。
基本的增删改查操作非常直观:新增用new User()加save(),查询用User::find(),删除用delete()。下面是一组典型示例。
<?php
// 新增记录
$user = new User();
$user->username = 'zhangsan';
$user->email = 'zhangsan@ipipp.com';
$user->save();
// 查询单条
$user = User::find()->where(['username' => 'zhangsan'])->one();
// 查询多条并按id倒序
$list = User::find()->where(['status' => 1])
->orderBy('id DESC')
->limit(10)
->all();
// 更新
$user->email = 'new@ipipp.com';
$user->update();
// 删除
$user->delete();需要强调一点:save()失败时不要急着看SQL,先用$user->getErrors()把验证错误打出来,很多所谓的“保存失败”其实是规则校验没通过,这也是新手最常踩的坑。
二、进阶查询:Query Builder与关联查询
Active Record适合单表为主的业务,但涉及复杂统计、多表join时,直接用yii\db\Query构建器更轻量。它不会实例化模型对象,内存占用和执行开销都更小。两种方式可以混用,关键看场景。
<?php
use yii\db\Query;
// 统计每个状态下的用户数
$rows = (new Query())
->select(['status', 'COUNT(*) AS cnt'])
->from('{{%user}}')
->groupBy('status')
->all();
// 关联查询:查询用户及其订单
$rows = (new Query())
->select(['u.username', 'o.amount'])
->from(['u' => '{{%user}}'])
->innerJoin(['o' => '{{%order}}'], 'u.id = o.user_id')
->where(['u.status' => 1])
->all();如果希望保留面向对象的写法,Yii也提供了关联查询支持。在模型中定义一个以get开头的方法,返回hasOne或hasMany的对象,之后就可以像访问属性一样访问关联数据。
<?php
class User extends ActiveRecord
{
public function getOrders()
{
// 一对多关联
return $this->hasMany(Order::class, ['user_id' => 'id']);
}
}
// 使用:懒加载
$user = User::findOne(10);
$count = count($user->orders);
// 使用:预加载,避免N+1查询
$users = User::find()->with('orders')->all();这里必须提一下臭名昭著的N+1查询问题。如果在循环里逐个访问$user->orders,每个用户都会触发一次额外SQL。解决办法就是上面演示的with()预加载,它会把关联数据一次性查出来,列表页数据量大时性能差距非常明显,实测上千条记录能从几百次SQL降到两次。
写操作涉及多条语句时,记得用事务包起来。Yii的事务用法很简洁:
<?php
$transaction = Yii::$app->db->beginTransaction();
try {
$order->save();
$stock->updateCounters(['num' => -1]);
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}三、CDN接入:让静态资源跑得更快
后端查询优化得再好,页面加载慢一样白搭。Yii默认通过资源包(AssetBundle)管理CSS和JS,接入CDN的正确姿势就是从资源包入手。创建一个自定义资源包,把baseUrl指向CDN地址即可。
<?php
namespace app\assets;
use yii\web\AssetBundle;
class CdnAsset extends AssetBundle
{
// CDN上的资源地址
public $baseUrl = 'https://cdn.ipipp.com/static/';
public $css = ['css/site.css'];
public $js = ['js/app.js'];
// 使用内容哈希作为版本号,便于CDN缓存刷新
public function init()
{
parent::init();
$this->css[] = 'css/site.css?v=' . filemtime(\Yii::getAlias('@webroot/css/site.css'));
}
}另一种更省事的做法是修改应用配置中的assetManager,通过assetMap把本地资源映射到CDN地址,框架自带的核心资源(比如jQuery、Bootstrap)也能一并替换:
<?php
// config/web.php 中配置
'components' => [
'assetManager' => [
'bundle' => false, // 禁用资源合并发布,直接走CDN
'assetMap' => [
'jquery.js' => 'https://cdn.ipipp.com/jquery/jquery-3.7.1.min.js',
],
],
],接入CDN有几个细节容易忽略。一是缓存刷新问题,静态文件更新后如果CDN节点还缓存着旧版本,用户看到的就永远是旧样式,推荐用文件哈希或时间戳做版本号;二是回源配置,源站建议开启gzip并设置合理的Cache-Control头,回源带宽能省不少;三是上传类资源(用户头像、附件等)建议直接存对象存储再挂CDN,不要和代码静态资源混在一起管理。
四、性能优化与常见误区
把ORM和CDN都用上之后,还有一些整体优化思路值得注意。首先是查询层面的惰性加载字段,列表页只需要几个字段时,用select()明确指定,避免SELECT *把大文本字段也拉出来;其次是缓存,Yii内置了数据缓存组件,热点查询结果完全可以缓存起来:
<?php
// 先查缓存,没有再查数据库
$data = Yii::$app->cache->getOrSet('hot_users', function () {
return User::find()
->select(['id', 'username'])
->where(['status' => 1])
->orderBy('created_at DESC')
->limit(50)
->asArray()
->all();
}, 300); // 缓存5分钟asArray()也是个实用技巧,它让查询直接返回数组而不是模型对象,纯展示型的接口用它能把内存占用和反序列化开销降下来一大截。
常见误区方面,有三点值得提醒。第一,不要在循环里调用save()批量插入,一条条插入会产生大量单条SQL,应该用batchInsert()一次性写入;第二,Active Record不是万能的,报表类复杂查询硬套AR只会让代码更难维护,这种场景老老实实用Query Builder或者原生SQL;第三,CDN只对静态资源有效,接口响应慢还得从SQL索引、缓存、连接池这些后端手段入手,别指望接了CDN就万事大吉。
总结一下,Yii的ORM体系在易用性和性能之间取得了不错的平衡,掌握Active Record、Query Builder的适用边界,配合预加载、事务和缓存,后端数据层的性能就有保障;再通过资源包把静态资源托管到CDN,整站的加载体验会有肉眼可见的提升。建议在自己的项目里按这套思路逐步改造,边做边用Yii的调试工具栏观察SQL数量和执行时间,效果会更直观。