TUTORIAL

数据库 ORM 源码解读

数据库 ORM 源码解读

数据库 ORM 源码解读

原生 SQL 写起来又臭又长,还容易拼接出错、引入注入漏洞。ORM(对象关系映射)把数据库表映射成类,把行映射成对象,让开发者用操作对象的方式操作数据库——$user->save() 而非 INSERT INTO users ...。几乎所有现代建站框架都内置 ORM,但它是怎么把对象操作翻译成 SQL 的?本篇带你拆解 ORM 的核心原理。

一、查询构造器:链式调用的秘密

ORM 最直观的功能是链式查询:where('id', 1).orderBy('time').get()。这种写法优雅,但底层如何工作?秘密在于每个链式方法都返回当前查询对象自身(this),把条件暂存在对象属性里,直到调用 get()first() 才真正执行 SQL。

// 查询构造器核心实现
class QueryBuilder {
  constructor(table, db) {
    this.table = table;
    this.db = db;
    this.wheres = [];     // where 条件暂存
    this.orders = [];     // orderBy 暂存
    this.limitNum = null; // limit 暂存
  }

  // 链式方法:返回 this 实现链式调用
  where(column, operator, value) {
    // 兼容 where('id', 1) 的简写形式
    if (value === undefined) { value = operator; operator = '='; }
    this.wheres.push({ column, operator, value, type: 'basic' });
    return this;  // 关键:返回自身
  }

  orWhere(column, operator, value) {
    if (value === undefined) { value = operator; operator = '='; }
    this.wheres.push({ column, operator, value, type: 'or' });
    return this;
  }

  orderBy(column, direction = 'ASC') {
    this.orders.push({ column, direction });
    return this;
  }

  limit(num) {
    this.limitNum = num;
    return this;
  }

  // 终结方法:把暂存条件拼成 SQL 并执行
  async get() {
    let sql = `SELECT * FROM ${this.table}`;
    const params = [];

    // 拼接 WHERE
    if (this.wheres.length) {
      const clauses = this.wheres.map(w => {
        params.push(w.value);
        const link = w.type === 'or' ? 'OR' : 'AND';
        return `${link} ${w.column} ${w.operator} ?`;
      });
      sql += ' WHERE ' + clauses.join(' ').replace(/^(AND|OR)\s/, '');
    }

    // 拼接 ORDER BY
    if (this.orders.length) {
      sql += ' ORDER BY ' + this.orders.map(o => `${o.column} ${o.direction}`).join(', ');
    }

    // 拼接 LIMIT
    if (this.limitNum) sql += ` LIMIT ${this.limitNum}`;

    return this.db.query(sql, params);  // 参数绑定防注入
  }
}

// 使用:链式调用,优雅直观
const users = await db.table('users')
  .where('status', 1)
  .where('age', '>', 18)
  .orderBy('created_at', 'DESC')
  .limit(10)
  .get();
// 生成:SELECT * FROM users WHERE status = ? AND age > ? ORDER BY created_at DESC LIMIT 10

注意所有值都用 ? 占位符并通过参数绑定传入,这是防 SQL 注入的根本措施——参数不会被视为 SQL 语法的一部分。尧图项目里严禁用字符串拼接构造 SQL,即便只是内部管理后台也一样,因为注入漏洞往往出现在"以为不会有问题"的地方。

二、模型与对象映射

查询构造器返回的还是原始数据(关联数组),而 ORM 的精髓在于"对象映射"——把一行数据封装成一个模型对象,对象上能挂方法(如格式化日期、计算属性)。模型类继承基类,自动获得增删改查能力,开发者只需定义表名和字段。

// 模型基类
class Model {
  // 子类覆盖
  static table = '';
  static fillable = [];   // 允许批量赋值的字段(防 mass assignment)

  constructor(attributes = {}) {
    this.attributes = attributes;
  }

  // 魔术方法:$user->name 自动取 attributes.name
  get(key) { return this.attributes[key]; }
  set(key, value) { this.attributes[key] = value; }

  // 新增:把当前对象插入数据库
  async save() {
    if (this.attributes.id) {
      // 有主键,执行 UPDATE
      const sets = [];
      const params = [];
      for (const key of Object.keys(this.attributes)) {
        if (key === 'id') continue;
        if (!this.constructor.fillable.includes(key)) continue; // 字段白名单
        sets.push(`${key} = ?`);
        params.push(this.attributes[key]);
      }
      params.push(this.attributes.id);
      const sql = `UPDATE ${this.constructor.table} SET ${sets.join(', ')} WHERE id = ?`;
      return db.query(sql, params);
    } else {
      // 无主键,执行 INSERT
      const cols = this.constructor.fillable.filter(c => this.attributes[c] !== undefined);
      const sql = `INSERT INTO ${this.constructor.table} (${cols.join(',')}) VALUES (${cols.map(() => '?').join(',')})`;
      const params = cols.map(c => this.attributes[c]);
      const result = await db.query(sql, params);
      this.attributes.id = result.insertId;  // 回填自增ID
      return result;
    }
  }

  // 静态查询:User.where(...).first()
  static where(...args) {
    return new QueryBuilder(this.table, db).where(...args);
  }
  static async find(id) {
    const rows = await new QueryBuilder(this.table, db).where('id', id).get();
    return rows[0] ? new this(rows[0]) : null;  // 封装成模型对象
  }
}

// 业务模型:只需声明表名和可填充字段
class Article extends Model {
  static table = 'articles';
  static fillable = ['title', 'content', 'author_id', 'status', 'publish_at'];

  // 模型方法:格式化发布日期
  getPublishDate() {
    return new Date(this.get('publish_at')).toLocaleDateString('zh-CN');
  }
}

// 使用:操作对象就是操作数据库
const article = new Article({
  title: '尧图建站教程',
  content: 'ORM 让数据库操作更优雅',
  author_id: 1,
  status: 1
});
await article.save();  // INSERT
article.set('title', '修改后的标题');
await article.save();  // UPDATE(因为有 id 了)
const found = await Article.find(1);
console.log(found.getPublishDate());  // 用模型方法

fillable 字段白名单是个重要安全机制——防止"批量赋值漏洞"。如果用户提交表单时偷偷加上 is_admin=1,没有白名单的话管理员权限就被非法提升。白名单只允许预期字段被写入,多余字段直接丢弃。这是尧图所有模型必配的防护。

三、关联关系与懒加载

真实业务里表与表有关联:一篇文章属于一个作者(belongs to)、一个作者有多篇文章(has many)。ORM 用关联关系方法描述这些联系,调用时自动发起额外查询。这里有个性能陷阱——N+1 查询问题:取出 10 篇文章,再循环取每篇的作者,就是 1+10=11 次查询。解法是"预加载"(eager loading),一次 JOIN 把关联数据全取回来。

// 关联关系定义
class Article extends Model {
  static table = 'articles';

  // 属于一个作者(articles.author_id → users.id)
  author() {
    return this.hasOne(User, 'id', 'author_id');
  }
}

class User extends Model {
  static table = 'users';

  // 拥有多篇文章
  articles() {
    return this.hasMany(Article, 'author_id', 'id');
  }
}

// 懒加载:用到时才查询(N+1 问题)
const articles = await Article.where('status', 1).limit(10).get();
for (const a of articles) {
  const author = await a.author();  // 每篇都查一次 → 11次查询
  console.log(a.get('title'), author.get('name'));
}

// 预加载:一次性带出关联数据(2次查询解决)
const articles = await Article.with('author')   // 告诉 ORM 顺带取作者
  .where('status', 1)
  .limit(10)
  .get();
// 第1次:SELECT * FROM articles WHERE status=1 LIMIT 10
// 第2次:SELECT * FROM users WHERE id IN (1,2,3,5,8)  ← 收集所有 author_id 一次查
for (const a of articles) {
  const author = a.relations['author'];  // 已在内存,无需查询
  console.log(a.get('title'), author.get('name'));
}

预加载的关键是"收集所有关联 ID,用 IN 一次查回,再按 ID 分配到各对象"。这个优化能把 N+1 次查询降到 2 次,列表页性能提升立竿见影。尧图在代码评审时会把 N+1 当作代码异味——任何循环里出现关联查询都要检查是否用了预加载。理解了查询构造器、模型映射、关联预加载这三大件,你就能读懂任意 ORM 源码,也能在框架 ORM 不够用时自己写查询构造器扩展。

返回教程列表