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 🙏

© 2024 – Pkg Stats / Ryan Hefner

lubejs

v3.0.0-preview08

Published

An easy-to-use nodejs oracledb client

Downloads

62

Readme

Lubejs

Lubejs 是一个用于 node.js 诣在方便使用SQL数据库连接. 取名为lube意为润滑,即作为js与sql间的润滑剂般的存在,我们可以尽情使用优雅的 js/ts来 替代拼接 sql 字符串。

本库部分灵感来自于EFTypeORM,致谢

English

lubejs是什么

lubejs 是一套类型化sql构建、执行工具,亦是一套强大易用的Typescript ORM开发框架。

  • 完备的SQL构建工具,使用最贴近SQL的语法编写SQL,极低的学习成本
  • 强大的Typescript类型支持,支持反向类型推导,返回明确类型,拥有完整类型安全体系,智能语法提示,提高开发效率以及预排除类型错误,强烈建立在typescript项目中使用lubejs。
  • ORM配套工具,Code first、数据迁移
  • 匹配多种数据库(目前只支持mssql)
  • 跨数据库兼容,为此,lubejs建立了标准行为库,把大多数常用的,而在各个数据库中又不尽相同的操作行为,包括在其中。

lubejs理念

  • 简洁,极简api,极易上手
  • 贴近自然,语法与标准sql极为接近,大大降低学习成本
  • 渐进式,lubejs分为两个层级的引用,core及完整功能包
  • 多数据库方言统一兼容,建立中间标准操作库并不断丰富。
  • 完整的typescript类型安全

快速开始

安装

使用 npm 安装:

# 安装lubejs库
npm install lubejs --save

# 安装lubejs-mssql驱动
npm install lubejs-mssql

开始

Hello world!

// hello-world.ts
import { connect, SQL } from 'lubejs'
// 导入mssql驱动
import 'lubejs-mssql'

(async () => {
  // 创建连接
  const db = await connect('mssql://user:password@localhost:1433/database');
  // SELECT 'hello world'
  console.log(await db.queryScalar(SQL.select('hello world!')));  // => 'hello world'

  await db.close();
})()

完整范例

// example.ts
import {
  connect,
  SQL,
  Decimal,
  Uuid,
  Connection,
  DbType,
  outputCommand,
} from "lubejs";
import "lubejs-mssql";

interface Table1 {
  id: number;
  name: string;
  stringField?: string;
  floatField?: number;
  dateField?: Date;
  decimalField?: Decimal;
  uuidField?: Uuid;
  updatedAt: Date;
  binaryField?: ArrayBuffer;
  createdAt: Date;
  operator?: string;
}

interface Pay {
  id?: number;
  year: number;
  month: number;
  amount: Decimal;
  personId: number;
}

interface Person {
  id?: number;
  name: string;
  age: number;
}

/**
 * 初始化数据库
 */
async function initDb(db: Connection) {
  await db.query(
    SQL.if(SQL.std.existsTable('table1')).then(SQL.dropTable("table1"))
  );

  await db.query(
    SQL.createTable("table1").as(({ column }) => [
      column("id", DbType.int32).identity().primaryKey(),
      column("name", DbType.string(100)).notNull(),
      column("stringField", DbType.string(100)).null(),
      column("floatField", DbType.float).null(),
      column("dateField", DbType.datetimeoffset).null(),
      column("decimalField", DbType.decimal(18, 6)),
      column("uuidField", DbType.uuid),
      column("updatedAt", DbType.datetimeoffset).default(SQL.std.now()),
      column("binaryField", DbType.binary(DbType.MAX)),
      column("createdAt", DbType.datetimeoffset).default(SQL.std.now()),
      column("operator", DbType.string(100)).null(),
    ])
  );

  await db.query(
    SQL.if(SQL.std.existsTable('pay')).then(SQL.dropTable("pay"))
  );

  await db.query(
    SQL.createTable("pay").as(({ column }) => [
      column("id", DbType.int32).identity().primaryKey(),
      column("year", DbType.int32),
      column("month", DbType.int32),
      column("amount", DbType.decimal(18, 2)),
      column("personId", DbType.int32),
    ])
  );

  await db.query(
    SQL.if(SQL.std.existsTable('person')).then(SQL.dropTable("person"))
  );

  await db.query(
    SQL.createTable("person").as(({ column }) => [
      column("id", DbType.int32).identity().primaryKey(),
      column("name", DbType.int32).notNull(),
      column("age", DbType.int32),
    ])
  );

}

/**
 * Table1表声明
 */
// 这是一个范例
async function example(db: Connection) {
  //---------------插入数据------------------
  /*
   * INSERT INTO table1 (stringField, floatField, dateField)
   * VALUES ('value1-1', 2, Convert(DATETIMEOFFSET, '2019-11-18 00:00:00'))
   * ('value1-2', 1, Convert(DATETIMEOFFSET, '2019-11-18 00:00:00'))
   * ('value1-3', 45, Convert(DATETIMEOFFSET, '2019-11-18 00:00:00'))
   */
  const insertSql = SQL.insert<Table1>("table1").values([
    {
      name: "item1",
      stringField: "value1-1",
      floatField: 3.14,
      dateField: new Date(),
      decimalField: new Decimal("3.1415"),
      uuidField: Uuid.new(),
      binaryField: Buffer.from('abcdefeg')
    },
    {
      name: "item2",
      stringField: "value1-2",
      floatField: 1.132,
      dateField: new Date(),
      decimalField: new Decimal("3.1415"),
      uuidField: Uuid.new(),
      binaryField: Buffer.from('abcdefeg')
    },
    {
      name: "item3",
      stringField: "value1-3",
      floatField: 45.2656,
      dateField: new Date(),
      decimalField: new Decimal("3.1415"),
      uuidField: Uuid.new(),
      binaryField: Buffer.from('abcdefeg')
    },
  ]);

  await db.query(insertSql);

  // 你还以使用以下方式插入,等效于上面的写法
  await db.insert<Table1>("table1", [
    {
      name: "item1",
      stringField: "value1-1",
      floatField: 3.14,
      dateField: new Date(),
      decimalField: new Decimal("3.1415"),
      uuidField: Uuid.new(),
      binaryField: Buffer.from('abcdefeg')
    },
    {
      name: "item2",
      stringField: "value1-2",
      floatField: 1.132,
      dateField: new Date(),
      decimalField: new Decimal("3.1415"),
      uuidField: Uuid.new(),
      binaryField: Buffer.from('abcdefeg')
    },
    {
      name: "item3",
      stringField: "value1-3",
      floatField: 45.2656,
      dateField: new Date(),
      decimalField: new Decimal("3.1415"),
      uuidField: Uuid.new(),
      binaryField: Buffer.from('abcdefeg')
    },
  ]);

  //---------------更新数据------------------
  // UPDATE t SET updatedAt = Convert(DateTime, '2019-11-18 00:00:00') FROM table1 t WHERE id = 1
  const t = SQL.table<Table1>("table1").as("t");
  const updateSql = SQL.update(t)
    .set({ updatedAt: new Date(), operator: "your name" })
    .where(t.id.eq(1));
  await db.query(updateSql);

  // 你还以使用以下方式更新,等效于上面的写法
  await db.update<Table1>(
    "table1",
    { updatedAt: new Date(), operator: "your name" },
    { id: 1 }
  );

  //---------------删除数据-------------------
  // DELETE t FROM table1 t WHERE t.id = 1
  const deleteSql = SQL.delete(t).from(t).where(t.id.eq(1));
  await db.query(deleteSql);

  // 你还以使用以下方式删除
  // DELETE table1 WHERE id = 1
  await db.delete("table1", { id: 1 });

  //----------------查询数据--------------------
  // SELECT t.* FROM table1 AS t WHERE t.id = 1 AND t.name = 'name1'
  const selectSql = SQL.select(t.star)
    .from(t)
    .where(SQL.and(t.id.eq(1), t.name.eq("name1")));
  console.log((await db.query(selectSql)).rows);

  //  You can also select in this way
  // SELECT * FROM table1 WHERE id = 1 AND name = 'name1'
  console.log(
    await await db.select("table1", {
      where: {
        id: 1,
        name: "item1",
      },
    })
  );

  // //---------------以下是一个复合查询------------
  const p = SQL.table<Person>("person").as("p");
  const pay = SQL.table<Pay>("pay");
  const sql = SQL.select({
        year: pay.year,
        month: pay.month,
        name: p.name,
        age: p.age,
        total: SQL.std.sum(pay.amount),
  })
    .from(pay)
    .join(p, pay.personId.eq(p.id))
    .where(p.age.lte(18))
    .groupBy(p.name, p.age, pay.year, pay.month)
    .having(SQL.std.sum(pay.amount).gte(new Decimal(100000)))
    .orderBy(pay.year.asc(), pay.month.asc(), SQL.std.sum(pay.amount).asc(), p.age.asc())
    .offset(20)
    .limit(50);

  console.log((await db.query(sql)).rows);
}


(async () => {
  // 创建一个Lube连接
  const db = await connect("mssql://sa:[email protected]/Test");
  // 打开连接
  await db.open();
  // 输出日志
  db.on('command', (cmd) => outputCommand(cmd, process.stdout))
  try {
    await initDb(db);
    await example(db);
  } finally {
    await db.close();
  }
})();

版本说明

注意: lubejs目前仍为预览版,内部会有部分调整,公共API可能会有小许调整,但不会有大调整。

渐进式分离

  • lubejs/core 为核心包,包括sql构建以及sql执行工具。
  • lubejs则为完整包,包括lubejs/core的所有内容以及orm功能,数据迁移cli等。

数据库支持列表

  • [x] mssql - 目前支持microsoft sqlserver 2012 或更高版本, 库基于 node-mssql开发.
  • [ ] mysql - 当前正在开发中
  • [ ] postgresql - 计划于2021年底开发

NodeJs版本支持

nodejs >= 12.0

概念

SQL构造器(SQL对象)

所有的SQL构造,均由 SQL对象发起,几乎所有的SQL的语句,均可从SQL对象创建,例如SQL.selectSQL.update,SQL.delete等。

// 导入SQL对象
import { SQL } from 'lubejs';

为了更贴近sql语法,您还可以使用解构来引入需要的关键字

const {
    insert,
    delete: $delete // 关键字delete需要使用别名
} = SQL

// 构建插入张三、李四两条记录到table1的语句
const sql = insert('table1').values([{ name: '张三', age: 19, sex: '男' }, { name: '李四', age: 25, sex: '男' }]);

// 构建table1表中删除id为1记录的sql语句
const sql = $delete('table1').where({ id: 1 })

更多SQL对象用法,请翻阅《api参考》

注意:deletejs关键字,需要使用别名代替,其它关键字亦是如此

标准行为(SQL.std)

lubejs为了更大程序的兼容多数据库,专门定义了标准行为,用于统一在跨数据库时的操作,避免在跨方言数据库迁移是的重复劳动。

SQL.std中定义了许多常用的函数、操作等行为

常用函数

| 说明 | 函数 | 备注 | | -------------------- | ------------------------------------------------------------ | ---- | | 类型转换 | SQL.std.convert(expr, dbType)Expression.prototype.to(dbType) | | | 当值为空时返回默认值 | SQL.std.nvl(value, defaultValue) | |

聚合函数

| 说明 | 函数 | 备注 | | ------ | --------------------- | ---- | | 计数 | SQL.std.count(expr) | | | 平均 | SQL.std.avg(expr) | | | 求和 | SQL.std.sum(expr) | | | 最大值 | SQL.std.max(expr) | | | 最小值 | SQL.std.min(expr) | |

日期函数

| 说明 | 函数 | 备注 | | ---------------------- | -------------------------------------- | ---- | | 当前时间 | SQL.std.now(expr) | | | UTC当前时间 | SQL.std.utcNow(expr) | | | 切换时区 | SQL.std.switchTimezone(date, offset) | | | 格式化日期 | SQL.std.formatDate(date, format) | | | 取日期中的年份 | SQL.std.yearOf(date) | | | 取日期中的月份 | SQL.std.monthOf(date) | | | 取日期中的日期 | SQL.std.dayOf(date) | | | 取两个日期之间的天数 | SQL.std.daysBetween(star, end) | | | 取两个日期之间的月数 | SQL.std.monthsBetween(star, end) | | | 取两个日期之间的年数 | SQL.std.yearsBetween(star, end) | | | 取两个日期之间的小时数 | SQL.std.hoursBetween(star, end) | | | 取两个日期之间的分钟数 | SQL.std.minutesBetween(star, end) | | | 取两个日期之间的秒钟数 | SQL.std.secondsBetween(star, end) | | | 获取加天数后的日期 | SQL.std.addDays(date, days) | | | 获取加月数后的日期 | SQL.std.addMonths(date, months) | | | 获取加年数后的日期 | SQL.std.addYears(date, years) | | | 获取加小时数后的日期 | SQL.std.addHours(date, hours) | | | 获取加分钟数后的日期 | SQL.std.addMinutes(date, minutes) | | | 获取加秒钟数后的日期 | SQL.std.addSeconds(date, seconds) | |

字符串函数

| 说明 | 函数 | 备注 | | -------------------------------- | -------------------------------------- | ---- | | 获取字符串字符数 | SQL.std.strlen(str) | | | 获取字符串字节数 | | | | 截取字符串 | SQL.std.substr(str, start, len) | | | 替换字符串 | SQL.std.replace(str, search, text) | | | 删除两侧空格 | SQL.std.trim(str) | | | 删除右侧空格 | SQL.std.trimEnd(str) | | | 转换成小写字母 | SQL.std.lower(str) | | | 转换成大写字母 | SQL.std.upper(str) | | | 获取字符串在另一个字符串中的位置 | SQL.std.strpos(str, search, startAt) | | | 获取一个字符的ascii码 | SQL.std.ascii(str) | | | 将一个ascii码转换成一个字符 | SQL.std.asciiChar(code) | | | 获取一个字符的unicode码 | SQL.std.unicode(str) | | | 将一个unicode码转换成一个字符 | SQL.std.unicodeChar(code) | |

数学函数

| 说明 | 函数 | 备注 | | -------- | ---- | ---- | | 求绝对值 | SQL.std.abs(value) | | | 指数曲线 | SQL.std.exp(value) | | | 向上取整 | SQL.std.ceil(value) | | | 向下取整 | SQL.std.floor(value) | | | 自然对数 | SQL.std.ln(value) | | | 对数 | SQL.std.log(value) | | | 圆周率(π) | SQL.std.pi() | | | 乘幂 | SQL.std.power(value, mi) | | | radians | SQL.std.radians(value) | | | degrees | SQL.std.degrees(value) | | | 随机数 | SQL.std.random(value) | | | 四舍五入 | SQL.std.round(value) | | | sign函数 | SQL.std.sign(value) | | | 开平方 | SQL.std.sqrt(value) | | | cos函数 | SQL.std.cos(value) | | | sin函数 | SQL.std.sin(value) | | | tan函数 | SQL.std.tan(value) | | | acos函数 | SQL.std.acos(value) | | | asin函数 | SQL.std.asin(value) | | | atan函数 | SQL.std.atan(value) | | | cot函数 | SQL.std.cot(value) | |

常用操作

| 说明 | 方法 | 备注 | | -------- | ---- | ---- | | 返回条件:是否存在表 | SQL.std.existsTable(tableName) | | | 返回条件:是不存在数据库 | SQL.std.existsDatabase(dbName) | | | 返回条件:是不存在视图 | SQL.std.existsView(viewName) | | | 返回条件:是不存在函数 | SQL.std.existsFunction(functionName) | | | 返回条件:是不存在存储过程 | SQL.std.existsProcedure(procedureName) | | | 返回条件:是不存在序列 | SQL.std.existsSequence(sequenceName) | | | 获取当前数据库 | SQL.std.currentDatabase() | | | 返回当前默认架构 | SQL.std.defaultSchema() | | | 获取序列下一个值 | SQL.std.sequenceNextValue(sequenceName) | |

数据库类型(DbType)

为了数据库兼容性,lubejs定义了一套中间数据类型DbType,一般情况下不建议直接使用相应方言的数据类型。

使用DbType

import { DbType, SQL } from 'lubejs';

// 将字面量 1 转换为boolean类型
const sql = SQL.select(SQL.literial(1).to(DbType.boolean))
// => SELECT CONVERT(1 as bit) AS [#column_1]

使用数据库原生类型来构造SQL

const sql = SQL.createTable('Person').as(builder => {
    builder.column('name', DbType.raw('text'))
})

DbType类型对应表

| 类型 | 对应JS类型 | 对应数据类型(mssql) | 说明 | | ------------------------------- | ------------------ | ------------------- | ---- | | DbType.int8 | Number | tinyint | | | DbType.int16 | Number | smallint | | | DbType.int32 | Number | int | | | DbType.int64 | BigInt | bigint | | | DbType.decimal(length) | Decimal | decimal | | | DbType.float | Number | float | | | DbType.double | Number | real | | | DbType.string(length) | String | nvarchar(x) | | | DbType.date | Date | date | | | DbType.datetime | Date | datetime | | | DbType.datetimeoffset | Date | datetimeoffset | | | DbType.binary | ArrayBuffer/Buffer | varbinary(x) | | | DbType.boolean | Boolean | bit | | | DbType.uuid | Uuid | UNIQUEIDENTIFIER | | | DbType.rowflag | ArrayBuffer | TIMESTAMP | | | (尚未支持) DbType.object | Object | nvarchar(max) | | | (尚未支持) DbType.array(dbType) | Array | nvarhcar(max) | |

行集(Rowset)

所有可以通过select from的对象均可称为行集,具体有以下内容:

  • 表/视图
  • SELECT子查询别名
  • WITH查询项
  • 表值函数返回值
  • 表变量

声明表对象

const personTable = SQL.table<Person>('Person');
const houseTable = SQL.table<House>('House')

为表对象添加别名

const p = SQL.table<Person>('Person').as('p');
const h = SQL.table<House>('House').as('h');

访问表字段

const sql = SQL.select(p.name).from(p)
// => SELECT p.name FROM Person p

添加字段别名

const sql = SQL.select(p.name.as('first_name')).from(p)
// => SELECT p.name as first_name FROM Person p

注意:表对象声明不是必须的,但如果不使用表对象,则每次使用该表时,都需要主动附带泛型类型(例如:SQL.select<Person>(SQL.star).from('Person')) 否则select语句会失去返回类型,而使用any代替

表达式(Expression)

在lubejs中所有表达式对象均由Expression类继承而来,其中包括:

  • 字段(Field)
  • 一元运算(UnaryOperation)
  • 二元运算(BinaryOperation)
  • 变量(Variant)
  • 标量函数调用(ScalarFuncInvoke)等)
  • 字面量(Literial)

使用表达式可以方便的进行运算操作,例如:

  • 加法运算
// 加法运算
p.age.add(1)
// => p.age + 1
  • 生成比较查询条件
// 生成比较查询条件
p.age.lte(18)
// => p.age >= 18

更多细节请参考WHERE条件

字面量表达式

通常情况下,我们可以直接使用JS传递字面量,lubejs会自动识别成结构化字面量,但是如果需要我们用到字面量进行计算的时候,因为JS值并不具有这些方法,我们可以使用SQL.literial(1)的方式来构建字面量SQL

// 查询18岁以上的人
const sql = SQL.select(p.star).from(p).where(SQL.literial(18).lt(p.age)); // => SELECT p.* FROM Person p WHERE 18 < p.age

表达式运算符清单(二元运算)

| 运算符 | 方法 | 说明 | | --------- | ------------------------------------------------------------ | ------------- | | + | SQL.add(left, right)Expression.prototype.add(value) | 加法运算 | | - | SQL.sub(left, right)Expression.prototype.sub(value) | 减法运算 | | * | SQL.mul(left, right)Expression.prototype.mul(value) | 乘法运算 | | / | SQL.div(left, right)Expression.prototype.div(value) | 除法运算 | | +(mssql) | SQL.concat(left, right)Expression.prototype.concat(value) | 字符串连接 | | %(mssql) | SQL.mod(left, right)Expression.prototype.mod(value) | 取模运算 | | &(mssql) | SQL.and(left, right)Expression.prototype.and(value) | AND位运算 | | |(mssql) | SQL.or(left, right)Expression.prototype.or(value) | OR位运算 | | ^(mssql) | SQL.xor(left, right)Expression.prototype.xor(value) | XOR位运算 | | >>(mssql) | SQL.shr(left, right)Expression.prototype.shr(value) | SHR右位移运算 | | <<(mssql) | SQL.shl(left, right)Expression.prototype.shl(value) | SHL左位移运算 | | >>(mssql) | SQL.xor(left, right)Expression.prototype.xor(value) | XOR位运算 |

表达式运算符清单(一元运算)

| 运算符 | 方法 | 说明 | | --------- | --------------- | --------- | | - | SQL.neg(expr) | 负号运算 | | ~(mssql) | `SQL.not(expr) | NOT位运算 |

我们还可以直接使用表达式来构建查询条件


查询条件(Condition)

JSON格式查询条件

JSON格式仅适用于单表查询情况下的简单的 =, in运算之间的and条件联接,一般用于快速查询某个对象,若要使用高级条件请看下文。

const sql = SQL.select(p.star).from(p).where({ name: '张三', age: [18, 19, 20] })
// => SELECT p.* FROM Person p WHERE p.name = '张三' AND p.age in (18, 19, 20)

比较条件

上面那条SQL可以使用这种写法代替

const sql = SQL.select(p.star).from(p).where(
    p.name.eq('张三')
    	.and(p.age.in(18, 19, 20))
)
// => SELECT p.* FROM Person p WHERE p.name = '张三' AND p.age in (18, 19, 20)

比较条件运算符

| 运算符 | 方法 | 说明 | | -------- | ------------------------------------------------------------ | --------- | | = | SQL.eq(left, right)Expression.prototype.eq(value) | 等于 | | <> | SQL.neq(left, right)Expression.prototype.neq(value) | 不等于 | | < | SQL.lt(left, right)Expression.prototype.lt(value) | 小于 | | > | SQL.gt(left, right)Expression.prototype.gt(value) | 大于 | | <= | SQL.lte(left, right)Expression.prototype.lte(value) | 小于等于 | | >= | SQL.gte(left, right)Expression.prototype.gte(value) | 大于等于 | | LIKE | SQL.like(left, right)Expression.prototype.like(value) | 近似匹配 | | NOT LIKE | SQL.notLike(left, right)Expression.prototype.notLike(value) | 不近似 | | IN | SQL.in(left, right)Expression.prototype.in(value) | 在...内 | | NOT IN | SQL.notIn(left, right)Expression.prototype.in(value) | 不在...内 |

逻辑条件

// AND 查询
const sql = SQL.select(p.star).from(p).where(
    p.age.lte(18)
    	.and(p.sex.eq('男'))
		.and(p.name.like('张%'))
)
// => SELECT p.* FROM Person p WHERE p.age >= 18 AND p.sex = '男' AND p.name like '张%'
// OR 查询
const sql = SQL.select(p.star).from(p).where(p.sex.eq('女').or(p.sex.eq('男')))
// => SELECT p.* FROM Person p WHERE p.sex = '女' OR p.sex = '男'

多个and条件等优化可读性:

// 查询张姓年满18岁的男性
const sql = SQL.select(p.star).from(p).where(
    SQL.and(
    	p.age.lte(18),
        p.sex.eq('男'),
        p.name.like('张%')
    )
)
// => SELECT p.* FROM Person p WHERE (p.age >= 18 AND p.sex = '男' AND p.name like '张%')

注意: 使用SQL.and/SQL.or构建的查询条件所返回的条件为分组条件

逻辑运算符

| 运算符 | 方法 | 说明 | | ------ | ------------------------------------------------------------ | -------- | | AND | SQL.and(...conditions)Condition.prototype.and(condition) | 与逻辑 | | OR | SQL.or(...conditions)Condition.prototype.neq(condition) | 或逻辑 | | NOT | SQL.not(condition) | 否定逻辑 |

分组条件

//
const sql = SQL.select(p.star).from(p).where(
    SQL.and(
        p.sex.eq('男'),
        p.name.like('张%'),
        SQL.group(
            p.age.lt('18').or(p.age.gte(60))
        )
    )
)
// => SELECT p.* FROM Person p
//    WHERE (p.sex = '男' AND p.name like '张%' AND (p.age < 18 OR p.age >= 60))

EXISTS子句

// 查询有房子的人
const sql = SQL.select(p.star).from(p).where(
    SQL.exists(SQL.select(h.id).from(h).where(h.personId.eq(p.id)))
)
// => SELECT p.* FROM Person p WHERE EXISTS(SELECT h.id FROM House h WHERE h.personId = p.id)

SQL构建

本单节会介绍怎么使用lubejs来构建sql语句,需要注意的是,本章节所讲的构建sql,并非执行sql,执行sql会在后续章节讲解。

定义表模型(仅typescript适用)

interface Person {
    // 因为是自增列,所以可空
    id?: number;
    name: string;
    age?: number;
    sex?: '男' | '女';
    description?: string;
}

interface House {
    id?: number;
    title: string;
    location?: string;
    description?: string;
}

声明表对象

const p = SQL.table<Person>('Person').as('p');
const h = SQL.table<House>('House').as('h');

构建Select语句

以下是一条完整的select语句

const sql = SQL.select({
    name: p.name,
    age: p.age
}).from(p).where(p.id.eq(1))
// => SELECT p.name as name, p.age as age FROM Person p WHERE p.id = 1

定义返回内容

JSON格式

const sql = SQL.select({
    name: p.name,
    age: p.age
}).from(p).where(p.id.eq(1))
// => SELECT p.name as name, p.age as age FROM Person p WHERE p.id = 1

使用此方法构建的sql会带上类型,在后续查询结果中亦会包含此类型。

整表返回(使用*号)

const sql = SQL.select(p.star).from(p)
// => SELECT p.* FROM Person p

使用 p.star 返回p本身所附带的类型,会被select所继承过去。

~~使用表达式(此功能已删除)~~

WHERE条件

请参考查询条件(Condition)章节

多表关联查询

多表查询

const sql = SQL.select({
    personName: p.name,
    houseTitle: h.houseTitle
})
	.from(p, h)
	.where(h.personId.eq(p.id))
// => SELECT p.name AS personName, h.title AS houseTitle FROM Person p, House h
//    WHERE h.personId = p.id

JOIN/LEFT JOIN查询

内联接查询

// 关联查询名下房产
const sql = SQL.select({
    personName: p.name,
    houseTitle: h.houseTitle
})
	.from(p)
	.join(h, h.personId.eq(p.id))
// => SELECT p.name AS personName, h.title AS houseTitle
//    FROM Person p INNER JOIN House h ON h.personId = p.id

左外联接查询

// 关联查询名下房产
const sql = SQL.select({
    personName: p.name,
    houseTitle: h.houseTitle
})
	.from(p)
	.leftJoin(h, h.personId.eq(p.id))
// => SELECT p.name AS personName, h.title AS houseTitle
//    FROM Person p LEFT OUTER JOIN House h ON h.personId = p.id

注意:考虑到SQL使用规范等因素,联接查询仅支持INNER JOINLEFT OUTER JION查询,不支持RIGHT JOIN等联接操作。

子查询

嵌套查询

// 查询名下房产数量
const sql = SQL.select({
    personName: p.name,
    houseCount: SQL.select(SQL.std.count(h.id)).from(h).where(h.personId.eq(p.id))
})
	.from(p)
// => SELECT
//       p.name AS personName,
//       (SELECT count(h.id) FROM House h WHERE h.personId = p.id) AS houseTitle
//    FROM Person p

子查询别名

相较于原生SQL,lubejs构建语法更为清晰,可读性更强。

const hc = SQL.select({
    personId: h.personId,
    houseCount: SQL.std.count(h.id)
}).from(h).groupBy(h.personId).as('hc')

const sql = SQL.select({
    personName: p.name,
    houseCount: hc.houseCount
})
	.from(p)
	.join(hc, hc.personId.eq(p.id))

// => SELECT p.name AS personName, hc.houseCount as houseCount
//    FROM Person p
//    JOIN (SELECT personId, count(h.id) AS houseCount FROM h GROUP BY h.personId) as hc

IN 子查询

const sql = SQL.select(h.star).from(h).where(h.personId.in(
    SQL.select(p.id).from(p).where(p.name.like('张%'))
))
// => SELECT h.* FROM House h WHERE h.personId IN (SELECT p.id FROM Person p WHERE p.name LIKE '张%')

分组查询(GROUP BY)

GROUP BY

// 查询名下房产数量
const sql = SQL.select({
    personName: p.name,
    houseCount: SQL.std.count(h.id)
})
	.from(p)
	.join(h, h.personId.eq(p.id))
	.groupBy(p.name)
// => SELECT p.name AS personName, COUNT(h.id) AS houseCount
//    FROM Person p JOIN House h ON h.personId = p.id
//    GROUP BY p.name

使用HAVING子句

// 查询名下房产大于等于2人员及房产数量
const sql = SQL.select({
    personName: p.name,
    houseCount: SQL.std.count(h.id)
})
	.from(p)
	.join(h, h.personId.eq(p.id))
	.where(h.location.eq('广州'))
	.groupBy(p.name)
    .having(SQL.std.count(h.id).gte(2))

// => SELECT p.name AS personName, COUNT(h.id) AS houseCount
//    FROM Person p JOIN House h ON h.personId = p.id
//    WHERE h.location = '广州'
//    GROUP BY p.name
//    HAVING COUNT(h.id) >= 2

使用With语句查询

const adult = SQL.select(p.star).from(p).where(p.age.gte('18')).asWith('adult')
const sql = SQL.with(adult).select(a.start).from(adult.as('a'))
// => WITH adult as (SELECT p.* FROM Person p WHERE p.age >= 18)
//      SELECT a.* FROM adult a

构建Insert语句

单条插入

// 单条插入
const sql = SQL.insert(personTable).values({
    name: '张三',
    age: 23,
    sex: '男',
    description: '这个可有可无'
});

// => INSERT INTO Person(name, age, sex, description) VALUES ('张三', 23, '男', '这个可有可无')

多条插入

// 多条插入
const sql = SQL.insert(personTable).values([
    {
        name: '张三',
        age: 23,
        sex: '男',
        description: '这个可有可无
    },
    {
        name: '李四',
        age: 43,
        sex: '男',
        description: '这个可有可无
    }
]);
// => INSERT INTO Person(name, age, sex, description)
//    VALUES ('张三', 23, '男', '这个可有可无'),
//           ('李四', 43, '男', '这个可有可无')

注意:插入表亦可以使用别名,但别名不会被转换为SQL,而是直接使用表名

构建UPDATE语句

使用表名更新

const sql = SQL.update(personTable).set({
    age: personTable.age.add(1) // 大了一岁咯
}).where(personTable.name.eq('张三'))

// => UPDATE Person SET age = p.age + 1 WHERE Person.name = '张三'

使用别名更新

// 这些在广州有房产的人大了一岁了
const sql = SQL.update(p).set({
    age: p.age.add(1) // 大了一岁咯
})
	.from(p)
    .join(h, h.personId.eq(p.id))
    .where(h.location.eq('广州'))

// => UPDATE p SET age = p.age + 1
//    FROM Person p
//    JOIN House h ON h.personId = p.id
//    WHERE h.location = '广州'

构建DELETE语句

使用表名删除

const sql = SQL.delete(personTable).where(personTable.age.gt(60))
// => DELETE Person WHERE Person.age > 60

使用别名删除

// 删除在广州有房产的人的数据
const sql = SQL.delete(p)
	.from(p)
    .join(h, h.personId.eq(p.id))
    .where(h.location.eq('广州'))
// => DELETE p
//    FROM Person p
//    JOIN House h ON h.personId = p.id
//    WHERE h.location = '广州'

函数调用

存储过程调用

假设我们有以下存储过程

CREATE PROCEDURE sp_get_person
(
	@type VARCHAR(20) = 'all',
    @total INT OUTPUT
)
AS
BEGIN
	SELECT @total = COUNT(p.id) FROM Person;
	IF (@type = 'audlt')
		SELECT * FROM Person p WHERE p.age >= 18;
	ELSE IF (@type = 'children')
		SELECT * FROM Person p WHERE p.age < 18;
	ELSE IF (@type = 'aged')
		SELECT * FROM Person p WHERE p.age >= 50;
	ELSE
		SELECT * FROM Person;
	END

	RETURN  100;
END

快速调用

const sql = SQL.execute<number, Person>('sp_get_person', ['children', 0]);

参数调用(当希望获取输出参数时)

const params: Parameter[] = [
    SQL.input('type', 'audlt'),
    SQL.output('total', DbType.int32)
]
const sql = SQL.execute<number, Person>('sp_get_person', ['children', 0]);
const result = await db.query(sql);

console.log(result.returnValue); // => 100
console.log(result.rows); // SELECT 查询结果
console.log(result.output.total); // => 表Person行数

创建存储过程声明

存储过程是一个重复调用的过程,为了使用更加方便,我们还可以创建一个函数来快速的调用它,并使它爱类型保护

// 将SQL存储过程声明为一个Typescript函数
const sp_get_person = SQL.makeExec<'audlt' | 'children' | 'aged' | 'all', number, [Person]>('sp_get_person');

// 然后我们可以这样调用来达到与快速调用一样的效果。
const sql = sp_get_person('children', 0);

使用SQL字符串构建语句

通常情况下我们不建议使用此种方式来构建SQL,因为该方式构建SQL将脱离Typescript类型监控。

其它语句

创建表

const sql = SQL.createTable('Person').as(({ column }) => [
        // 标识列
        column('id', DbType.int32).notNull().primaryKey().identity(),
        column('name', DbType.string(100)).notNull(),
        column('age', DbType.int32).null(),
        column('sex', DbType.string(2)).null(),
        column('description', DbType.string(100)).null(),
	    // 默认值 getDate()
	    column('createDate', DbType.datetime).default(SQL.std.now())
    ])
// => CREATE TABLE Person(
//   id int not null primary key identity(1, 1),
//   name nvarchar(100) not null,
//   age int null,
//   sex nvarchar(2) null,
//   description nvarchar(100) null
//   createDate datetime default (sysdate())
// )

修改表

  • 添加列
const sql = SQL.alterTable('Person').addColumn(column => column('rowflag', DbType.rowflag).notNull())
// => ALTER TABLE Person add column rowfloag TIMESTAMP NOT NULL
  • 删除列
const sql = SQL.alterTable('Person').dropColumn('rowflag')
// => ALTER TABLE Person drop column rowfloag
  • 创建外键
const sql = SQL.alterTable('House').addForeignKey(
    fk => fk('FK_HOUSE_PERSON').on('personId').reference('Person', ['id'])
)
// => ALTER TABLE Person ADD FOREIGN KEY FK_HOUSE_PERSON ON personId REFERENCE Person(id)
  • 删除外键
const sql = SQL.alterTable('House').dropForeignKey('FK_HOUSE_PERSON')

创建索引

const sql = SQL.createIndex('IX_Person_name').on('Person', ['name']);
// => CREATE INDEX IX_Person_name ON Person(name)

删除索引

const sql = SQL.dropIndex('Person', 'IX_Person_name')
// => DROP INDEX IX_Person_name

注意:由于各种数据库方言在该行为上不一致,因此在此处需要传递表名

由于篇幅所限,本章节不再介绍其余的语句使用方法,具体请翻阅《api参考》。

Lubejs 所支持的语句

数据操作语句

| 语句 | 使用 | 说明 | | ----------------------------------- | ------------------------------------------------------------ | ------------------------- | | insert | SQL.insert(...).values(...) | | | update | SQL.update(...).set(...).from(...).where(...) | | | select | SQL.update(...).from(...).where(...)更多高级用法请参考构建SELECT语句 | | | delete | SQL.delete(...).from(...).where(...) | | | case when ... then ... else ... end | SQL.case(...).when(...).else() | CASE语句 | | execute | SQL.execute(...)SQL.proc(...).execute(...) | 存储过程调用 | | | SQL.makeExec(...) | 将SQL存储过程声明为JS函数 | | invoke | SQL.invokeAsScalar(...)SQL.invokeAsTable(...) | 函数调用 | | | SQL.makeInvoke(...) | 将SQL函数声明为JS函数 |

数据结构操作语句

| 语句 | 使用 | 说明 | | ---------------- | ------------------------------------------------------------ | ---- | | create table | SQL.createTable(...).as(...)更多高级用法,请参考《创建表》 | | | altert table | SQL.alterTable(...).as(...)更多高级用法,请参考《修改表》 | | | drop table | SQL.dropTable(...) | | | create view | SQL.createView(...).as(...) | | | altert view | SQL.alterView(...).as(...) | | | drop view | SQL.dropView(...) | | | create procedure | SQL.createProcedure(...).as(...) | | | altert procedure | SQL.alterProcedure(...).as(...) | | | drop procedure | SQL.dropProcedure(...) | | | create function | SQL.createFunction(...).as(...) | | | altert function | SQL.alterFunction(...).as(...) | | | drop function | SQL.dropFunction(...) | | | create sequence | SQL.createSequence(...).as(...).startWith(...)incermentBy(...) | | | drop sequence | SQL.dropSequence(...) | | | create database | SQL.createDatabase(...).collate(...) | | | altert database | SQL.alterDatabase(...).collate(...) | | | drop database | SQL.dropDatabase(...) | | | create index | SQL.createIndex(...).on(...) | | | drop index | SQL.dropIndex(...) | |

程序控制语句

| 语句 | 使用 | 说明 | | -------------- | --------------------------------- | ---- | | if..then..else | SQL.if(...).then(...).else(...) | | | while | SQL.while(...).do(...) | | | begin ... end | SQL.block(...) | | | break | SQL.break() | | | return | SQL.return(...) | | | continue | SQL.continue | |

SQL执行

创建表

import { SQL, DbType, connect } from 'lubejs';

async (() => {
	const db = await connect('mssql://user:password@localhost:1433');
	await db.query(SQL.createDatabase('test-database'));
    await db.changeDatabase('test-database');
    await db.query(SQL.createTable('Person').as(({ column }) => [
        column('id', DbType.int32).notNull().primaryKey().identity(),
        column('name', DbType.string(100)).notNull(),
        column('age', DbType.int32).null(),
        column('sex', DbType.string(2)).null(),
        column('description', DbType.string(100)).null()
    ]));
    await db.close();
})();

使用连接(Connection类)

Connection类是整个数据库层的基础,它还封闭了许多比SQL构造更为易用的方法,如(insert、update、select、delete等)

你可以使用以下语句创建一个数据库连接:

const db = await connect('mssql://user:password@localhost:1433/database');

执行查询(.query)

使用构造SQL查询

const sql = SQL.select(1);
const result = await db.query(sql);
console.log(result); // => { rows: [{ '#column_1': 1 }] }

同时传递参数

const sql = SQL.select(SQL.input('@p', 1));
const = await db.query(sql);
console.log(result); // => { rows: [{ '#column_1': 1 }] }

获取输出参数

const p1 = SQL.output('p', DbType.int32);
const sql = SQL.select(SQL.assign(p1, 1));
const = await db.query(sql); // => SELECT @p = 1;
// 在输出值列表中获取其值
console.log(result.output['p']); // => 1
// 亦可以使用原参数获取其值
console.log(p1.value); // => 1

使用原始SQL字符串查询

使用SQL字符串

const sql = 'SELECT 1 AS [#column_1]';
const result = await db.query(sql);
console.log(result); // => { rows: [{ '#column_1': 1 }]} }

注意:原始SQL字符串查询,lubejs不会为其指定字段名。

同时传递参数

const sql = 'SELECT @p1 AS [#column_1]';
const result = await db.query(sql, [ 1 ]);
console.log(result); // => { rows: [{ '#column_1': 1 }]} }

返回值:

  • 类型: QueryResult<T, R, O>
  • 结构
{
    rows: T[];                  // 查询语句第一个返回的数据集
    returnValue: R;             // 返回值,通过为存储过程的返回值
    rowsets: O;                 // 如果SQL返回多个数据集,则存储在此属性中,该对象为一个数组,第一个值为.rows的引用
    rowsAffected: number;      // 受影响函数
    output: Record<string, Scalar>;   // 返回输出参数值
}

注意: 如果数据库驱动本身不支持多数据集返回,则rowsets只能存在.rows的引用一个元素。

查询单个值(.queryScalar)

查询单个值

const sql = SQL.select(1);
const result = await db.queryScalar(sql);
console.log(result); // => 1

使用参数与传递参数与.query方法一致

使用构建表达式查询

我们还可以直接使用表达式查询值,lubejs会自动为其构建为SELECT语句进行查询,并返回第一个值。

const result = await db.queryScalar(SQL.literial(1));
console.log(result); // => 1

插入数据(.insert)

使用表对象查询

await db.insert(personTable, {
    name: '张三',
    age: 42,
    sex: '男',
    description: '这个可有可无'
});

直接使用表名插入

await db.insert<Person>('Person', {
    name: '张三',
    age: 42,
    sex: '男',
    description: '这个可有可无'
});

注意:使用表名插入并且不指定泛型类型时,会失去类型检查

查找单行(.find)

查询单条记录:

const row = await db.find(personTable, { name: '张三' });
console.log (row);
// => {
//    id: 1,
//    name: '张三',
//    age: 42,
//    sex: '男',
//    description: '这个可有可无'
// }

选择多行(.select)

const rows = await db.select(personTable, {
    where: {
        name: '张三'
    }
});

console.log(rows);
// => [{
//    id: 1,
//    name: '张三',
//    age: 42,
//    sex: '男',
//    description: '这个可有可无'
// }]

指定返回字段

const rows = await db.select(personTable, {
    fields: ['name']
    where: {
        name: '张三'
    }
});

console.log(rows);
// => [{
//    name: '张三'
// }]

删除数据(.delete)

await db.delete<Person>('Person', {
    id: 1
});

更新数据(.update)

await db.update<Person>('Person', {
    age: 43 //大了一岁咯
}, { id: 1 });

输出SQL日志(.on('command', handler))

如果您需要知道Connection干了什么,可以使用以下方法输出日志

db.on('command', cmd => {
    //
    console.log('SQL: ' + cmd.sql);
    // 调用时所传递的参数
    console.log('PARAMS:' + JSON.stringify(cmd.params));
})

配置文件

许多情况我们可能不希望把连接字符串或配置写在代码中,或者我们也需要使用数据库迁移工具,这时候我们可以配置配置文件,lubejs的配置文件名称为.lubejs.ts/.lubejs.js,因为需要引用驱动等原因,不支持使用JSON格式配置。

配置文件结构为:

// 引入配置类型,js中可以忽略
import { LubeConfig } from 'lubejs';
// 引入驱动
import 'lubejs-mssql';
import './orm-configure'
// import 'orm';

export const config: LubeConfig = {
  // 默认配置项名称, 当使用 connect 函数创建连接时,如果不传递参数,则自动使用该配置,名称必须是`configures`中存在的节点名称
  default: 'lubejs-test',
  // 数据迁移文件夹,用于存放数据迁移代码,在数据迁移时会用到,默认为 `migrates`
  migrateDir: 'migrates',
  // 配置项
  configures: {
    'lubejs-test': {
      // 使用驱动名称,需要引入驱动后方可使用
      dialect: 'mssql',
      // 数据库服务器名称,亦可以是IP地址
      host: 'rancher.vm',
      // 使用用户名
      user: 'sa',
      // 密码
      password: 'your!password,
      // 数据库名称
      database: 'lubejs-orm-test',
      port: 1433,
    }
  }
};

export default config;

ORM

概念

lubejs的ORM与标准的ORM模型基本一致,但其中也包括了一些,比如全局主键类型

全局主键类型(EntityKey)

lubejs为了加强类型管理,定义了主键类型规范:

一个实体,有且仅能有一个属性作为主键

lubejs会在EntityKey接口中去获取该属性类型,用作主键的类型,用于Repository.prototype.get等方法的类型检查。因此实体类必须实现EntityKey接口。

默认情况下,EntityKey为一个空接口,这时主键的类型为Scalar类型,如果不去定义EntityKey接口,我们调用Repository.prototype.get时是这样的

await repo.get(1);
await repo.get('1');
await repo.get(new Date());
// 以上均能通过类型检查

可以看到我们将失去更为精确的类型检查。

因此,在声明实体之前,我们可以使用Typescript声明合并特性,来定义EntityKey接口

// 声明全局实体主键
declare module 'lubejs' {
    export interface EntityKey {
        // 如果为自动生成,需要声明为可空,否则使用.insert时可能通不过语法检查。
        id?: number;
    }
}

如果我们不想在每个实体中重复定义主键,我们还可以使用以下方式为所有的实体类隐式声明主键属性:

contextBuilder.hasGlobalKey('id', Number);

实体类(Entity)

实体是lubejs orm操作数据库的基本单位,如果我们需要通过Repository操作数据库,则必须先定义实体类。

实体类又分为以下几种:

  • 表实体,映射到数据库中是一个表
  • 视图实体,映射到数据库中是一个视图
  • 查询实体,该实体仅是一个只读的查询SQL,并不能映射到数据库中的对象

所有用户自定义实体类,可以从Entity类继承,亦可以不从Entity类继承,但是实体必须为Class,因为Typescript中的ineterfacetype将在编译后被删除,并且不可以对interface使用装饰器,但是:不从Entity继承的实体类,没有静态方法.create方法

在下面的章节中,会讲解如何创建实体。

导航属性(关联关系属性)

实体类可以声明导航属性,导航属性可以为我们表与表之间的关联查询提供非常使得的操作。

例如:我们为User实体类声明了一个 **一对一(主)**导航属性,我们获取User实体数据的时候使用以下代码同时获取Employee实体:

const user = await userRepo.get(1, { includes: { employee: true }});
// => { id: 1, ..., employee: { userId: 1, ... }}

在我们创建实体类并使用导航属性时,会有以下特性:

  • 导航属性是双向的,即使在另一个实体中未声明导航属性,建模器亦会为其声明一个隐式的导航属性,隐式声明的属性名称约定请参考关联关系
  • 导航属性中属性从属关系的实体,需要外键属性,即使外键属性未被声明,建模器亦会为其声明一个隐匿的外键属性,隐式声明的属性名称约定请参考关联关系

另外,导航属性还会有关联保存的特性,具体请参考:仓储对象(Repository)

外键属性

实体中引用另外一个实体主键属性的属性,我们称之为外键属性。

例如:Employee实体中的userId属性,引用了User表的主键属性iduserId就是一个外键属性。

隐式属性

导航属性外键属性还可以被隐式声明,即不在实体类中声明,但是由建模器自动声明,在查询时隐式外键属性会随实体一并返回,而隐式导航属性则需要特殊方法绕过Typescript语法检查才可获取(在后续章节会讲到)。并且在返回的数据中隐式导航属性及隐式外键属性均是不可枚举。通过Object.keys(obj)不会返回该属性,使用JSON.stringify(obj)对其序列化时,该属性也不会被序列化。

隐式导航属性命名约定如下:

  • 当导航属性为单一引用对象时,取被引用的表名(例:Employee),首字母小写,当该实体已经存在该同名属性时(例: employee),则直接使用该属性。

  • 当导航属性为列表对象引用(如:一对多、多对多)时,取被引用的表名(例Employee),首字母小写,并转换为复数(例:employees),当该实体已经存在该同名属性时,则直接使用该属性。

隐式外键属性命名约定如下:

  • 取被引用表名(此例为为User),首字母小写,再加上Id后缀为外键属性名,如果该实体已经存在该同名属性时,则直接使用该属性

注意:无论是自动声明导航属性,还是自动声明外键属性,如果实体已经存在同名属性,建模器会直接使用该属性,并且该属性不再是隐式属性,如果类型不符,lubejs并不会报错提示(JS/TS类型反射功能缺失原因),因此要特别注意这种方式声明的属性的类型,避免踩坑,,强烈建议使用显式声明或者完全隐式声明。

数据库上下文(DbContext)

一般情况下DbContext类对应着一个数据库。DbContext的声明并不是必须的,如果不声明DbContext,我们可以直接使用DbContext类来访问数据库。

我们可以直接使用DbContext实体来操纵数据,其使用方法与Repository基本一致,请参考仓储对象(Repository)

而DbContext,不可以直接查询数据,必须通过Queryable查询。

创建上下文类实例

我们可以直接使用 createContext 方法来创建DbContext实例;

import { createContext } from 'lubejs';
import { DB } from './db';

// 第一个参数为DbContext类构造函数,第二个参数为连接配置
const ctx = await createContext(DB, 'mssql://user:password@localhost:1433/database');

使用配置文件创建

当我们不传递构配置时,lubejs会自动根据配置文件中对应DbContext类名的配置来创建。因此我们要特别注意DbContext的名称不要冲突。

import { createContext } from 'lubejs';
const ctx = await createContext(DB);

创建默认的DbContext类实例

这里需要注意的是,默认的DbContext不一定就是lubejs内置的DbContext类,moduleBuilder注册的第一个DbContext会自动替代lubejs内置的DbContext类成为默认DbContext。当不传递任何参数调用createContext的时候,系统会创建一个默认的DbContext(如果未指定,则为lubejs内置DbContext类)。

import { createContext } from 'lubejs';
const ctx = await createContext();

注意:使用配置文件方式创建前,需要配置与上下文类名称相对应的连接配置

可查询对象(Queryable)

Queryable实现了所有的查询功能,并且同时它本身还是一个异步遍历器,你可以像使用列表。其API设计也参考了Javascript原生的Array对象,许多用法上都非常相似。

为了优化性能,Queryable拥有延迟执行特性,仅有在调用.fetchAll(),和.fetchFirst()或者使用异步遍历器for await (const item of userQuerable)去遍历Queryable对象时时才会真正的从数据库查询数据。

获取可查询对象实例
const userQueryable = ctx.getQueryable(User);
获取所有数据(fetchAll)
const allUsers = await userQueryable.fetchAll();
获取第一行(fetchFirst)
const user = await userQueryable.fetchFirst();
过滤数据(filter)

Queryable.prototype.filter函数会传递一个Rowset对象,用于用户构造过滤条件,只需要返回一个查询条件Condition即可过滤数据

const adminUser = await userQueryable.filter(p => p.name.eq('admin')).fetchFirst();
获取关联数据(include)

Queryable.prototype.include 指定要查询的子项

const user = userQueryable.include({
    employee: true
}).fetchFirst()
// user => {
//   name: '...',
//   // ...
//   employee: {
//     //...
//   }
// }

即使是多级关联数据,也可以一次性查询

const user = userQueryable.include({
    employee: {
        positions: true
    }
}).fetchFirst();
// user => {
//   name: '...',
//   // ...
//   employee: {
//     //...
//     positions: [...]
//   }
// }
使用异步遍历器遍历可查询对象
for await (const item of userQuerable) {
    const console.log(item);
}

仓储对象(Repository)

仓储对象主要提供数据获取、插入、更新、保存等功能。

获取一个User实体的仓库对象
const userRepo = ctx.getRepository(User);
使用异步遍历器遍历仓库对象
for await (const item of userRepo) {
    const console.log(item);
}
获取单个实体数据(get)

传递键值,获取实体数据,当获取一个不存在的数据时,将抛出异常。如果不希望抛出异常,请使用Queryable.prototype.filter方法查询。

使用DbContext获取

const user = await ctx.get(User, 1);
// user 为 User类的实例

使用Repository获取

const user = userRepo.get(1);
// user 为 User类的实例

通过.get获取的对象,本身是实体类的实例

插入实体数据(insert)

在插入实体实例时,如果实体实例的导航属性有值,还会对导航属性的对象进行保存操作。

使用DbContext插入

指定实体构造函数插入

await ctx.insert(User, {
    name: 'admin',
    password: '123456'
})

亦可以直接使用实体类实例进行插入

await ctx.insert(User.create({
    name: 'admin',
    password: '123456'
}))

使用Repository插入

使用JSON对象插入

await userRepo.insert({
    name: 'admin',
    password: '123456'
})

亦可以直接使用实体类实例进行插入

await userRepo.insert(User.create({
    name: 'admin',
    password: '123456'
}))
更新实体数据(update)

在所有数据更新的操作中,lubejs均是依据主键是否存在来判断数据库中是否存在该数据,在更新实体数据时,如果实体实例的导航属性有值,还会对导航属性的对象进行保存操作。

使用DbContext更新

指定实体构造函数更新

const user = await ctx.get(User, 1);
user.password = 'changed!password';
await ctx.update(User, user)

亦可以直接使用实体实例进行更新

// .get所返回的本身就是实体类实例
const user = await userRepo.get(User, 1);
user.password = 'changed!password';
await userRepo.update(user);

update 仅会修改数据,如果数据库中不存在该数据,则会抛出异常。

使用Repository更新

const user = await userRepo.get(1);
user.password = 'changed!password';
await userRepo.update(user);

update 仅会修改数据,如果数据库中不存在该数据,则会抛出异常。

并发问题处理

在前面的章节提到过在使用Repository保存数据时有数据被覆盖的风险,因此我们需要处理并发问题,我们推荐的解决方案是为存在并发可能的实体添加rowflag属性,当实体存在rowflag属性,在update时会自动附加rowflag作为条件更新,当更新不到数据时,会抛出异常。

删除实体数据(delete)

delete 仅会删除数据,如果数据库中不存在该数据,则会抛出异常。考虑到使用安全性,delete操作时,不会对任何导航属性进行操作,因此,不可以使用delete来保存导航属性的数据。

并发问题

为了避免覆盖,建议为存在并发可能的实体类添加rowflag属性。

使用DbContext删除

指定实体构造函数删除

const user = await ctx.get(User, 1);
await ctx.delete(User, user)

亦可以直接使用实体实例删除

const user = await ctx.get(User, 1);
await ctx.delete(user);

使用Repository删除

指定实体构造函数删除

const user = await userRepo.get(1);
await userRepo.delete(user)

亦可以直接使用实体实例删除

const user = await userRepo.get(1);
await userRepo.delete(user);
保存实体数据(save)

你可能会疑惑 .update.save方法,为什么保存数据会有两个方法,事实上.save提供了更为高级的功能。如果存在关联属性,并且该关联属性值不为undefined时,.save方法会对关联属性进行分析一并提交,并且update仅会更新数据,并不会自动插入/更新。

使用DbContext保存

const user = await ctx.get(User, 1, { includes: { employee: true }});
user.password = 'changed!password';
user.employee.description = '该职员更改了密码'

await ctx.save(user);
// 同时保存了 user 及 user.employee

使用Repository保存

const user = await userRepo.get(1, { includes: { employee: true }});
user.password = 'changed!password';
user.employee.description = '该职员更改了密码'

await userRepo.save(user);
// 同时保存了 user 及 user.employee

对于当前对象的保存规则如下:

  • 当数据库中存在该数据时,进行更新操作
  • 当数据库中不存在该数据时,进行插入操作

关联属性保存规则如下:

  • 当关联属性为undefined时,不对关联属性进行任何操作
  • 当关联属性为**一对一(主)**关系时
    • 当该属性有值时,对该值进行(插入/更新)操作
    • 当该属性为null时,对其引用数据进行删除操作
  • 当关联属性为**一对一(从)**关系时
    • 当该属性为null时,删除引用关系(将外键设置为DBNULL)
    • 当该属性存在值时,对其引用数据进行插入/更新操作
  • 当关联属性为 一对多关系时
    • 当该属性有值时,分析数据库中现有数据,对存在与该属性并存在于数据库中的数据进行更新操作,对不存在的数据并存在于该属性的数据进行插入操作,对数存在于数据库而不存在于该属性中的数据进行删除操作。
    • 当该属性为null或者为[]时,对数据库中已有的关联的数据进行清空操作。
  • 当关联属性为多对多关系时
    • 当该属性有值时,分析中间关系表数据库中现有数据,对存在与该属性并存在于数据库中的数据进行更新操作,对不存在的数据并存在于该属性的数据进行插入操作,对数存在于数据库而不存在于该属性中的数据进行删除操作。同时对关联表的数据进行更新操作。(不建议在此进行关联表的更新操作,如果同时存在删除关系及更新数据时,仅会删除中间关系表中的关联数据,而不会更新目标表的数据)
    • 当该属性为null或者为[]时,对关联中间表数据库中的数据进行清空操作。

当关联属性为多级时,该规则亦适用。

注意:使用保存数据时要重点考虑并发情况,否则将存在数据被覆盖可能 。

更多范例,请看关联关系

建模

ORM建模有以下两种方式:

  • 使用装饰器声明,目前使用装饰器声明的实体不能通过类的继承获得装饰器配置信息,但这只是暂时的,在下一个小版本更新时我们会为lubejs添加此功能。
  • 使用api声明

这两种声明方式亦可以混合使用,装饰器声明方式会优先执行,api声明则会合并/覆盖(如果允许)装饰器的声明。

注意: 装饰器模式只支持Typescript,并且需要在tsconfig.json中必须打开experimentalDecoratorsemitDecoratorMetadata选项,否则将无法获取属性类型导致报错。

创建实体类文件

表实体(装饰器声明)

声明一个User实体类

import { DB } from '../index'
import {
  column,
  comment,
  context,
  Entity,
  EntityKey,
  identity,
  key,
  nullable,
  oneToOne,
  principal,
  table,
} from 'lubejs';

@comment('用户表')   // 声明批注,该批注会进入数据库扩展属性/批注中。
@table()           // 声明为表实体
@context(() => DB) // 将实体绑定到DbContext
@data([            // 声明种子数据,在执行数据迁移时会被自动初始化到数据库,在初始化数据时会自动开启标识列插入。
    { id: 1, name: 'admin', password: '123456' }
])
export class User extends Entity implements EntityKey {
    // 声明为列
	@column()
    // 声明为主键
  	@key()
    // 声明为标识列
    @identity()
    @comment('ID')
    id?: bigint;

    @comment('UserName')
    @column()
    name!: string;

    @comment('Password')
    // 声明为可空,不声明时默认为可空
    @nullable()
    @column()
    password?: string;

    @comment('Description')
    @nullable()
    @column()
    description?: string;
}

你可能会疑惑为什么引用DB类时需要使用箭头函数,事实上这里可能会存在noedjs循环引用问题,当我们将类独立存放于单个文件中的时候,声明DB类的文件中引用了User类文件中,而声明User类的文件中亦引用了DB类,因此使用函数可以延迟执行,待所有类都创建完毕后执行,从而不会造成访问类时获取一个undefined值,在实体与实体之间的引用关系亦存在此问题。

表实体(API声明)
import {
  modelBuilder,
  DbContext,
  Repository,
  DbType,
  Entity,
  SQL,
  EntityKey,
  Binary,
  Decimal,
} from 'lubejs';

/**
 * 用户实体类
 */
export class User extends Entity implements EntityKey {
  id?: bigint;
  name!: string;
  password!: string;
  description?: string;
  employee?: Employee;
}

modelBuilder.context(DB, context => {
context
    // 声明实体
    .entity(User)
    // 将实体声明为表
    .asTable(table => {
      // 添加表批注
      table.hasComment('Employee');
      table
        // 声明列
        .property(p => p.id, BigInt)
        // 声明标识列
        .isIdentity()
        // 添加列批注
        .hasComment('ID');
      table.property(p => p.name, String).hasComment('EmployeeName');
      table
        .property(p => p.password, String)
        // 声明为可空列
        .isNullable()
        .hasComment('Password');
      table
        .property(p => p.description, String)
        .isNullable()
        .hasComment('Description');
      // 声明主键
      table.hasKey(p => p.id).hasComment('PrimaryKey');
      // 声明种子数据
      table.hasData([{ id: 0, name: 'admin' }]);
    })
});

关联关系

本章节内容会讲到关联关系的保存范例,请先阅读保存实体数据,有助于理解本节内容。

一对一关系(主)

假设我们有两个实体: UserEmployee,外键Employee.userId引用了User.id。User的实体声明如下:

显式声明导航属性

假设实体Employee已经声明了user属性的情况,可以按以下方式声明。

// # entities/user.ts
// ....其它导入
import { Employee } './employee'

@comment('User')   // 声明批注,该批注会进入数据库扩展属性/批注中。
@table()           // 声明为表实体
@context(() => DB) // 将实体绑定到DbContext
@data([            // 声明种子数据,在执行数据迁移时会被自动初始化到数据库,在初始化数据时会自动开启标识列插入。
    { id: 1, name: 'admin', password: '123456' }
])
export class User extends Entity implements EntityKey {
    // 声明为列
	@column()
    // 声明为主键
  	@key()
    // 声明为标识列
    @identity()
    @comment('ID')
    id?: bigint;

    // ... 其它属性

    @detail() // 声明为明细属性
    @principal()  // 将该属性声明为主要的一对一关系
    @oneToOne(() => Employee, p => p.user) // 声明一对一关系
    employee?: Employee;
}

隐式声明导航属性

假设实体Employee并未声明user属性,下面的例子会自动为其声明隐式**一对一(从)**导航属性user

// # entities/user.ts
// ....其它导入
import { Employee } './employee'

@comment('User')   // 声明批注,该批注会进入数据库扩展属性/批注中。
@table()           // 声明为表实体
@context(() => DB) // 将实体绑定到DbContext
@data([            // 声明种子数据,在执行数据迁移时会被自动初始化到数据库,在初始化数据时会自动开启标识列插入。
    { id: 1, name: 'admin', password: '123456' }
])
export class User extends Entity implements EntityKey {
    // 声明为列
	@column()
    // 声明为主键
  	@key()
    // 声明为标识列
    @identity()
    @comment('ID')
    id?: bigint;

    // ... 其它属性

    @principal()  // 将该属性声明为主要的一对一关系
    @oneToOne(() => Employee) // 声明一对一关系
    employee?: Employee;
}

一对一关系还可以将导航属性声明为明细属性,声明为明细属性后,我们

获取关联属性

const user = await userRepo.get(1, { includes: { employee: true } });
// => { id: 1, ..., employee: { userId: 1, ... }}

创建同时保存关联属性

const user = User.create({
    name: 'admin',
    // ...
    employee: {
        name: '管理员',
        description: '关联创建'
    }
});
await userRepo.insert(user);
// 同时会保存User以及Employee

删除关联关系

要删除关联关系,必须在一对一(从)实体进行操作,具体保存规则,请参考保存实体数据(save)

一对一关系(从)

我们继续上一个例子,创建实体文件 entities/employee.ts,一对一关系(从)

显式声明外键及导航属性

// # entities/employee.ts
// 其它导入...
import { User } from './user'

@table()
@comment('Employee')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
  @column()
  @key()
  @comment('EmployeeID')
  @identity()
  id?: bigint;

  // 其它属性...

  // 外键属性
  @column()
  @comment('UserID')
  userId?: bigint;

  // 声明 一对一(从)属性
  @foreignKey('userId')  // 声明外键
  @oneToOne(() => User, p => p.employee)  // 声明一对一关系
  user?: User | null;  // 如果orderId字段是可空的,需要声明为可以为null,用于断开连接关系
}

隐式声明外键及导航属性

外键属性可以有建模器自动创建,不需要显示声明,例如:

// # entities/employee.ts
// 其它导入...
import { User } from './user'

@table()
@comment('Employee')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
  @column()
  @key()
  @comment('EmployeeID')
  @identity()
  id?: bigint;

  // 其它属性...

  // 声明 一对一(从)属性
  @foreignKey()  // 声明为外键一对一属性
  @oneToOne(() => User)  // 声明一对一关系,
  user?: User | null;  // 如果orderId字段是可空的,需要声明为可以为null,用于断开连接关系
}

假设User实体中不声明导航属性employee,建模器将在User实体中自动创建该隐式导航属性(一对一(主)) employee,同时建模器还会在Employee实体中创建外键属性userId

在一对一关系中,隐式导航属性的创建规则如下:

同时插入一对一(主)实体数据

const employee = Employee.create({
    name: '管理员',
    // ...
    user: {
        name: 'admin',
        password: '123456'
    }
});
await employeeRepo.save(employee);
// 同时插入了Employee及User

删除关联关系

const employee = await employeeRepo.get(1);
employee.user = null;
await employeeRepo.save(employee);
一对多关系

一对多关系与多对一关系关联。

显示声明导航属性

假设 实体OrderDetail已经声明多对一导航属性order,我们可以通过以下方式使其关联。

// 其它导入...
import { OrderDetail } from './order-detail'

/**
 * Order
 */
 @table()
 @context(() => DB)
 @comment('Order')
 export class Order extends Entity implements EntityKey {
   @column()
   @comment('ID')
   @key()
   @identity()
   id?: bigint;

   // ...

   @oneToMany(() => OrderDetail, p => p.order)
   details?: OrderDetail[];
 }

隐式声明导航属性

假设 实体OrderDetail并未声明多对一导航属性order,建模器会为其自动创建隐式多对一导航属性order

// 其它导入...
import { OrderDetail } from './order-detail'

/**
 * Order
 */
 @table()
 @context(() => DB)
 @comment('Order')
 export class Order extends Entity implements EntityKey {
   @column()
   @comment('ID')
   @key()
   @identity()
   id?: bigint;

   // ...

   @oneToMany(() => OrderDetail)
   details?: OrderDetail[];
 }

插入一对多关系数据

const order = Order.create({
    orderNo: '202101010001',
    // ...
    details: [
        {
            product: '铅笔',
            count: 1,
            price: new Decimal(0.56),
            // ...
        },
        {
            product: '文具盒',
            count: 1,
            price: new Decimal(10.65),
            // ...
        },
        {
            product: '笔记本',
            count: 1,
            price: new Decimal(3.5)
        }
    ]
});

await orderRepo.insert(order);

添加删除明细项

下列代码会删除OrderDetail中product为铅笔的记录,并且插入一条新的product为圆珠笔的记录。

const order = await orderRepo.get(1, { includes: { detail: true }});
order.details.splice(0, 1); // 删除第一个,即铅笔
order.details.push(OrderDetail.create({
    product: '圆珠笔',
    count: 1,
    price: new Decimal(1.2),
    // ...
}));

await orderRepo.save(order);
多对一关系

显式声明导航属性

假设实体Order已经定义一对多导航属性details,我们可以通过以下方式使其关联。

// ...
import { Order } from './order'

/**
 * OrderDetail
 */
 @table()
 @context(() => DB)
 @comment('OrderDetail')
 export class OrderDetail extends Entity implements EntityKey {
   @column()
   @comment('ID')
   @identity()
   @key()
   id?: bigint;

   // ...

   @comment('OrderId')
   @column()
   orderId?: bigint;

   @foreignKey('orderId') // 指定外键字段
   @manyToOne(() => Order, p => p.details)
   order?: Order | null; // 如果orderId字段是可空的,需要声明为可以为null,用于断开连接关系
 }

断开关联关系

下列代码可以断开关联关系

const orderDetail = await orderDetailRepo.get(1);
orderDetail.order = null;
await orderDetailRepo.save(orderDetail);

// => 将orderDetail.orderId 更新为 DBNULL

隐式声明导航及外键属性

假设实体类Order并未定义一对多属性,下列代码会自动为Order实体类创建隐式导航属性**orderDetail**,并为实体类Order自动创建隐式外键属性orderId

// ...
import { Order } from './order'

/**
 * OrderDetail
 */
 @table()
 @context(() => DB)
 @comment('OrderDetail')
 export class OrderDetail extends Entity implements EntityKey {
   @column()
   @comment('ID')
   @identity()
   @key()
   id?: bigint;

   // ...

   @comment('OrderId')
   @column()
   orderId?: bigint;

   @manyToOne(() => Order)
   order?: Order;
 }
多对多关系

在关系型数据库中,多对多关系需要依靠中间关系表完成,因此多对多关系亦需要一个中间关系实体类,该类可以由建模器自动创建,亦可以由用户显式创建。

显示声明导航属性及中间关系实体

职员: entities/employee.ts

// ...
import { Position } from './position'

@table()
@comment('Employee')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
  @column()
  @key()
  @comment('EmployeeID')
  @identity()
  id?: bigint;

  // ...

  @manyToMany(() => Position, p => p.employees) // 关联对向导航属性
  positions?: Position[];

  @oneToMany(() => EmployeePosition, p => p.employee)
  employeePositions?: EmployeePosition[];
}

职位: entities/position.ts

import { Employee } from './employee'

@table()
@comment('Position')
@context(() => DB)
export class Position extends Entity implements EntityKey {
  @column()
  @comment('PositionID')
  @identity()
  @key()
  id?: bigint;

  // ...

  @manyToMany(() => Employee, p => p.positions)   // 关联对向导航属性
  employees?: Employee[];

  @oneToMany(() => EmployeePosition, p => p.position)
  employeePositions?: EmployeePosition[];
}

中间关系实体: entities/employee-position.ts

// ...
import { Position } from './position'
import { Employee } from './employee'

@table()
@context(() => DB)
// @among(() => Position, () => Employee, 'position', 'employee')
@among<EmployeePosition, Position, Employee>(() => Position, () => Employee, p => p.position, p => p.employee)
export class EmployeePosition extends Entity implements EntityKey {
  @column()
  @comment('ID')
  @key()
  @identity()
  id?: bigint;

  @comment('PositionID')
  @column()
  positionId!: bigint;

  @foreignKey('positionId')
  @manyToOne(() => Position, p => p.employeePositions) // 定义导航属性,关联至一侧表
  position?: Position;

  @column()
  @comment('EmployeeID')
  employeeId!: bigint;

  // 定义导航属性,关联至外键表
  @foreignKey('employeeId')
  @manyToOne(() => Employee, p => p.employeePositions) // 定义导航属性,关联至另一侧表
  employee?: Employee;
}

隐式声明导航属性及中间关系实体

在下列范例中,建模器会隐式创建下列内容:

  • 为实体类Position创建多对多关系导航属性employees
  • 为实体类Position创建一对多关系导航属性employeePositions,关联至隐式实体类EmployeePosition
  • 为实体类Employee创建一对多关系导航属性employeePositions,关联至隐式实体类EmployeePosition
  • 自动创建一个名为EmployeePosition的隐式中间实体类,实体名称由两个实体名称相连接,先后根据实体名称首字母顺序而定,如果已经存在同名实体,则会直接使用该实体作为中间关系实体。同时该中间关系实体类结构与上一例子中的entities/employee-position.ts一致,具体会有以下属性:
    • 主键,根据全局主键配置自动创建。
    • 一个名为position的多对一导航属性,关联至实体Position
    • 一个名为positionId的外键属性,指向Position.id
    • 一个名为employee的多对一导航属性,关联致实体Employee
    • 一个名为employeeId的外键属性,指向Employee.id

职员: entities/employee.ts

// ...
import { Position } from './position'

@table()
@comment('Employee')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
  @column()
  @key()
  @comment('EmployeeID')
  @identity()
  id?: bigint;

  // ...

  @manyToMany(() => Position) // 关联对向导航属性
  positions?: Position[];
}

职位: entities/position.ts

import { Employee } from './employee'

@table()
@comment('Position')
@context(() => DB)
export class Position extends Entity implements EntityKey {
  @column()
  @comment('PositionID')
  @identity()
  @key()
  id?: bigint;

  // ...
}

插入多对多关系数据

const employee = Employee.create({
    name: '张三',
    // ...
    positions: [{
        name: '销售部经理(兼)',
        // ...
    }, {
        name: '副总经理',
        // ...
    }]
})

await employeeRepo.save(employee);

以上代码会先后插入下列数据:

  • Employee,张三,假设id为1
  • Position表,销售经理(兼)【假设id为1】,以及 副总经理【假设id为2】
  • EmployeePosition表,[{ employeeId: 1, positionId: 1 }, { employeeId: 1, positionId: 2 }]

删除关联关系

const employee = employeeRepo.get(1);
employee.positions = []; // 也可以 = null
await employeeRepo.save(employee);

上述代码将删除 EmployeePosition表中所有employeeId为1的记录。

创建上下文类文件

上下文DB类,db.ts文件

import { DbContext, Repository, repository } from 'lubejs';
import { User } from './entities/user'
import { Employee } from './entities/employee'

export class DB extends DbContext {
  	@repository(() => User) // 声明一个仓库属性,可以直接从该属性获取仓库,并且该属性为延迟创建,只有在访问该属性时才会被创建。
  	user: Repository<User>;

    @repository(() => Employee)
    employee: Repository<Employee>;
}

操作实体数据

在仓库对象,与可查询对象中已经详细介绍如何操作数据,在此不在赘述。

完整范例

  • 装饰器声明: ORM

  • 配置代码声明: ORM

使用Repository

数据迁移

要使用数据迁移功能,需要用到cli工具,cli工具依然包括在lubejs包中,当我们安装好lubejs后,我们会得到一个lube的命名,我们可以使用lube命名来进行数据迁移操作。

创建配置文件

在使用数据迁移之前,我们要先创建lubejs的配置文件.lubejs.ts或者.lubejs.js,否则无法运行迁移工具,创建方法请参考配置文件

创建迁移文件

lube migrate add [name]

此命令会将实体结构生成成到 ./migrates/<yyyyMMddHHmmss>_Init.ts的文件中,同时还会生成一个后缀名为.snapshot.ts的同名文件。在下次执行该命令时,会将最新的实体结构与该快照进行对比,q砶只针对差异生成迁移文件。

示例:

# 创建一个名为 Init 的迁移文件。
lube migrate add Init

如果这时没有更改过实体类等结构,再次运行

lube migrate add AddOrderModule

这时会得到一个空的./migrates/<yyyyMMddHHmmss>_AddOrderModule.ts文件。

手动编写迁移文件

在生成迁移文件后,您可以在上面编写您的数据库迁移代码,不用但以快照与实际结构会有所差异,当下次运行该命令时,如果监测到文件变化,lubejs会重新生成快照文件以进行对比。

import { Migrate, SQL, DbType, MigrateBuilder } from 'lubejs';

export class Init implements Migrate {
  async up(
    builder: MigrateBuilder, // 迁移代码构建器
    dialect: string // 执行时所使用的数据库方言
  ): Promise<void> {
   	// 在此编写您的部署代码
  }

  async down(
    builder: MigrateBuilder,
    dialect: string
  ): Promise<void> {
   // 在此编写您的回滚代码

}

export default Init;

所有的迁移代码,必须使用MigrateBuilder类创建,也就是上述示例中的builder对象,该对象与SQL构造器有着相似的使用方式,但在MigrateBuilder类中重点提供了数据迁移方面的功能,而减少了其它数据操作功能。

在这里需要提到的是,我们应该尽可能的不要使用builder.sql(...)方法来创建迁移代码,因为该方式所产生的结构变更,lubejs无法追踪源SQL字符串以便生成快照,因此可能在下次生成迁移文件时可能导致数据结构的不正确。

更新到数据库

lube migrate update [name]

此命令会将数据库更新到名称为[name]的迁移版本,如果数据库版本新于该版本,则会降级至该版本。

当不指定[name] 参数时,表示使用最新迁移版本。

同步数据库

lube migrate sync

此命令与update不同,该命令仅会分析当前实体的数据结构,并将对应数据库的结构更新到与实体一致,并不会执行迁移文件中的代码,通常我们使用此命令来快速创建测试环境数据库,不建议在生产环境中使用该命令。

导出升级/降级脚本

lube migrate script --source <source_name> --target <target_name> --output <output_file>

该命令可以为迁移文件生成SQL代码,其中<source_name>为源版本迁移文件名称,<target_name>为目标版本迁移文件名称,并将命令导出到<output_file>文件中。

更为详细的操作,请使用lube --help查看。

其它问题

JSON序列化问题

通常我们在使用JS的时候都是用JSON对象来进行序列化的,但是由于部分类型BigInt并未实现序列化功能,会在序列化时遇到错误Lubejs中的特殊标量类型Scalar的序列化情况如下:

| 类型 | 说明 | 具体情况 | | ------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | Binary | typescript类型别名,实际为BufferArrayBuffer或者TypedArrayBuffer等 | 为了不污染原生对象原则,lubejs未对其序列化行为作出改变,具体序列化结果根据实际值类型而定:- 如值为Buffer类型时,会调用Buffer.prototype.toString()再序列化(通常会乱码)。- 如值为ArrayBuffer类型时,会返回{} | | Uuid | Uuid类 | 已实现.toJSON,会返回"00000000-0000-0000-0000-000000000000"格式的字符串 | | BigInt | v8引原生类型 | 序列化时将遇到错误。 | | Decimal | 来原于decimal.js-light | 已实现.toJSON,会序列成字符串,例:"100" |

建议的解决方案如下:

  1. 自定义序列化

    JSON.stringify({ bigint: 1n }, (key, value) => {
        if (typeof value === 'bigint') {
            return  value.toString();
        } else {
            return value;
        }
    });
  2. 为添加.toJSON方法实现

    BigInt.prototype.toJSON = function() { return this.toString() }
       
    JSON.stringify(1n); // => '"1"'

另外反序列化时,也需要注意类型。

各数据库间差异

| 功能 | mssql | mysql | postgres | | --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | 函数功能 | 支持 | 支持 | 支持 | | 存储过程功能 | 支持 | 支持 | 不支持声明,仅可使用函数代替 | | 调用存储过程 | execute <sp_name> | call <sp_name> | select <fn_name>(...) | | 函数多行集返回 | 不支持,仅单行集返回 | 不支持,仅单行集返回