Sutando: 全新的 Node.js ORM 轮子

116 阅读5分钟

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

  1. 更加符合直觉的查询筛选方式

    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。

  2. 更方便的模型关联查询

    这个就不做对比了,大家可以自行脑补其他 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();
    
  3. 其他一些常用的使用方式

    // 分页,会附带 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();
}