关系数据库

关系数据库

在项目开发中,经常需要操作数据库(如:增删改查等功能),手工拼写 SQL 语句非常麻烦,同时还要注意 SQL 注入等安全问题。为此框架提供了模型功能,方便操作数据库。

扩展模型功能

框架默认没有提供模型的功能,需要加载对应的扩展才能支持,对应的模块为 think-model。修改扩展的配置文件 src/config/extend.js(多模块项目为 src/common/config/extend.js),添加如下的配置:

const model = require('think-model' module.exports = [ model(think.app) // 让框架支持模型的功能 ]

添加模型的扩展后,会添加方法 think.Modelthink.modelctx.modelcontroller.modelservice.model

配置数据库

模型由于要支持多种数据库,所以配置文件的格式为 Adapter 的方式,文件路径为 src/config/adapter.js(多模块项目下为 src/common/config/adapter.js)。

const mysql = require('think-model-mysql' exports.model = { type: 'mysql', // 默认使用的类型,调用时可以指定参数切换 common: { // 通用配置 logConnect: true, // 是否打印数据库连接信息 logSql: true, // 是否打印 SQL 语句 logger: msg => think.logger.info(msg) // 打印信息的 logger }, mysql: { // mysql 配置 handle: mysql }, mysql2: { // 另一个 mysql 的配置 handle: mysql }, sqlite: { // sqlite 配置 }, postgresql: { // postgresql 配置 } }

如果项目里要用到同一个类型的多个数据库配置,那么可以通过不同的 type 区分,如:mysqlmysql2,调用时可以指定参数切换。

const user1 = think.model('user' // 使用默认的数据库配置,默认的 type 为 mysql,那么就是使用 mysql 的配置 const user2 = think.model('user', 'mysql2' // 使用 mysql2 的配置 const user3 = think.model('user', 'sqlite' // 使用 sqlite 的配置 const user4 = think.model('user', 'postgresql' // 使用 postgresql 的配置

由于可以调用时指定使用哪个 type,理论上可以支持无限多的类型配置,项目中可以根据需要进行配置。

Mysql

Mysql 的 Adapter 为 think-model-mysql,底层基于 mysql 库实现,使用连接池的方式连接数据库,默认连接数为 1。

const mysql = require('think-model-mysql' exports.model = { type: 'mysql', mysql: { handle: mysql, // Adapter handle user: 'root', // 用户名 password: '', // 密码 database: '', // 数据库 host: '127.0.0.1', // host port: 3306, // 端口 connectionLimit: 1, // 连接池的连接个数,默认为 1 prefix: '', // 数据表前缀,如果一个数据库里有多个项目,那项目之间的数据表可以通过前缀来区分 } }

除了用 host 和 port 连接数据库外,也可以通过 socketPath 来连接,更多配置选项请见 https://github.com/mysqljs/mysql#connection-options

SQLite

SQLite 的 Adapter 为 think-model-sqlite,底层基于 sqlite3 库实现,使用连接池的方式连接数据库,默认连接数为 1。

const sqlite = require('think-model-sqlite' exports.model = { type: 'sqlite', sqlite: { handle: sqlite, // Adapter handle path: path.join(think.ROOT_PATH, 'runtime/sqlite'), // sqlite 保存的目录 database: '', // 数据库名 connectionLimit: 1, // 连接池的连接个数,默认为 1 prefix: '', // 数据表前缀,如果一个数据库里有多个项目,那项目之间的数据表可以通过前缀来区分 } }

PostgreSQL

PostgreSQL 的 Adapter 为 think-model-postgresql,底层基于 pg 库实现,使用连接池的方式连接数据库,默认连接数为 1。

const postgresql = require('think-model-postgresql' exports.model = { type: 'postgresql', postgresql: { handle: postgresql, // Adapter handle user: 'root', // 用户名 password: '', // 密码 database: '', // 数据库 host: '127.0.0.1', // host port: 3211, // 端口 connectionLimit: 1, // 连接池的连接个数,默认为 1 prefix: '', // 数据表前缀,如果一个数据库里有多个项目,那项目之间的数据表可以通过前缀来区分 } }

除了用 host 和 port 连接数据库外,也可以通过 connectionString 来连接,更多配置选项请见 https://node-postgres.com/features/connecting

创建模型文件

模型文件放在 src/model/ 目录下(多模块项目为 src/common/model 以及 src/[module]/model),继承模型基类 think.Model,文件格式为:

// src/model/user.js module.exports = class extends think.Model { getList() { return this.field('name').select( } }

也可以在项目根目录下通过 thinkjs model modelName 快速创建模型文件。

如果项目比较复杂,希望对模型文件分目录管理,那么可以在模型目录下建立子目录,如: src/model/front/user.jssrc/model/admin/user.js,这样在模型目录下建立 frontadmin 目录,分别管理前台和后台的模型文件。

含有子目录的模型实例化需要带上子目录,如:think.model('front/user'),具体见这里。

实例化模型

项目启动时,会扫描项目下的所有模型文件(目录为 src/model/,多模块项目下目录为 src/common/model 以及各种 src/[module]/model),扫描后会将所有的模型类存放在 think.app.models 对象上,实例化时会从这个对象上查找,如果找不到则实例化模型基类 think.Model

think.model

实例化模型类。

think.model('user' // 获取模型的实例 think.model('user', 'sqlite' // 获取模型的实例,修改数据库的类型 think.model('user', { // 获取模型的实例,修改类型并添加其他的参数 type: 'sqlite', aaa: 'bbb' } think.model('user', {}, 'admin' // 获取模型的实例,指定为 admin 模块(多模块项目下有效)

ctx.model

实例化模型类,获取配置后调用 think.model 方法,多模块项目下会获取当前模块下的配置。

const user = ctx.model('user'

controller.model

实例化模型类,获取配置后调用 think.model 方法,多模块项目下会获取当前模块下的配置。

module.exports = class extends think.Controller { async indexAction() { const user = this.model('user' // controller 里实例化模型 const data = await user.select( return this.success(data } }

service.model

实例化模型类,等同于 think.model

含有子目录的模型实例化

如果模型目录下含有子目录,那么实例化时需要带上对应的子目录,如:

const user1 = think.model('front/user' // 实例化前台的 user 模型 const user2 = think.model('admin/user' // 实例化后台的 user 模型

CRUD 操作

think.Model 基类提供了丰富的方法进行 CRUD 操作,下面来一一介绍。

查询数据

模型提供了多种方法来查询数据,如:

  • find 查询单条数据

同时模型支持通过下面的方法指定 SQL 语句中的特定条件,如:

添加数据

模型提供了下列的方法来添加数据:

  • add 添加单条数据

更新数据

模型提供了下列的方法来更新数据:

删除数据

模型提供了下列的方法来删除数据:

手动执行 SQL 语句

有时候模型包装的方法不能满足所有的情况,这时候需要手工指定 SQL 语句,可以通过下面的方法进行:

  • query 手写 SQL 语句查询

事务

对于数据安全要求很高的业务(如:订单系统、银行系统)操作时需要使用事务,这样可以保证数据的原子性、一致性、隔离性和持久性,模型提供了操作事务的方法。

手工操作事务

可以手工通过 model.startTransmodel.commitmodel.rollback 方法操作事务。

transaction

每次操作事务时都手工执行 startTrans、commit 和 rollback 比较麻烦,模型提供了 model.transaction 方法快速操作事务。

设置主键

可以通过 pk 属性设置数据表的主键,具体见 model.pk

设置 schema

可以通过 schema 属性设置数据表结构,具体见 model.schema

关联查询

数据库中表经常会跟其他数据表有关联,数据操作时需要连同关联的表一起操作。如:一个博客文章会有分类、标签、评论,以及属于哪个用户,支持的类型有:一对一、一对一(属于)、一对多和多对多。

可以通过 model.relation 属性配置详细的关联关系。

一对一

一对一关联,表示当前表含有一个附属表。假设当前表的模型名为 user,关联表的模型名为 info,那么配置中字段 key 的默认值为 id,字段 fKey 的默认值为 user_id

module.exports = class extends think.Model { get relation() { return { info: think.Model.HAS_ONE }; } }

执行查询操作时,可以得到类似如下的数据:

[ { id: 1, name: '111', info: { //关联表里的数据信息 user_id: 1, desc: 'info' } }, ...]

一对一(属于)

一对一关联,属于某个关联表,和 HAS_ONE 是相反的关系。假设当前模型名为 info,关联表的模型名为 user,那么配置字段 key 的默认值为 user_id,配置字段 fKey 的默认值为 id

module.exports = class extends think.Model { get relation() { return { user: think.Model.BELONG_TO } } }

执行查询操作时,可以得到类似下面的数据:

[ { id: 1, user_id: 1, desc: 'info', user: { name: 'thinkjs' } }, ... ]

一对多

一对多的关系。假如当前模型名为 post,关联表的模型名为 comment,那么配置字段 key 默认值为 id,配置字段 fKey 默认值为 post_id

module.exports = class extends think.Model { get relation() { return { comment: { type: think.Model.HAS_MANY } } } }

执行查询数据时,可以得到类似下面的数据:

[{ id: 1, title: 'first post', content: 'content', comment: [{ id: 1, post_id: 1, name: 'welefen', content: 'first comment' }, ...] }, ...]

如果关联表的数据需要分页查询,可以通过 model.setRelation 方法进行。

多对多

多对多关系。假设当前模型名为 post,关联模型名为 cate,那么需要一个对应的关联关系表。配置字段 rModel 默认值为 post_cate,配置字段 rfKey 默认值为 cate_id

module.exports = class extends think.Model { get relation() { return { cate: { type: think.Model.MANY_TO_MANY, rModel: 'post_cate', rfKey: 'cate_id' } } } }

查询出来的数据结构为:

[{ id: 1, title: 'first post', cate: [{ id: 1, name: 'cate1', post_id: 1 }, ...] }, ...]

分布式/读写分离

有时候数据库需要用到分布式数据库,或者进行读写分离,这时候可以给配置里添加 parser 完成,如:

exports.model = { type: 'mysql', mysql: { user: 'root', password: '', parser: sql => { // 这里会把当前要执行的 SQL 传递进来 const sqlLower = sql.toLowerCase( if(sql.indexOf('select ') === 0) { return { host: '', port: '' } } else { return { host: '', port: '' } } } } }

parser 里可以根据 sql 返回不同的配置,会将返回的配置和默认的配置进行合并。

常见问题

数据库的连接数最大连接数是多少?

假设项目有二个集群,每个集群有十台机器,每台机器开启了四个 worker,数据库配置的连接池里的连接数为五,那么总体的最大连接数为:2 * 10 * 4 * 5 = 400

如何查看相关调试信息?

模型使用的 debug 名称为 think-model,可以通过 DEBUG=think-model npm start 启动服务然后查看调试信息。

API

model.schema

设置表结构,默认从数据表中获取,也可以自己配置增加额外的配置项。

module.exports = class extends think.Model { get schema() { return { id: { // 字段名称 type: 'int(11)', ... } } } }

支持的字段为:

  • type {String} 字段的类型,包含长度属性

model.relation

配置数据表的关联关系。

module.exports = class extends think.Model { // 配置关联关系 get relation() { return { cate: { // 配置跟分类的关联关系 type: think.Model.MANY_TO_MANY, ... }, comment: { // 配置跟评论的关联关系 } } } }

每个关联关系支持的配置如下:

  • type 关联关系类型,默认为 think.Model.HAS_ONE 一对一:think.Model.HAS_ONE 一对一(属于):think.Model.BELONG_TO 一对多:think.Model.HAS_MANY 多对多:think.Model.MANY_TO_MANY

model.setRelation(name, value)

设置关联关系后,查询等操作都会自动查询关联表的数据。如果某些情况下不需要查询关联表的数据,可以通过 setRelation方法临时关闭关联关系查询。

全部关闭

通过 setRelation(false) 关闭所有的关联关系查询。

module.exports = class extends think.Model { constructor(...args){ super(...args this.relation = { comment: think.Model.HAS_MANY, cate: think.Model.MANY_TO_MANY }; } getList(){ return this.setRelation(false).select( } }

部分启用

通过 setRelation('comment') 只查询 comment 的关联数据,不查询其他的关联关系数据。

module.exports = class extends think.Model { constructor(...args){ super(...args this.relation = { comment: think.Model.HAS_MANY, cate: think.Model.MANY_TO_MANY }; } getList2(){ return this.setRelation('comment').select( } }

部分关闭

通过 setRelation('comment', false) 关闭 comment 的关联关系数据查询。

module.exports = class extends think.Model { constructor(...args){ super(...args this.relation = { comment: think.Model.HAS_MANY, cate: think.Model.MANY_TO_MANY }; } getList2(){ return this.setRelation('comment', false).select( } }

重新全部启用

通过 setRelation(true) 重新启用所有的关联关系数据查询。

module.exports = class extends think.Model { constructor(...args){ super(...args this.relation = { comment: think.Model.HAS_MANY, cate: think.Model.MANY_TO_MANY }; } getList2(){ return this.setRelation(true).select( } }

动态更改配置项

虽然通过 relation 属性配置了关联关系,但有时候调用的时候希望动态修改某些值,如:设置分页,这时候也可以通过 setRelation 方法来完成。

module.exports = class extends think.Model { constructor(...args){ super(...args this.relation = { comment: think.Model.HAS_MANY, cate: think.Model.MANY_TO_MANY }; } getList2(page){ // 动态设置 comment 的分页 return this.setRelation('comment', {page}).select( } }

model.db(db)

获取或者设置 db 的实例,db 为 Adapter handle(如:think-model-mysql) 的实例。事务操作时由于要复用一个连接需要使用该方法。

module.exports = class extends think.Model { async getList() { // 让 user 复用当前的 Apdater handle 实例,这样后续可以复用同一个数据库连接 const user = this.model('user').db(this.db() } }

model.modelName

实例化模型时传入的模型名

const user = think.model('user'

实例化时传入的模型名为 user,那么 model.modelName 值为 user

model.config

实例化模型时传入的配置,模型实例化时会自动传递,不用手工赋值。

{ host: '127.0.0.1', port: 3306, ... }

model.tablePrefix

获取数据表前缀,从配置里的 prefix 字段获取。如果要修改的话,可以通过下面的方式:

module.exports = class extends think.Model { get tablePrefix() { return 'think_'; } }

model.tableName

获取数据表名,值为 tablePrefix + modelName。如果要修改的话,可以通过下面的方式:

module.exports = class extends think.Model { get tableName() { return 'think_user'; } }

model.pk

获取数据表的主键,默认值为 id。如果数据表的主键不是 id,需要自己配置,如:

module.exports = class extends think.Model { get pk() { return 'user_id'; } }

有时候不想写模型文件,而是在控制器里直接实例化,这时候又想改变主键的名称,那么可以通过设置 _pk 属性的方式,如:

module.exports = class extends think.Controller { async indexAction() { const user = this.model('user' user._pk = 'user_id'; // 通过 _pk 属性设置 pk const data = await user.select( } }

model.options

模型操作的一些选项,设置 where、limit、group 等操作时最终都会解析到 options 选项上,格式为:

{ where: {}, // 存放 where 条件的配置项 limit: {}, // 存放 limit 的配置项 group: {}, ... }

model.lastSql

获取最近一次执行的 SQL 语句,默认值为空。

const user = think.model('user' console.log(user.lastSql // 打印最近一条的 sql 语句,如果没有则为空

model.model(name)

  • name {String} 要实例化的模型名

实例化别的模型,支持子目录的模型实例化。

module.exports = class extends think.Model { async getList() { // 如果含有子目录,那么这里带上子目录,如: this.model('front/article') const article = this.model('article' const data = await article.select( ... } }

model.limit(offset, length)

  • offset {Number} SQL 语句里的 offset

设置 SQL 语句里的 limit,会赋值到 this.options.limit 属性上,便于后续解析。

module.exports = class extends think.Model() { async getList() { // SQL: SELECT * FROM `test_d` LIMIT 10 const list1 = await this.limit(10).select( // SQL: SELECT * FROM `test_d` LIMIT 10,20 const list2 = await this.limit(10, 20).select( } }

model.page(page, pagesize)

  • page {Number} 设置当前页数

设置查询分页,会解析为 limit 数据。

module.exports = class extends think.Model() { async getList() { // SQL: SELECT * FROM `test_d` LIMIT 0,10 const list1 = await this.page(1).select( // 查询第一页,每页 10 条 // SQL: SELECT * FROM `test_d` LIMIT 20,20 const list2 = await this.page(2, 20).select( // 查询第二页,每页 20 条 } }

每页条数可以通过配置项 pageSize 更改,如:

// src/config/adapter.js exports.model = { type: 'mysql', mysql: { database: '', ... pageSize: 20, // 设置默认每页为 20 条 } }

model.where(where)

  • where {String | Object} 设置查询条件

设置 where 查询条件,会添加 this.options.where 属性,方便后续解析。可以通过属性 _logic 设置逻辑,默认为 AND。可以通过属性 _complex 设置复合查询。

注意:where 条件中的值必须要在 Logic 里做数据校验,否则可能会有 SQL 注入漏洞。

普通条件

module.exports = class extends think.Model { where1(){ //SELECT * FROM `think_user` return this.where().select( } where2(){ //SELECT * FROM `think_user` WHERE ( `id` = 10 ) return this.where{id: 10}).select( } where3(){ //SELECT * FROM `think_user` WHERE ( id = 10 OR id < 2 ) return this.where('id = 10 OR id < 2').select( } where4(){ //SELECT * FROM `think_user` WHERE ( `id` != 10 ) return this.where{id: ['!=', 10]}).select( } }

null 条件

module.exports = class extends think.Model { where1(){ //SELECT * FROM `think_user` where ( title IS NULL return this.where{title: null}).select( } where2(){ //SELECT * FROM `think_user` where ( title IS NOT NULL return this.where{title: ['!=', null]}).select( } }

EXP 条件

ThinkJS 默认会对字段和值进行转义,防止安全漏洞。有时候一些特殊的情况不希望被转义,可以使用 EXP 的方式,如:

module.exports = class extends think.Model { where1(){ //SELECT * FROM `think_user` WHERE ( (`name` ='name') ) return this.where{name: ['EXP', "=\"name\""]}).select( } }

LIKE 条件

module.exports = class extends think.Model { where1(){ //SELECT * FROM `think_user` WHERE ( `title` NOT LIKE 'welefen' ) return this.where{title: ['NOTLIKE', 'welefen']}).select( } where2(){ //SELECT * FROM `think_user` WHERE ( `title` LIKE '%welefen%' ) return this.where{title: ['like', '%welefen%']}).select( } //like 多个值 where3(){ //SELECT * FROM `think_user` WHERE ( (`title` LIKE 'welefen' OR `title` LIKE 'suredy') ) return this.where{title: ['like', ['welefen', 'suredy']]}).select( } //多个字段或的关系 like 一个值 where4(){ //SELECT * FROM `think_user` WHERE ( (`title` LIKE '%welefen%') OR (`content` LIKE '%welefen%') ) return this.where{'title|content': ['like', '%welefen%']}).select( } //多个字段与的关系 Like 一个值 where5(){ //SELECT * FROM `think_user` WHERE ( (`title` LIKE '%welefen%') AND (`content` LIKE '%welefen%') ) return this.where{'title&content': ['like', '%welefen%']}).select( } }

IN 条件

module.exports = class extens think.Model { where1(){ //SELECT * FROM `think_user` WHERE ( `id` IN ('10','20') ) return this.where{id: ['IN', '10,20']}).select( } where2(){ //SELECT * FROM `think_user` WHERE ( `id` IN (10,20) ) return this.where{id: ['IN', [10, 20]]}).select( } where3(){ //SELECT * FROM `think_user` WHERE ( `id` NOT IN (10,20) ) return this.where{id: ['NOTIN', [10, 20]]}).select( } }

BETWEEN 查询

module.exports = class extens think.Model { where1(){ //SELECT * FROM `think_user` WHERE ( (`id` BETWEEN 1 AND 2) ) return this.where{id: ['BETWEEN', 1, 2]}).select( } where2(){ //SELECT * FROM `think_user` WHERE ( (`id` BETWEEN '1' AND '2') ) return this.where{id: ['between', '1,2']}).select( } }

多字段查询

module.exports = class extends think.Model { where1(){ //SELECT * FROM `think_user` WHERE ( `id` = 10 ) AND ( `title` = 'www' ) return this.where{id: 10, title: "www"}).select( } //修改逻辑为 OR where2(){ //SELECT * FROM `think_user` WHERE ( `id` = 10 ) OR ( `title` = 'www' ) return this.where{id: 10, title: "www", _logic: 'OR'}).select( } //修改逻辑为 XOR where2(){ //SELECT * FROM `think_user` WHERE ( `id` = 10 ) XOR ( `title` = 'www' ) return this.where{id: 10, title: "www", _logic: 'XOR'}).select( } }

多条件查询

module.exports = class extends think.Model { where1(){ //SELECT * FROM `think_user` WHERE ( `id` > 10 AND `id` < 20 ) return this.where{id: {'>': 10, '<': 20}}).select( } //修改逻辑为 OR where2(){ //SELECT * FROM `think_user` WHERE ( `id` < 10 OR `id` > 20 ) return this.where{id: {'<': 10, '>': 20, _logic: 'OR'}}).select() } }

复合查询

module.exports = class extends think.Model { where1(){ //SELECT * FROM `think_user` WHERE ( `title` = 'test' ) AND ( ( `id` IN (1,2,3) ) OR ( `content` = 'www' ) ) return this.where{ title: 'test', _complex: {id: ['IN', [1, 2, 3]], content: 'www', _logic: 'or' } }).select() } }

model.field(field)

  • field {String} 查询字段,支持 AS

设置 SQL 语句中的查询字段,默认为 *。设置后会赋值到 this.options.field 属性上,便于后续解析。

module.exports = class extends think.Model{ async getList() { // SQL: SELECT `d_name` FROM `test_d` const data1 = await this.field('d_name').select( // SQL: SELECT `c_id`,`d_name` FROM `test_d` const data2 = await this.field('c_id,d_name').select( // SQL: SELECT c_id AS cid,`d_name` FROM `test_d` const data3 = await this.field('c_id AS cid, d_name').select( } }

model.fieldReverse(field)

  • field {String} 查询字段,不支持 AS

查询时设置反选字段(即:不查询配置的字段,而是查询其他的字段),会添加 this.options.fieldthis.options.fieldReverse 属性,便于后续分析。

该功能的实现方式为:查询数据表里的所有字段,然后过滤掉配置的字段。

module.exports = class extends think.Model{ async getList() { // SQL: SELECT `id`, `c_id` FROM `test_d` const data1 = await this.fieldReverse('d_name').select( } }

model.table(table, hasPrefix)

  • table {String} 表名,支持值为一个 SELECT 语句

设置当前模型对应的表名,如果 hasPrefix 为 false 且 table 不是 SQL 语句,那么表名会追加 tablePrefix,最后的值会设置到 this.options.table 属性上。

如果没有设置该属性,那么最后解析 SQL 时通过 mode.tableName 属性获取表名。

model.union(union, all)

  • union {String} union 查询字段

设置 SQL 中的 UNION 查询,会添加 this.options.union 属性,便于后续分析。

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` UNION (SELECT * FROM think_pic2) return this.union('SELECT * FROM think_pic2').select( } getList2(){ //SELECT * FROM `think_user` UNION ALL (SELECT * FROM `think_pic2`) return this.union{table: 'think_pic2'}, true).select( } }

model.join(join)

  • join {String | Object | Array} 要组合的查询语句,默认为 LEFT JOIN

组合查询,支持字符串、数组和对象等多种方式。会添加 this.options.join 属性,便于后续分析。

字符串

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` LEFT JOIN think_cate ON think_group.cate_id=think_cate.id return this.join('think_cate ON think_group.cate_id=think_cate.id').select( } }

数组

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` LEFT JOIN think_cate ON think_group.cate_id=think_cate.id RIGHT JOIN think_tag ON think_group.tag_id=think_tag.id return this.join([ 'think_cate ON think_group.cate_id=think_cate.id', 'RIGHT JOIN think_tag ON think_group.tag_id=think_tag.id' ]).select( } }

对象:单个表

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` INNER JOIN `think_cate` AS c ON think_user.`cate_id`=c.`id` return this.join{ table: 'cate', join: 'inner', //join 方式,有 left, right, inner 3 种方式 as: 'c', // 表别名 on: ['cate_id', 'id'] //ON 条件 }).select( } }

对象:多次 JOIN

module.exports = class extends think.Model { getList(){ //SELECT * FROM think_user AS a LEFT JOIN `think_cate` AS c ON a.`cate_id`=c.`id` LEFT JOIN `think_group_tag` AS d ON a.`id`=d.`group_id` return this.alias('a').join{ table: 'cate', join: 'left', as: 'c', on: ['cate_id', 'id'] }).join{ table: 'group_tag', join: 'left', as: 'd', on: ['id', 'group_id'] }).select() } }

对象:多个表

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` LEFT JOIN `think_cate` ON think_user.`id`=think_cate.`id` LEFT JOIN `think_group_tag` ON think_user.`id`=think_group_tag.`group_id` return this.join{ cate: { on: ['id', 'id'] }, group_tag: { on: ['id', 'group_id'] } }).select( } }

module.exports = class extends think.Model { getList(){ //SELECT * FROM think_user AS a LEFT JOIN `think_cate` AS c ON a.`id`=c.`id` LEFT JOIN `think_group_tag` AS d ON a.`id`=d.`group_id` return this.alias('a').join{ cate: { join: 'left', // 有 left,right,inner 3 个值 as: 'c', on: ['id', 'id'] }, group_tag: { join: 'left', as: 'd', on: ['id', 'group_id'] } }).select() } }

对象:ON 条件含有多个字段

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` LEFT JOIN `think_cate` ON think_user.`id`=think_cate.`id` LEFT JOIN `think_group_tag` ON think_user.`id`=think_group_tag.`group_id` LEFT JOIN `think_tag` ON (think_user.`id`=think_tag.`id` AND think_user.`title`=think_tag.`name`) return this.join{ cate: {on: 'id, id'}, group_tag: {on: ['id', 'group_id']}, tag: { on: { // 多个字段的 ON id: 'id', title: 'name' } } }).select() } }

对象:table 值为 SQL 语句

module.exports = class extends think.Model { async getList(){ let sql = await this.model('group').buildSelectSql( //SELECT * FROM `think_user` LEFT JOIN ( SELECT * FROM `think_group` ) ON think_user.`gid`=( SELECT * FROM `think_group` ).`id` return this.join{ table: sql, on: ['gid', 'id'] }).select( } }

model.order(order)

  • order {String | Array | Object} 排序方式

设置 SQL 中的排序方式。会添加 this.options.order 属性,便于后续分析。

字符串

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` ORDER BY id DESC, name ASC return this.order('id DESC, name ASC').select( } getList1(){ //SELECT * FROM `think_user` ORDER BY count(num) DESC return this.order('count(num) DESC').select( } }

数组

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` ORDER BY id DESC,name ASC return this.order(['id DESC', 'name ASC']).select( } }

对象

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` ORDER BY `id` DESC,`name` ASC return this.order{ id: 'DESC', name: 'ASC' }).select( } }

model.alias(aliasName)

  • aliasName {String} 表别名

设置表别名。会添加 this.options.alias 属性,便于后续分析。

module.exports = class extends think.Model { getList(){ //SELECT * FROM think_user AS a; return this.alias('a').select( } }

model.having(having)

  • having {String} having 查询的字符串

设置 having 查询。会设置 this.options.having 属性,便于后续分析。

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` HAVING view_nums > 1000 AND view_nums < 2000 return this.having('view_nums > 1000 AND view_nums < 2000').select( } }

model.group(group)

  • group {String} 分组查询的字段

设定分组查询。会设置 this.options.group 属性,便于后续分析。

module.exports = class extends think.Model { getList(){ //SELECT * FROM `think_user` GROUP BY `name` return this.group('name').select( } }

model.distinct(distinct)

  • distinct {String} 去重的字段

去重查询。会设置 this.options.distinct 属性,便于后续分析。

module.exports = class extends think.Model { getList(){ //SELECT DISTINCT `name` FROM `think_user` return this.distinct('name').select( } }

model.beforeAdd(data)

  • data {Object} 要添加的数据

添加前置操作。

model.afterAdd(data)

  • data {Object} 要添加的数据

添加后置操作。

model.afterDelete(data)

删除后置操作。

model.beforeUpdate(data)

  • data {Object} 要更新的数据

更新前置操作。

有时候希望提交了某值则更新,如果值为空的话就不更新的功能,那么可以通过这个方法来操作:

module.exports = class extends think.Model { beforeUpdate(data) { for (const key in data) { // 如果值为空则不更新 if(data[key] === '') { delete data[key]; } } return data; } }

model.afterUpdate(data)

  • data {Object} 要更新的数据

更新后置操作。

model.afterFind(data)

  • data {Object} 查询的单条数据

find 查询后置操作。

model.afterSelect(data)

  • data [Array] 查询的数据

select 查询后置操作。

model.add(data, options)

  • data {Object} 要添加的数据,如果数据里某些字段在数据表里不存在会自动被过滤掉

添加一条数据,返回值为插入数据的 id。

如果数据表没有主键或者没有设置 auto increment 等属性,那么返回值可能为 0。如果插入数据时手动设置主键的值,那么返回值也可能为 0。

module.exports = class extends think.Controller { async addAction(){ let model = this.model('user' let insertId = await model.add{name: 'xxx', pwd: 'yyy'} } }

有时候需要借助数据库的一些函数来添加数据,如:时间戳使用 mysql 的 CURRENT_TIMESTAMP 函数,这时可以借助 exp 表达式来完成。

module.exports = class extends think.Controller { async addAction(){ let model = this.model('user' let insertId = await model.add{ name: 'test', time: ['exp', 'CURRENT_TIMESTAMP()'] } } }

model.thenAdd(data, where)

  • data {Object} 要添加的数据

当 where 条件未命中到任何数据时才添加数据。

module.exports = class extends think.Controller { async addAction(){ const model = this.model('user' //第一个参数为要添加的数据,第二个参数为添加的条件,根据第二个参数的条件查询无相关记录时才会添加 const result = await model.thenAdd{name: 'xxx', pwd: 'yyy'}, {email: 'xxx'} // result returns {id: 1000, type: 'add'} or {id: 1000, type: 'exist'} } }

也可以把 where 条件通过 this.where 方法直接指定,如:

module.exports = class extends think.Controller { async addAction(){ const model = this.model('user' const result = await model.where{email: 'xxx'}).thenAdd{name: 'xxx', pwd: 'yyy'} // result returns {id: 1000, type: 'add'} or {id: 1000, type: 'exist'} } }

model.addMany(dataList, options)

  • dataList {Array} 要添加的数据列表

一次添加多条数据。

module.exports = class extends think.Controller { async addAction(){ let model = this.model('user' let insertIds = await model.addMany([ {name: 'xxx', pwd: 'yyy'}, {name: 'xxx1', pwd: 'yyy1'} ] } }

model.selectAdd(fields, table, options)

  • fields {Array | String} 列名

添加从 options 解析出来子查询的结果数据。

module.exports = class extends think.Controller { async addAction(){ let model = this.model('user' let insertIds = await model.selectAdd( 'xxx,xxx1,xxx2', 'tableName', { id: '1' } } }

model.delete(options)

  • options {Object} 操作选项,会通过 parseOptions 方法解析

删除数据。

module.exports = class extends think.Controller { async deleteAction(){ let model = this.model('user' let affectedRows = await model.where{id: ['>', 100]}).delete( } }

model.update(data, options)

  • data {Object} 要更新的数据

更新数据。

module.exports = class extends think.Controller { async updateAction(){ let model = this.model('user' let affectedRows = await model.where{name: 'thinkjs'}).update{email: 'admin@thinkjs.org'} } }

默认情况下更新数据必须添加 where 条件,以防止误操作导致所有数据被错误的更新。如果确认是更新所有数据的需求,可以添加 1=1 的 where 条件进行,如:

module.exports = class extends think.Controller { async updateAction(){ let model = this.model('user' let affectedRows = await model.where('1=1').update{email: 'admin@thinkjs.org'} } }

有时候更新值需要借助数据库的函数或者其他字段,这时候可以借助 exp 来完成。

module.exports = class extends think.Controller { async updateAction(){ let model = this.model('user' let affectedRows = await model.where('1=1').update{ email: 'admin@thinkjs.org', view_nums: ['exp', 'view_nums+1'], update_time: ['exp', 'CURRENT_TIMESTAMP()'] } } }

model.thenUpdate(data, where)

  • data {Object} 要更新的数据

当 where 条件未命中到任何数据时添加数据,命中数据则更新该数据。

updateMany(dataList, options)

  • dataList {Array} 要更新的数据列表

更新多条数据,dataList 里必须包含主键的值,会自动设置为更新条件。

this.model('user').updateMany([{ id: 1, // 数据里必须包含主键的值 name: 'name1' }, { id: 2, name: 'name2' }])

model.increment(field, step)

  • field {String} 字段名

字段值增加。

module.exports = class extends think.Model { updateViewNums(id){ return this.where{id: id}).increment('view_nums', 1 //将阅读数加 1 } updateViewAndUserNums(id) { return this.where{id}).increment(['view_nums', 'user_nums'], 1 //将阅读数和阅读人数加 1 } updateViewAndUserNums(id) { return this.where{id}).increment{view_nums: 2, user_nums: 1} //将阅读数加2,阅读人数加 1 } }

model.decrement(field, step)

  • field {String} 字段名

字段值减少。

module.exports = class extends think.Model { updateViewNums(id){ return this.where{id: id}).decrement('coins', 10 //将金币减 10 } }

model.find(options)

  • options {Object} 操作选项,会通过 parseOptions 方法解析

查询单条数据,返回的数据类型为对象。如果未查询到相关数据,返回值为 {}

module.exports = class extends think.Controller { async listAction(){ let model = this.model('user' let data = await model.where{name: 'thinkjs'}).find( //data returns {name: 'thinkjs', email: 'admin@thinkjs.org', ...} if(think.isEmpty(data)) { // 内容为空时的处理 } } }

可以通过 think.isEmpty 方法判断返回值是否为空。

model.select(options)

  • options {Object} 操作选项,会通过 parseOptions 方法解析

查询多条数据,返回的数据类型为数组。如果未查询到相关数据,返回值为 []

module.exports = class extends think.Controller { async listAction(){ let model = this.model('user' let data = await model.limit(2).select( //data returns [{name: 'thinkjs', email: 'admin@thinkjs.org'}, ...] if(think.isEmpty(data)){ } } }

可以通过 think.isEmpty 方法判断返回值是否为空。

model.countSelect(options, pageFlag)

  • options {Number | Object} 操作选项,会通过 parseOptions 方法解析

分页查询,一般需要结合 page 方法一起使用。如:

module.exports = class extends think.Controller { async listAction(){ let model = this.model('user' let data = await model.page(this.get('page')).countSelect( } }

返回值数据结构如下:

{ pageSize: 10, //每页显示的条数, think-model@1.1.8 之前该字段为 pagesize currentPage: 1, //当前页 count: 100, //总条数 totalPages: 10, //总页数 data: [{ //当前页下的数据列表 name: "thinkjs", email: "admin@thinkjs.org" }, ...] }

有时候总条数是放在其他表存储的,不需要再查当前表获取总条数了,这个时候可以通过将第一个参数 options 设置为总条数来查询。

module.exports = class extends think.Controller { async listAction(){ const model = this.model('user' const total = 256; // 指定总条数查询 const data = await model.page(this.get('page')).countSelect(total } }

model.getField(field, num)

  • field {String} 字段名,多个字段用逗号隔开

获取特定字段的值,可以设置 where、group 等条件。

获取单个字段的所有列表

module.exports = class extends think.Controller { async listAction(){ const data = await this.model('user').getField('c_id' // data = [1, 2, 3, 4, 5] } }

指定个数获取单个字段的列表

module.exports = class extends think.Controller { async listAction(){ const data = await this.model('user').getField('c_id', 3 // data = [1, 2, 3] } }

获取单个字段的一个值

module.exports = class extends think.Controller { async listAction(){ const data = await this.model('user').getField('c_id', true // data = 1 } }

获取多个字段的所有列表

module.exports = class extends think.Controller { async listAction(){ const data = await this.model('user').getField('c_id,d_name' // data = {c_id: [1, 2, 3, 4, 5], d_name: ['a', 'b', 'c', 'd', 'e']} } }

获取指定个数的多个字段的所有列表

module.exports = class extends think.Controller { async listAction(){ const data = await this.model('user').getField('c_id,d_name', 3 // data = {c_id: [1, 2, 3], d_name: ['a', 'b', 'c']} } }

获取多个字段的单一值

module.exports = class extends think.Controller { async listAction(){ const data = await this.model('user').getField('c_id,d_name', true // data = {c_id: 1, d_name: 'a'} } }

model.count(field)

  • field {String} 字段名,如果不指定那么值为 *

获取总条数。

module.exports = class extends think.Model{ // 获取字段值之和 getScoreCount() { // SELECT COUNT(score) AS think_count FROM `test_d` LIMIT 1 return this.count('score' } }

model.sum(field)

  • field {String} 字段名

对字段值进行求和。

module.exports = class extends think.Model{ // 获取字段值之和 getScoreSum() { // SELECT SUM(score) AS think_sum FROM `test_d` LIMIT 1 return this.sum('score' } }

model.min(field)

  • field {String} 字段名

求字段的最小值。

module.exports = class extends think.Model{ // 获取最小值 getScoreMin() { // SELECT MIN(score) AS think_min FROM `test_d` LIMIT 1 return this.min('score' } }

model.max(field)

  • field {String} 字段名

求字段的最大值。

module.exports = class extends think.Model{ // 获取最大值 getScoreMax() { // SELECT MAX(score) AS think_max FROM `test_d` LIMIT 1 return this.max('score' } }

model.avg(field)

  • field {String} 字段名

求字段的平均值。

module.exports = class extends think.Model{ // 获取平均分 getScoreAvg() { // SELECT AVG(score) AS think_avg FROM `test_d` LIMIT 1 return this.avg('score' } }

model.query(sqlOptions)

  • sqlOptions {String | Object} 要执行的 sql 选项

指定 SQL 语句执行查询,sqlOptions 会通过 parseSql 方法解析,使用该方法执行 SQL 语句时需要自己处理安全问题。

module.exports = class extends think.Model { getMysqlVersion() { return this.query('select version(' } }

model.execute(sqlOptions)

  • sqlOptions {String | Object} 要操作的 sql 选项

执行 SQL 语句,sqlOptions 会通过 parseSql 方法解析,使用该方法执行 SQL 语句时需要自己处理安全问题。

module.exports = class extends think.Model { xxx() { return this.execute('set @b=5;call proc_adder(2,@b,@s' } }

model.parseSql(sqlOptions, ...args)

  • sqlOptions {String | Object} 要解析的 SQL 语句

解析 SQL 语句,将 SQL 语句中的 __TABLENAME__ 解析为对应的表名。通过 util.format 将 args 数据解析到 sql 中。

module.exports = class extends think.Model { getSql(){ const sql = 'SELECT * FROM __GROUP__ WHERE id=10'; const sqlOptions = this.parseSql(sql //{sql: "SELECT * FROM think_group WHERE id=10"} } getSql2(){ const sql = 'SELECT * FROM __GROUP__ WHERE id=10'; const sqlOptions = this.parseSql{sql, debounce: false} //{sql: SELECT * FROM think_group WHERE id=10", debounce: false} } }

model.parseOptions(options)

  • options {Object} 要合并的 options,会合并到 this.options 中一起解析

解析 options。where、limit、group 等操作会将对应的属性设置到 this.options 上,该方法会对 this.options 进行解析,并追加对应的属性,以便在后续的处理需要这些属性。

const options = await this.parseOptions{limit: 1} /** options = { table: '', tablePrefix: '', pk: '', field: '', where: '', limit: '', group: '', ... } */

调用 this.parseOptions 解析后,this.options 属性会被置为空对象 {}

model.startTrans()

  • return {Promise}

开启事务。

model.commit()

  • return {Promise}

提交事务。

model.rollback()

  • return {Promise}

回滚事务。

module.exports = class extends think.Model { async addData() { // 如果添加成功则 commit,失败则 rollback try { await this.startTrans( const result = await this.add{} await this.commit( return result; } catch(e){ await this.rollback( } } }

如果事务操作过程中需要实例化多个模型操作,那么需要让模型之间复用同一个数据库连接,具体见 model.db

model.transaction(fn)

  • fn {Function} 要执行的函数,如果有异步操作,需要返回 Promise

使用事务来执行传递的函数,函数要返回 Promise。如果函数返回值为 Resolved Promise,那么最后会执行 commit,如果返回值为 Rejected Promise(或者报错),那么最后会执行 rollback。

module.exports = class extends think.Model { async updateData(data){ const result = await this.transaction(async () => { const insertId = await this.add(data return insertId; }) } }

由于事务里的操作需要在同一个连接里执行,如果处理过程中涉及多个模型的操作,需要多个模型复用同一个数据库连接,这时可以通过 model.db 方法达到复用数据库连接的效果。

module.exports = class extends think.Model { async updateData(data){ const result = await this.transaction(async () => { const insertId = await this.add(data // 通过 db 方法让 user_cate 模型复用当前模型的数据库连接 const userCate = this.model('user_cate').db(this.db() let result = await userCate.add{user_id: insertId, cate_id: 100} return result; }) } }

model.cache(key, config)

  • key {String} 缓存 key,如果不设置会获取 SQL 语句的 md5 值作为 key

设置查询缓存,只在 selectfindgetField 等查询相关的方法下有效。会自动合并 cache Adapter、model cache 的配置。

// cache adapter 配置 exports.cache = { type: 'file', file: { handle: fileCache, ... } } // model adapter 配置 exports.model = { type: 'mysql', mysql: { handle: mysqlModel, ... cache: { // 额外的缓存配置 type: 'file', handle: fileCache } } }

最终会将 cache adapter 配置、model cache 配置、以及参数里的配置合并起来作为 cache 的配置。

module.exports = class extends think.Controller { indexAction() { // 设置缓存 key 为 userList,有效期为 2 个小时 return this.model('user').cache('userList', {timeout: 2 * 3600 * 1000}).select( } }

model.lock(lock)

  • lock {Boolean} 是否 lock

SELECT 时加锁,在 SELECT 语句后面加上 FOR UPDATE

module.exports = class extends think.Controller { async indexAction() { const user = this.model('user' const data = await user.lock(true).where{id: 1}).find( await user.where{id: data}).update{score: 1} } }

model.buildSelectSql(options, noParentheses)

  • options {object}

根据条件生成 SELECT 语句。

module.exports = class extends think.Controller { async indexAction() { const user = this.model('user' const sql = await user.where{id: 1}).buildSelectSql( } }

常见问题

高并发下,多个查询语句只会执行一次

为了查询语句有更高的性能,我们认为,在一次 SQL 语句查询期间,有相同的 SQL 语句需要执行时,那么返回的值是一样的,那么就可以把第一次的查询结果缓存,然后同步给后面的查询语句即可,我们称之为 debounce

如果不希望开启这个功能,那么可以在数据库配置中添加 debounce: false 来关闭,如:

const mysql = require('think-model-mysql' exports.model = { type: 'mysql', mysql: { handle: mysql, // Adapter handle user: 'root', // 用户名 password: '', // 密码 database: '', // 数据库 host: '127.0.0.1', // host port: 3306, // 端口 connectionLimit: 1, // 连接池的连接个数,默认为 1 prefix: '', // 数据表前缀,如果一个数据库里有多个项目,那项目之间的数据表可以通过前缀来区分 debounce: false // 关闭 debounce 功能 } }

数据库支持 emoji 表情

数据库的编码一般会设置为 utf8,但 utf8 并不支持 emoji 表情,如果需要数据库支持 emoji 表情,需要将数据库编码设置为 utf8mb4

同时需要在数据库配置中添加或修改 charset 的值为 utf8mb4,如:

const mysql = require('think-model-mysql' exports.model = { type: 'mysql', mysql: { handle: mysql, // Adapter handle user: 'root', // 用户名 password: '', // 密码 database: '', // 数据库 host: '127.0.0.1', // host port: 3306, // 端口 connectionLimit: 1, // 连接池的连接个数,默认为 1 prefix: '', // 数据表前缀,如果一个数据库里有多个项目,那项目之间的数据表可以通过前缀来区分 charset: 'utf8mb4' } }

模型设置添加 after(Find|Select) 钩子之后关联模型数据未获取

因为关联模型也是利用这几个钩子来实现的,如果在继承类中复写了这几个方法的话需要手动的调用基类中的同名方法才会执行关联模型数据获取。

module.exports = class extends think.Model { afterFind(...args) { super.afterFind(...args //do something... } }