Sutando 是一个全新的 Node.js ORM,支持 MySQL, PostgreSQL, MariaDB, SQLite 等多种数据库,目前只支持 Javascript(Typescript 在支持中)。
Sutando 借鉴了 PHP 框架 Laravel 的 ORM - Eloquent,如果你之前用过 Laravel,那你使用 Sutando 几乎没有学习成本,因为它们的使用起来几乎相同。
项目地址: github.com/sutandojs/s…
文档地址:cn.sutando.org
为什么要用 Sutando?
你可能会问,现在已经有那么多成熟的 ORM 了,为什么还要用 Sutando 呢?
一句话说,就是它 简洁易用的 API:
-
更加符合直觉的查询筛选方式
Sutando 没有选择把所有查询放在一个参数对象里,也没有特别去设定操作符,而是完全使用链式操作去构建查询语句。下面是一个简单对比的例子,查询出 某个作者 或 浏览量大于 100 的文章:
// sequelize Post.findAll({ where: { [Op.or]: [ { author_id: 12 }, { views: { [Op.gt]: 100 } } ] } }); // prisma prisma.post.findMany({ where: { OR: [ { author_id: 12 }, { views: { gt: 100 } } ] } }); // sutando Post.query().where('author_id', 12).orWhere('views', '>', 100).get();
这个例子已经足够简单,但依然能看出 sutando 要比 sequelize 和 prisma 清晰很多,而且你可能会发现这个写法有点眼熟,没错,Sutando 的查询构造器就是基于 knex。
-
更方便的模型关联查询
这个就不做对比了,大家可以自行脑补其他 ORM 的实现方式,因为有的 ORM 需要写原生 SQL 语句才能做到(说的就是你,prisma)。
// 预加载, 查找 50 岁以上的用户,并附带查询他们 所有的文章 和 红色的汽车 const users = await User.query() .with( 'posts', { cars: q => q.where('color', 'red') } ) .where('age', '>', 50) .get(); // 延迟加载,已经查询到用户的情况下,加载他们的所有文章 await users.load('posts'); // 直接获取关联数据 const posts = await user.getRelated('posts'); // 关联筛选 const redCars = await user.related('cars').where('color', 'red').get(); // 关联模型计数,这样每个用户会多出来一个 posts_count 属性 const users = await User.query().withCount('posts').get();
-
其他一些常用的使用方式
// 分页,会附带 total, currentPage 等一些常用属性和方法 const users = User.query().where('age', '>', 23).paginate(15); // whereX,可以在 where 后直接加上字段名 const users = User.query().whereAge('>', 23).paginate(15);
使用
如果看到这里你依然对 Sutando 有兴趣,那么我们就要进入枯燥的教学了 - 如何安装和使用 Sutando。
连接数据库
用你喜欢的包管理来安装 sutando
:
npm install sutando --save
# 或
yarn add sutando
# 或
pnpm add sutando
根据要使用的数据库安装以下其中一项:
npm install pg --save
npm install sqlite3 --save
npm install better-sqlite3 --save
npm install mysql --save
npm install mysql2 --save
npm install tedious --save
配置数据库连接
使用 addConnection
方法添加数据库连接,第一个参数为连接信息,第二个参数为连接名称,不填写默认为 default
。
数据库连接可添加多个,后续 定义模型 等需要设置数据库连接的地方,默认都会使用 default
连接。
const { sutando } = require('sutando');
sutando.addConnection({
// 使用的数据库包名
client: 'mysql2',
// 相应的连接信息
connection: {
host : '127.0.0.1',
port : 3306,
user : 'your_database_user',
password : 'your_database_password',
database : 'myapp_test'
},
}, 'default');
定义模型
const { Model } = require('sutando');
// 最简单的模型定义
class User extends Model {}
// 详细配置,以下均为可选
class User extends Model {
connection = 'default'; // 连接名称,默认为 default
table = 'users'; // 数据表名,默认为模型名称的复数形式,采用蛇形命名
primaryKey = 'id'; // 主键,默认为 id
timestamps = true; // 是否自动维护 CREATED_AT 和 UPDATED_AT 时间戳字段
softDeletes = false; // 是否开启软删除
attributes = {
// name: 'default name'
}; // 设置默认值
hidden = []; // 序列化后要隐藏的字段
visible = []; // 序列化后要显示的字段
appends = []; // 把访问器内容添加到序列化结果
with = []; // 默认加载的关联关系
static CREATED_AT = 'created_at'; // 新增数据时间字段名
static UPDATED_AT = 'updated_at'; // 最后更新时间字段名
static DELETED_AT = 'deleted_at'; // 软删除时间字段名
// 自定义访问器
// user.full_name
getFullNameAttribute() {
return this.attributes.first_name + ' ' + this.attributes.last_name;
}
// 自定义作用域
// User.query().popular().get()
scopePopular(query) {
return query.where('votes', '>', 100);
}
// 自定义模型关联
// user.posts
relationPosts() {
return this.hasMany(Post);
}
}
增删改查
查询
Sutando 可以使用模型查询,也可以使用数据库连接直接查询,它们的背后都是查询构造器,支持链式操作。
// 连接查询
const db = sutando.connection(); // 参数为连接名称,默认为 default
const user = await db.table('users')
.where('name', 'Jack')
.orderBy('id', 'desc')
.first();
const query = db.table('users');
query.where('age', '>', 80);
const oldmen = await query.get();
// 模型查询
const user = await User.query()
.with('posts')
.popular()
.orderBy('id', 'desc')
.first();
const query = User.query();
query.where('age', '>', 80);
const oldmen = await query.get();
值得注意的是,连接查询只支持 where
等基础的查询方法,得到的结果是普通的数据对象;
而模型查询支持 预加载、作用域 等更多功能,得到的结果是 Model
模型实例(单条)或 Collenction
集合实例(多条),它们也有着更丰富的功能。
// 查询多条, get
const users = await User.query().get();
// 查询单条, first
const user = await User.query().first();
// 根据主键查询,find
const user = await User.query().find(5);
const users = await User.query().find([5, 6, 7]);
// 分页查询,paginate
const users = await User.query().paginate(15 /* 条数 */, 6 /* 页数 */);
新增
// 使用模型新增
const user = new User;
user.name = 'Jacky Chen';
await user.save();
// 使用查询构造器新增
await User.query().create({
name: 'sutando'
});
更新
// 使用模型更新
user.name = 'new name';
await user.save();
// 使用查询构造器更新
await User.query().where('id', '>', 5).update({
status: 1
});
删除
// 模型删除
await user.delete();
// 查询构造器删除
await User.query().where('id', '>', 5).delete();
软删除
// 查看模型是否被软删除
if (user.trashed()) { /* */ }
// 恢复软删除数据
await user.restore(); // 模型
await User.query().withTrashed().where('id', 5).restore(); // 查询构造器
// 强制删除数据
await user.forceDelete();
await User.query().where('id', '>', 5).forceDelete();
// 查询包含软删除的数据
await User.query().withTrashed().get();
// 只检索软删除的数据
await User.query().onlyTrashed().get();
模型关联
Sutando 支持 一对一,一对多,多对多 的关联形式,每个关联方法需要以 relation
作为开头,下面是一个简单的示例:
// 文章模型
class Post extends Model {
// hasOne, 一对一,每篇文章有一个标题图 posts.id = thumbnails.post_id
relationThumbnail() {
return this.hasOne(Thumbnail);
}
// belongsTo, 反向对应,每篇文章对应一个作者 posts.author_id = users.id
relationAuthor() {
return this.belongsTo(User, 'author_id');
}
// hasMany, 一对多,每篇文章有多张图片 posts.id = images.post_id
relationImages() {
return this.hasMany(Image);
}
// belongsToMany, 多对多,每篇文章都有多个标签
// posts.id = post_tag.post_id & tags.id = post_tag.tag_id
relationTags() {
return this.belongsToMany(Tag);
}
// 关联都是一个查询构造器,可添加条件语句
// 获取文章的评论时,只查询公开的评论
relationComments() {
return this.hasMany(Comment).where('state', 'publish');
}
}
// 查询
const author = await post.getRelated('author');
const comments = await post.related('comments').limit(10).get();
// 新增
const comment = new Comment({
content: 'hello'
});
await post.related('comments').save(comment);
事务
// 闭包事务
await sutando.transaction(async trx => {
const user = new User;
user.name = 'david';
await user.save({
client: trx
});
await User.query().transacting(trx).update(/* ... */);
});
// 手动事务
const trx = await sutando.beginTransaction();
try {
const user = new User;
user.name = 'david';
await user.save({
client: trx
});
await User.query().transacting(trx).update(/* ... */);
await trx.commit();
} catch (e) {
await trx.rollBack();
}