npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tyno-orm

v1.0.3

Published

orm mysql

Readme

tyno-orm

轻量级 TypeScript ORM,支持 MySQL,设计参考 ThinkPHP 的链式操作风格。

快速开始

npm install tyno-orm mysql2

配置数据库

import { Db } from 'tyno-orm';

Db.setConfig({
  default: {
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: '',
    database: 'my_db',
    prefix: 'ty_',    // 可选:表前缀
  }
});

定义模型

import { Model } from 'tyno-orm';

class User extends Model {
  static table = 'user';            // 表名(不设置则自动从类名驼峰转下划线)
  static pk = 'id';                 // 主键,默认 'id'
  static autoWriteTimestamp = true; // 自动写入时间戳
  static createTime = 'create_time';
  static updateTime = 'update_time';
}

核心功能

1. 模型自动表名

子类不设置 table 时,自动将类名从驼峰转为下划线作为表名:

| 类名 | 自动表名 | |------|---------| | User | user | | UserProfile | user_profile | | OrderItem | order_item | | BlogPostContent | blog_post_content |

class UserProfile extends Model {}
// table 自动为 'user_profile',无需手动指定

2. 查询构造器(链式调用)

所有条件方法支持链式调用,返回 Query 实例,终止方法返回实际数据。

WHERE 条件

// 基础条件 (field, value) — 省略操作符默认 =
User.where('status', 1)

// 三参数 (field, operator, value)
User.where('age', '>', 18)

// 对象形式 — 自动展开为 AND 条件
User.where({ status: 1, age: 20 })

// 闭包子查询 — 生成 ( ... ) 嵌套
User.where(q => q.where('status', 1).whereOr('age', '>', 18))
// SQL: SELECT * FROM user WHERE (status = 1 OR age > 18)
// OR 条件
User.where('status', 1).whereOr('name', '张三')

// IN / NOT IN
User.whereIn('id', [1, 2, 3])
User.whereNotIn('id', [4, 5, 6])

// BETWEEN / NOT BETWEEN
User.whereBetween('age', 18, 30)
User.whereNotBetween('age', 18, 30)

// NULL / NOT NULL
User.whereNull('deleted_at')
User.whereNotNull('update_time')

// 原始 SQL 表达式
User.whereRaw('age > 18 AND status IN (1, 2)')

字段选择

User.field('id', 'name', 'email')    // 多参数分别传
User.field('id, name')               // 单字符串也支持

排序

User.orderBy('create_time', 'desc')  // 默认 asc,可指定 desc
User.orderBy('age').orderBy('create_time', 'desc')  // 多字段排序
User.orderRaw('FIELD(status, 1, 2, 3)')  // 原始排序表达式

分页与数量限制

User.limit(10)                // LIMIT
User.limit(10).offset(20)     // LIMIT + OFFSET
User.page(2, 15)              // 快捷分页:page(页码, 每页条数)

// 分页查询:一次执行 count + select,返回总数和当前页数据
const { total, list } = await User.paginate(1, 10)

// 带条件的分页查询
const { total, list } = await User
  .where('status', 1)
  .orderBy('id', 'desc')
  .paginate(1, 10)

GROUP BY / HAVING

User.field('status, COUNT(*) as count')
    .groupBy('status')
    .having('count', '>', 10)

User.field('status, COUNT(*) as count')
    .groupBy('status')
    .havingRaw('COUNT(*) > 10')

JOIN

User.join('orders', 'user.id = orders.user_id')          // INNER JOIN
User.leftJoin('orders', 'user.id = orders.user_id')       // LEFT JOIN
User.rightJoin('orders', 'user.id = orders.user_id')      // RIGHT JOIN
User.join('orders o', 'user.id = o.user_id')              // 带别名

行锁

User.where('id', 1).lock('for update')   // SELECT ... FOR UPDATE
User.where('id', 1).lock('share')        // SELECT ... FOR SHARE

UNION

const q1 = User.field('id, name');
const q2 = User.field('id, name').where('status', 1);
q1.union(q2)        // UNION
q1.union(q2, 'union all')  // UNION ALL

SQL 调试

// fetchSql 模式:不执行 SQL,只输出
User.where('status', 1).fetchSql().select()
// 控制台打印: SELECT * FROM `ty_user` WHERE `status` = 1

// 获取生成的 SQL 和绑定参数
const q = User.where('status', 1).where('age', '>', 18)
console.log(q.toSql())       // 已代入参数的可读 SQL
console.log(q.getBindings()) // 绑定参数数组

3. 终止操作

所有终止方法返回 Promise,执行实际数据库操作。

// 查询多条记录 → 返回 Model 实例数组
const users = await User.where('status', 1).limit(10).select()

// 查询单条记录 → 返回 Model 实例或 null
const user = await User.where('name', '张三').find()
const user = await User.find(1)          // 按主键查询

// 聚合
const total = await User.count()           // COUNT(*)
const count = await User.where('status', 1).count()  // 条件计数
const maxAge = await User.max('age')       // MAX
const minAge = await User.min('age')       // MIN
const sum = await User.sum('score')        // SUM
const avg = await User.avg('score')        // AVG

// 分页查询 → 一次返回 total + list
const { total: pageTotal, list } = await User.field('id, name').orderBy('id', 'asc').paginate(1, 10)

4. 写入操作

// 插入 → 返回影响行数
await User.insert({ name: '李四', age: 28, status: 1 })

// 插入并返回自增 ID
const id = await User.insertGetId({ name: '李四', age: 28, status: 1 })

// 批量插入
await User.insertAll([
  { name: '张三', age: 20 },
  { name: '李四', age: 25 },
])

// 更新 → 需提前设置 WHERE 条件
await User.where('id', 1).update({ name: '张三丰' })
// 或使用模型静态方法直接更新
await User.update({ name: '张三丰' }, { id: 1 })

// 删除
await User.where('id', 1).delete()
await User.destroy(1)   // 按主键删除

// 创建实例(工厂方法,返回 Proxy 包装的 Model 实例)
const newUser = User.create({ name: '王五', age: 22, status: 1 })
await newUser.save()    // 直接 INSERT

// new + .属性名 = 赋值 + .save() 模式
const user = new User()
user.name = '测试'
user.age = 25
user.status = 1
await user.save()       // INSERT,自动回填主键

user.name = '测试更新'
await user.save()       // UPDATE,_exists 已为 true

5. 模型实例操作

select()find() 返回的是 Model 实例,提供丰富的实例方法和语法糖。

5.1 属性访问:.name 语法

Model 实例通过 Proxy 代理,支持像访问普通对象属性一样直接读写字段,同时自动触发获取器/设置器:

const user = await User.find(1)

// 读取:直接 .属性名 访问,自动走 getAttribute
user.name           // 等价于 user.getAttribute('name')
user.id             // 1
user.status         // 获取器 getStatusAttr 会被触发

// 写入:直接赋值,自动走 setAttribute
user.name = '张三'  // 等价于 user.setAttribute('name', '张三')
user.age = 28       // 设置器 setAgeAttr 会被触发

Proxy 访问优先级:

  1. 如果属性存在于 _attributes 字典中 → 走 getAttribute()(触发获取器)
  2. 如果属性是已预加载的关联名 → 返回关联数据
  3. 否则 → 走原生 Reflect.get(访问实例自身的属性和方法)
// _attributes 中有 'name' → getAttribute('name')
user.name

// 已预加载的关联 → 返回关联模型数据
user.profile    // User.with(['profile']) 预加载后可直接 .profile 访问

// 调用实例方法 → 正常 Reflect.get
user.save()
user.toJSON()

5.2 get / setAttribute 方法

除了 .name 语法糖,也可以显式调用方法:

// 设置单个属性(会触发 setXxxAttr 设置器)
user.setAttribute('name', '新名字')
user.setAttribute('age', 28)

// 批量填充(会逐字段触发设置器)
user.fill({ name: '张三', age: 28, status: 1 })

// 获取单个属性(会触发 getXxxAttr 获取器)
user.get('name')
user.getAttribute('name')

5.3 save() — 自动判断 INSERT 还是 UPDATE

save() 是 Model 最核心的实例方法。它根据实例的 _exists 标记自动判断执行插入还是更新:

// 场景 1:新建实例 → INSERT
const user = new User({ name: '张三', age: 20, status: 1 })
// _exists = false(没有主键值,且 constructor data 中无 pk)
await user.save()   // 执行 INSERT,自动回填主键到实例

// 场景 2:从数据库查出的实例 → UPDATE
const user = await User.find(1)
// _exists = true(options.exists = true)
user.name = '张三丰'
await user.save()   // 执行 UPDATE WHERE id = 1

save() 内部流程:

save()
  ├── _exists === false(新建)
  │     ├── convertToDb()          收集 _attributes 所有数据
  │     ├── processTimestamps()    若开启 autoWriteTimestamp,自动写入时间戳
  │     └── insertGetId()          执行 INSERT,拿到自增 ID
  │           └── 回填 this._attributes[pk] = 新ID
  │
  ├── _exists === true(已有记录)
  │     ├── convertToDb()          收集 _attributes 所有数据
  │     └── update()              执行 UPDATE WHERE pk = ?
  │
  └── syncOriginal()               将当前数据同步为 _original,isDirty() 重置

_exists 判断规则(构造函数中):

// 从数据库查询 → exists = true
const user = await User.find(1)
// constructor(data, { exists: true }) → _exists = true

// 手动 new,data 中有主键值 → 视情况
const user = new User({ id: 1, name: '张三' })
// data['id'] 存在 → 隐式标记 _exists = true(认为从数据库来的)
// 此时 save() = UPDATE

const user = new User({ name: '张三' })
// data 中无主键 → _exists = false
// 此时 save() = INSERT

// 通过 options 显式控制
const user = new User({ id: 1, name: '张三' }, { exists: false })
// 强制 _exists = false,save() = INSERT

5.4 delete() — 删除实例

const user = await User.find(1)
await user.delete()
// 执行 DELETE FROM user WHERE id = 1
// _exists 标记置为 false

// 不存在的实例调用 delete() 会抛错
const u = new User({ name: 'test' })
await u.delete()   // Error: Model does not exist

5.5 数据变更检测

const user = await User.find(1)

user.getOriginal('name')  // '张三' — 获取从数据库查出时的原始值
user.getOriginal()        // { id: 1, name: '张三', ... } — 全部原始值

user.name = '张三丰'
user.isDirty()            // true — 当前数据和原始数据不一致

await user.save()         // 保存后 syncOriginal() 被调用
user.isDirty()            // false — 重新同步

5.6 序列化

const user = await User.find(1)
console.log(user.toJSON())
// → { id: 1, name: '张三', ... }

console.log(JSON.stringify(user))
// toJSON() 在 JSON.stringify 中自动调用

6. 获取器 / 设置器

定义 getXxxAttr / setXxxAttr 方法,自动在读写属性时调用:

class User extends Model {
  // 读取 full_name 时自动拼接
  getFullNameAttr(value: string, data: Record<string, any>) {
    return `${data.first_name} ${data.last_name}`
  }

  // 写入 password 时自动哈希
  setPasswordAttr(value: string) {
    return hashSync(value, 10)
  }
}

const user = await User.find(1)
console.log(user.full_name)  // 自动触发 getFullNameAttr

7. 序列化控制

class User extends Model {
  static hidden = ['password', 'secret']    // 隐藏字段
  static visible = ['id', 'name', 'email']  // 仅可见字段
  static append = ['full_name', 'role']     // 追加计算属性
}

user.toJSON()
// hidden 模式下排除 password, secret
// visible 模式下只包含 id, name, email
// append 追加 full_name, role

8. 自动时间戳

class User extends Model {
  static autoWriteTimestamp = true
  static createTime = 'create_time'
  static updateTime = 'update_time'
}

// insert 时自动写入 create_time 和 update_time
// update 时自动更新 update_time
await User.insert({ name: '张三' })
await User.where('id', 1).update({ name: '张三丰' })

9. 关联预加载

四种关联类型通过 Model 基类的实例方法定义,无需外部 import 关联类。

9.1 关联类型概览

| 关联类型 | 关系 | 外键位置 | 查询结果 | |---------|------|---------|---------| | hasOne() | 一对一 | 关联表 | 单条 Model 或 null | | hasMany() | 一对多 | 关联表 | Model 数组 | | belongsTo() | 多对一 | 当前表 | 单条 Model 或 null | | belongsToMany() | 多对多 | 中间表 | Model 数组 |

9.2 hasOne() — 一对一

假设 User 有一个 Profile:

user 表:id, name, ...
profile 表:id, user_id, avatar, bio, ...   ← user_id 是外键
class Profile extends Model {
  static table = 'profile'
  static pk = 'id'
}

class User extends Model {
  static table = 'user'

  profile() {
    return this.hasOne(Profile, 'user_id', 'id')
    //                 关联模型  外键(关联表)  主键(当前表)
  }
}
// 预加载后可直接 .profile 访问
const user = await User.with(['profile']).find(1)
console.log(user.profile?.avatar)

// 或直接获取关联数据
const profile = await new User({ id: 1 }).profile().getResults()
// → SELECT * FROM `profile` WHERE `user_id` = 1 LIMIT 1

9.3 hasMany() — 一对多

假设 User 有多条 Order:

user 表:id, name, ...
order 表:id, user_id, order_no, total, ...   ← user_id 是外键
class Order extends Model {
  static table = 'order'
}

class User extends Model {
  static table = 'user'

  orders() {
    return this.hasMany(Order, 'user_id', 'id')
    //                  关联模型  外键(关联表)  主键(当前表)
  }
}
const user = await User.with(['orders']).find(1)
console.log(user.orders)              // Order 实例数组
console.log(user.orders[0].order_no)  // 第一笔订单号

9.4 belongsTo() — 多对一

与 hasOne/hasMany 相反,外键在当前表上。假设 Order 属于一个 User:

order 表:id, user_id, order_no, ...   ← user_id 是外键(在当前表)
user 表:id, name, ...
class Order extends Model {
  static table = 'order'

  user() {
    return this.belongsTo(User, 'user_id', 'id')
    //                     关联模型  外键(当前表)  主键(关联表)
  }
}
const order = await Order.with(['user']).find(1)
console.log(order.user)       // 关联的 User 实例
console.log(order.user.name)

9.5 belongsToMany() — 多对多

通过中间表关联。假设 User 和 Role 多对多:

user       表:id, name, ...
role       表:id, name, ...
user_role  表:user_id, role_id    ← 中间表
class Role extends Model {
  static table = 'role'
}

class User extends Model {
  static table = 'user'

  roles() {
    return this.belongsToMany(
      Role,        // 关联模型类
      'user_role', // 中间表名
      'role_id',   // 中间表指向关联模型的键
      'user_id',   // 中间表指向当前模型的键
      'id',        // 当前模型主键
      'id'         // 关联模型主键
    )
  }
}
// BelongsToMany 生成的 SQL:
// SELECT `role`.*
// FROM `role`
// INNER JOIN `user_role` ON `user_role`.`role_id` = `role`.`id`
// WHERE `user_role`.`user_id` = ?

const user = await User.with(['roles']).find(1)
console.log(user.roles)           // Role 实例数组

9.6 预加载 with() — 避免 N+1 查询

with() 方法使用 eagerLoad 批量预加载,一次查询加载所有关联数据,避免循环查询:

// ❌ N+1 查询:每条 User 各查一次 Profile
const users = await User.select()
for (const u of users) {
  const profile = await u.profile().getResults() // 每条触发一次 SQL
}

// ✅ 预加载:2 条 SQL 搞定(1 条用户 + 1 条关联)
const users = await User.with(['profile', 'orders']).select()
// 内部调用 HasOne.eagerLoad() 和 HasMany.eagerLoad()
// 将关联数据注入到每个 user 实例的 _relations 中
// 之后直接 .profile / .orders 访问即可

预加载内部流程(以 User.with(['profile']).select() 为例):

User.with(['profile']).select()
  ├── 1. query() 取出用户列表 → users: Model[]
  ├── 2. HasOne.eagerLoad(users, Profile, 'user_id', 'id')
  │       ├── 收集所有 users 的 id → [1, 2, 3, ...]
  │       ├── 执行:SELECT * FROM profile WHERE user_id IN (1, 2, 3, ...)
  │       └── 按 user_id 分组注入到各 user._relations['profile']
  └── 3. 用户可通过 user.profile 访问(Proxy 读取 _relations)

9.7 延迟加载

不通过 with(),直接调用关联方法获取数据(每次调用都执行 SQL):

const user = new User({ id: 1 })

// hasOne → 返回单条
const profile = await user.profile().getResults()

// hasMany → 返回数组
const orders = await user.orders().getResults()

// belongsTo → 返回单条
const order = new Order({ id: 1, user_id: 5 })
const owner = await order.user().getResults()

9.8 Model 关联方法签名

class Model {
  // 一对一:关联表有外键指向当前表
  hasOne(related: typeof Model, foreignKey: string, localKey: string): HasOne

  // 一对多:关联表有外键指向当前表
  hasMany(related: typeof Model, foreignKey: string, localKey: string): HasMany

  // 多对一:当前表有外键指向关联表
  belongsTo(related: typeof Model, foreignKey: string, ownerKey: string): BelongsTo

  // 多对多:通过中间表关联
  belongsToMany(
    related: typeof Model,
    pivotTable: string,       // 中间表名
    relatedPivotKey: string,  // 中间表→关联模型的键
    parentPivotKey: string,   // 中间表→当前模型的键
    parentKey: string,        // 当前模型主键
    relatedKey: string        // 关联模型主键
  ): BelongsToMany
}

10. 模型事件

通过 EventManager 为模型挂载生命周期钩子,在 CRUD 操作的前后插入自定义逻辑。

10.1 事件类型

| 事件 | 触发时机 | |------|---------| | beforeInsert | 插入前 | | afterInsert | 插入后 | | beforeUpdate | 更新前 | | afterUpdate | 更新后 | | beforeWrite | insert 或 update 前都会触发 | | afterWrite | insert 或 update 后都会触发 | | beforeDelete | 删除前 | | afterDelete | 删除后 | | beforeFind | 查询前 | | afterFind | 查询后 |

10.2 注册监听

每个模型类持有一个独立的 EventManager

import { EventManager } from 'tyno-orm'

class User extends Model {
  static table = 'user'
  static events = new EventManager()
}

// 注册事件监听
User.events.on('beforeInsert', (data: Record<string, any>) => {
  console.log('即将插入:', data)
})

User.events.on('afterInsert', (id: number | string) => {
  console.log('插入成功, id =', id)
})

User.events.on('beforeUpdate', (data: Record<string, any>) => {
  data.updated_at = new Date().toISOString() // 修改即将写入的数据
})

User.events.on('afterDelete', (id: any) => {
  console.log('已删除记录:', id)
})

User.events.on('beforeFind', () => {
  console.log('执行查询前')
})

User.events.on('afterFind', (results: any[]) => {
  console.log('查询完成,结果数:', results.length)
})

10.3 beforeWrite / afterWrite

通用型事件,不区分 insert 和 update,只要发生写入就触发:

// insert 时会触发:beforeWrite → beforeInsert → [SQL] → afterInsert → afterWrite
// update 时会触发:beforeWrite → beforeUpdate → [SQL] → afterUpdate → afterWrite

User.events.on('beforeWrite', (data: Record<string, any>) => {
  // insert 和 update 都会经过这里
  data.operator = 'system'
})

User.events.on('afterWrite', () => {
  console.log('写入完成')
})

10.4 移除监听

const handler = (data: Record<string, any>) => {
  console.log(data)
}

User.events.on('beforeInsert', handler)

// 不再需要时移除
User.events.off('beforeInsert', handler)

// 清空所有事件
User.events.clear()

10.5 事件触发流程

User.insert({ name: '张三' })
  ├── beforeWrite(data)      ← 通用写入前,可修改 data
  ├── beforeInsert(data)     ← 插入前,可修改 data
  ├── 执行 INSERT 语句
  ├── afterInsert(id)        ← 插入后,参数为自增 ID
  └── afterWrite()           ← 通用写入后

User.where('id', 1).update({ name: '张三丰' })
  ├── beforeWrite(data)      ← data = { name: '张三丰' }
  ├── beforeUpdate(data)
  ├── 执行 UPDATE 语句
  ├── afterUpdate()
  └── afterWrite()

User.destroy(1)
  ├── beforeDelete()
  ├── 执行 DELETE 语句
  └── afterDelete(id)

User.select()
  ├── beforeFind()
  ├── 执行 SELECT 语句
  └── afterFind(results)     ← 参数为查询结果数组

10.6 完整示例:日志审计

class Log extends Model {
  static table = 'audit_log'
}

class User extends Model {
  static table = 'user'
  static events = new EventManager()

  static boot() {
    this.events.on('beforeInsert', (data: Record<string, any>) => {
      data.password = hashSync(data.password, 10)
    })

    this.events.on('afterInsert', async (id: number | string) => {
      await Log.insert({
        action: 'user.create',
        target_id: id,
        created_at: new Date().toISOString()
      })
    })

    this.events.on('afterUpdate', async () => {
      await Log.insert({
        action: 'user.update',
        target_id: 0, // ...从上下文获取
        created_at: new Date().toISOString()
      })
    })

    this.events.on('afterDelete', async (id: any) => {
      await Log.insert({
        action: 'user.delete',
        target_id: id,
        created_at: new Date().toISOString()
      })
    })
  }
}

// 应用启动时调用
User.boot()

10.7 EventManager API

class EventManager {
  // 注册事件监听(支持同一事件多个回调)
  on(event: ModelEvent, callback: Function): void

  // 移除指定事件的某个回调
  off(event: ModelEvent, callback: Function): void

  // 触发事件(内部容错:单个回调异常不影响其他回调)
  trigger(event: ModelEvent, ...args: any[]): void

  // 清空所有事件的监听
  clear(): void
}

11. 数据库事务

Db.transaction() 封装了 MySQL 的事务流程:获取连接 → 开启事务 → 执行业务 → 提交/回滚 → 释放连接。

11.1 基本用法

await Db.transaction(async (conn) => {
  await User.insert({ name: '张三' })
  await User.insert({ name: '李四' })
  // 任何异常自动回滚
})

11.2 事务原理

Db.transaction() 内部流程:

Db.transaction(callback)
  ├── 1. Db.connect(name)                   获取连接池
  ├── 2. conn.getConnection()              从池中取出一个物理连接
  ├── 3. poolConn.beginTransaction()        开启事务
  ├── 4. callback(poolConn)                执行业务逻辑(传入原始连接)
  │       └── 用户在此执行 insert/update/delete 等操作
  ├── 5. poolConn.commit()                 提交事务
  ├── 6. 返回 callback 的返回值
  └── 异常时:
        ├── poolConn.rollback()             回滚事务
        └── throw e                        重新抛出异常
  └── finally:
        └── poolConn.release()             归还连接到池

11.3 手动事务(不推荐,除非有特殊需求)

通过 Connection 直接获取物理连接,完全手动控制:

import { Db } from 'tyno-orm'

const conn = Db.connect()
const poolConn = await conn.getConnection()

try {
  await poolConn.beginTransaction()

  // 方式 1:通过 Query 实例执行
  const q = Db.table('user')
  const { sql, bindings } = q.query().field('id, name').buildSelect()
  await poolConn.execute(sql, bindings)

  // 方式 2:直接在连接上执行原始 SQL
  await poolConn.execute('UPDATE user SET status = ? WHERE id = ?', [1, 5])

  await poolConn.commit()
} catch (e) {
  await poolConn.rollback()
  throw e
} finally {
  poolConn.release()  // 必须手动释放!
}

11.4 事务中的返回值

transaction() 将 callback 的返回值透传回来:

const newUserId = await Db.transaction(async (conn) => {
  const id = await User.insertGetId({ name: '张三', status: 1 })
  await User.where('id', id).update({ status: 2 }) // 用 INSERT 拿到的 ID 做后续操作
  return id
})

console.log('新用户 id:', newUserId)  // 拿到事务中返回的值

11.5 指定连接名称

多数据库场景下指定使用哪个连接的事务:

Db.setConfig({
  default: { /* ... */ },
  bbs:     { /* ... */ },
})

// 在 bbs 数据库上开启事务
await Db.transaction(async (conn) => {
  // 注意:事务中需要用对应连接的 Query,直接调用 SQL
}, 'bbs')

11.6 事务注意事项

  • 连接隔离transaction() 内部用 getConnection() 获取独立连接,不会与其他请求共享,提交/回滚后才归还连接池
  • 自动回滚:callback 中任何 throw 的异常都会触发 rollback()
  • 必须释放:无论成功还是失败,finally 块确保 poolConn.release() 被调用
  • 事务中的 errno:如果事务中某条 SQL 失败(如唯一键冲突),MySQL 的连接状态会出错,后续操作需要手动处理。推荐在 callback 中捕获异常并主动 throw

12. 原始查询

import { Db } from 'tyno-orm'

// 通过 Db.table 快速查询
const rows = await Db.table('user').where('status', 1).select()

// 原始 SQL 表达式
import { Raw } from 'tyno-orm'
User.field(new Raw('COUNT(*) as count, SUM(score) as total'))
User.where(new Raw('age > 18'))

13. 数据库连接

// 获取连接
const conn = Db.connect()           // 默认连接
const conn2 = Db.connect('bbs')     // 指定连接名

// 模型指定连接
class User extends Model {
  static connectionName = 'bbs'
}

// 多数据库配置
Db.setConfig({
  default: { /* ... */ },
  bbs: {     /* ... */ },
})

14. 复杂查询示例

// 多表关联 + 条件 + 排序 + 分页
const users = await User
  .field('u.id, u.name, o.order_no, o.total')
  .alias('u')
  .join('orders o', 'u.id = o.user_id')
  .leftJoin('profile p', 'u.id = p.user_id')
  .where('u.status', '=', 1)
  .orderBy('u.create_time', 'desc')
  .page(1, 20)
  .select()

// 聚合查询
const stats = await User
  .field('status, COUNT(*) as count, AVG(age) as avg_age')
  .groupBy('status')
  .having('count', '>', 5)
  .orderBy('count', 'desc')
  .select()

完整 CRUD 流程示例

import { Db, Model } from 'tyno-orm'

Db.setConfig({ default: { host: 'localhost', port: 3306, user: 'root', password: '', database: 'my_db' } })

class User extends Model {
  static table = 'user'
  static autoWriteTimestamp = true
}

// 插入
const id = await User.insertGetId({ name: '张三', email: '[email protected]', status: 1 })

// 查询
const user = await User.find(id)
console.log(user.toJSON())

// 更新
user.setAttribute('name', '张三丰')
await user.save()

// 删除
await user.delete()

项目结构

src/
├── builder/
│   └── mysql-builder.ts   # MySQL SQL 方言编译
├── model/
│   ├── relations/         # 关联:HasOne, HasMany, BelongsTo, BelongsToMany
│   │   ├── relation.ts
│   │   ├── has-one.ts
│   │   ├── has-many.ts
│   │   ├── belongs-to.ts
│   │   └── belongs-to-many.ts
│   ├── events.ts          # 模型事件系统
│   └── model.ts           # 模型基类
├── query/
│   ├── state.ts           # 查询状态工厂
│   └── query.ts           # 查询构造器
├── connection.ts          # 数据库连接(连接池)
├── db.ts                  # 门面类
├── raw.ts                 # 原始 SQL 表达式
├── types.ts               # 类型定义
└── index.ts               # 导出入口