Update README.md
relaen是 noomi 团队打造的一套node环境下基于typescript的 ORM 框架。支持 Active Record 和 Data Mapper模式,支持实体类操作、创建查询构造器、原生sql、一级缓存和日志文件等。relaen 参考了很多其他优秀的ORM实现,实现更加清晰便捷的开发方式。并且提供实体自动生成工具 relaen-cli,将数据库表快速自动生成对应开发实体模型。
目前支持 MySQL、Oracle Database 12c +、Microsoft SQL Server 2012 +、PostgreSQL 10+、MariaDB、Sqlite 数据库。
为了方便开发者能快速了解并且使用 Relean,本章节将提供一个简单完整的快速入手例子。
本例子 IDE 使用 vscode 工具,数据库采用 MySQL,运行环境为 node.js。具体步骤如下:
新建数据库
首先新建名为 relaen 数据库,再新建一个 t_user 的测试表,建表语句如下:
CREATE TABLE `t_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(32) DEFAULT NULL, `user_pwd` char(32) DEFAULT NULL, PRIMARY KEY (`user_id`) )
relaen-cli 生成实体
由于 Relean 的工作是以 entity 为基础,所以完成数据库建立以后,需要生成数据库中表对应 entity 实体。Relaen 为开发者提供 relaen-cli 工具快速构建数据库中映射的 entity 实体。工具使用步骤如下:
在指定工程文件夹中,全局下载安装 relaen-cli 工具包;
# 下载安装relaen-cli工具 npm install -g relaen-cli # 下载安装数据库mysql依赖 npm install -g mysql
执行指令生成 config.json 文件,用于配置数据库连接参数;
# 构建relaen-cli项目文件 relaen-cli -i
配置数据连接 config.json 文件
{ "dialect":"mysql", // 数据库类型 "options":{ "host":"localhost", // 数据库地址 "port":3306, // 端口 "user":"root", // 数据库用户名 "password":"root", // 数据库密码 "database":"relaen" // 连接数据库名 }, "output":"out", // 生成实体文件夹 "tableSplit":"_", // 数据库表生成实体分界线 "tableStart":1, // 数据库表生成实体名起始下标,例如数据库名 t_user 生成 User 实体 "columnSplit":"_", // 字段分界线 "columnStart":0 // 字段名起始下标,例如 user_id 生成 userId 字段 }
执行生成实体指令,在当前工程文件目录生成相应实体类。
# 配置好数据库连接文件,运行生成实体 relaen-cli -g
Relaen 项目构建
将数据库映射实体生成完毕,就可以构建 Relaen 工程项目,下载 relaen 依赖和相关数据库 mysql 依赖,配置相关数据库连接参数,引入生成好的实体。
新建 Relean 工程文件,下载 relaen 依赖和 mysql 依赖
# 导入实体文件,下载relaen依赖 npm install relaen # 下载数据库连接所需依赖,如mysql npm install mysql
创建实体文件夹entity,数据库连接配置文件relaen.json,测试文件test.ts和tsconfig.json
TODO tsconfig.json 需要target 2015,打开experimentalDecorators,emitDecoratorMetadata
// 文件目录 relaen |—— entity |—— node_modules |—— package-lock.json |—— relaen.json |—— test.ts |—— tsconfig.json
将上一步 relaen-cli 工具生成的实体文件拷贝至 relaen 工程 entity 文件夹中,引入实体
配置数据库连接 relaen.json 配置文件,简单配置如下:
{ "dialect": "mysql", //数据库产品名 "host": "localhost", //数据库服务器地址 "port": 3306, //端口 "username": "root", //用户名 "password": "root", //密码 "database": "relaen", //数据库名 "entities": ["entity/*.ts"], //实体文件配置,为编译后js地址 "cache": true, //开启以及缓存 "debug": true //是否调试模式 }
简单案例
import { RelaenManager } from "relaen"; import { Shop } from "./entity/shop"; // 启动Relaen RelaenManager.init("relaen.json"); // 新增店铺方法 async function addShop() { let shop: Shop = new Shop(); shop.shopName = 'yang的店铺'; shop.address = '四川绵阳'; await shop.save(); } // 运行 addShop();
any
string
boolean
number
15000
sqlite 的可选配置为 Relaen 对数据库并发优化配置
实体是一个映射到数据库表的类,是对象和关系数据库之间的一种映射关系。
Relaen 中可以通过定义一个类来创建一个实体,并用 @Entity 注解标记来声明实体映射的数据库表信息。实体继承 Relaen 提供 BaseEntity 类,使得实体可以直接使用,实现 Active Record 模式。
@Entity
// 声明 Shop 实体,对应数据库中 t_shop 表 @Entity("t_shop") export class Shop extends BaseEntity{ }
@Entity 注解参数如下:
数据库表由字段组成,Relaen 中映射的实体类也由字段映射的属性组成。通过@Column 注解将表字段映射到实体属性中,通过列注解参数标识,将表字段的具体类型等信息映射到实体属性类型中。
@Column
@Entity("t_shop") export class Shop extends BaseEntity{ @Column({ name:'shop_name', // 字段名 type:'string', // 类型为string nullable:true, // 允许为空 length:64 // 长度为64位 }) public shopName:string; }
@Column 注解参数如下:
每一个实体必须要有一个主键。在relaen中,可以通过@Id 注解来表示该属性对应实体的主键,relaen目前只支持单主键。
@Id
@Entity("t_shop") export class Shop extends BaseEntity{ // 主键注解 @Id() @Column({ name:'shop_id', type:'number', nullable:false, identity:true }) public shopId:number; constructor(idValue?:number){ super(); this.shopId = idValue; } }
@Id 注解参数如下:
实体列中一种特殊的属性,配合关系注解使用,标识实体关系中对应的具体列属性,即数据库中外键关系的具体表字段名和属性。使用 @JoinColumn 注解标识实体中的关系列,具体参数如下,案例在实体关系中展示:
@JoinColumn 注解参数
Relaen 中实体之间的关系采用常见关系型数据库的规范,主要分为以下类型:
一对一关系:@OneToOne
一对多关系:@OneToMany
多对一关系:@ManyToOne
备注:常见多对多关系在 Relean 中使用一对多和多对一进行替代,这样使得关系实体可以更加灵活,方便添加其他额外字段属性。
在 Relaen 中配合上述关系注解,还使用到 @JoinColumn 关系列注解,来详细描述关系中实体对应的列信息。
Relaen 在实体关系中还涉及到关系实体的加载,常见 ORM 使用懒加载属性配置,我们使用实体代理获取关系实体来替代配置懒加载属性,简化配置。
一对一关系是指实体中实例一一对应,例:一个实例 A 只包含另一个实例 B,同理实例 B 也只包含实例 A。
在 Relean 中使用 @OneToOne 注解来标识实体一对一关系,当前实体类 @Entity 注解表示关系依赖对象,@OneToOne 注解中实体表示被依赖对象。
其中配合使用 @JoinColumn 注解表示详细依赖对象和被依赖对象的具体列名,此注解在依赖方对象存在(即数据库表外键引入表)。
@Entity("t_user_info") export class UserInfo extends BaseEntity{ // 一对一关系注解,User 被依赖实体 @OneToOne({entity:'User'}) // 关系列,name 当前表数据库字段,refName 引用表数据库字段 @JoinColumn({ name:'USER_ID', refName:'USER_ID', nullable:true }) public user:User; // 代理获取关系对象 public async getUser():Promise<User>{ return this['user']?this['user']:await EntityProxy.get(this,'user'); } }
@OneToOne 注解参数
一对多/多对一是指一个实例包含多个其他实例的关系,例:一个实例 A 包含多个实例 B,相对实例 A 即为一对多,相反实例 B 即为多对一。
在 Relaen 中使用 @OneToMany、@ManyToOne 注解来标识实体一对多、多对一关系。其中 @OneToMany 用于一方,没有 @JoinColumn 引入列;@ManyToOne 用于多方,有 @JoinColumn 引入列。
// 一对多 @Entity("t_user_info") export class UserInfo extends BaseEntity{ // 一对多关系注解 @OneToMany({ entity:'Shop', mappedBy:'userInfoForOwnerId' }) public shopForOwnerIds:Array<Shop>; public async getShopForOwnerIds():Promise<Array<Shop>>{ return this['shopForOwnerIds']?this['shopForOwnerIds']:await EntityProxy.get(this,'shopForOwnerIds'); } }
@OneToMany 注解参数:
// 多对一 @Entity("t_shop") export class Shop extends BaseEntity{ // 多对一关系注解 @ManyToOne({entity:'UserInfo'}) @JoinColumn({ name:'owner_id', refName:'user_id', nullable:true }) public userInfoForOwnerId:UserInfo; public async getUserInfoForOwnerId(): Promise<UserInfo> { return this['userInfoForOwnerId'] ? this['userInfoForOwnerId'] : await EntityProxy.get(this, 'userInfoForOwnerId'); } }
@ManyToOne 注解参数:
多对多关系是指一类实例包含多个另一类实例,并且也被另一类实例多个包含。例:A 类多个实例包含 B 类多个实例,反之 B 类多个实例也包含多个 A 类实例。
常见其它 ORM 会使用 ManyToMany 来标识多对多关系,但在 Relaen 中采用一对多和多对一来代替多对多关系,并将多对多关系的数据库表映射为实体类,方便开发者更加灵活的创建和设置多对多关系数据库表字段。本章节将具体举例说明,如下:
以student和teacher实体为例。student可以选择多个teacher,teacher可以有多个student。
student
teacher
一般情况下我们创建一个student_teacher表,并使用两者的主键作为联合主键,来记录两个实体的多对多关系。
student_teacher
Relaen 中只允许唯一主键,所以不能使用联合主键,需要在student_teacher表中单独创建一个主键,然后将student和teacher中主键作为该表的外键。这样我们可以在该表中对上述字段使用@OneToMany、@ManyToOne来代替@ManyToMany注解。
@OneToMany
@ManyToOne
@ManyToMany
数据库中的表如下:
# student +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | | address | varchar(255) | YES | | NULL | | +---------+--------------+------+-----+---------+----------------+
# teacher +-------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | | course_name | varchar(255) | YES | | NULL | | +-------------+--------------+------+-----+---------+----------------+
# student_teacher +------------+---------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+---------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | student_id | int(11) | YES | MUL | NULL | | | teacher_id | int(11) | YES | MUL | NULL | | +------------+---------+------+-----+---------+----------------+
去掉一些必要的形式代码,三个实体的关键代码如下
@Entity("student") export class Student extends BaseEntity{ // student 中使用 OneToMany 映射 @OneToMany({entity:'StudentTeacher',mappedBy:'student'}) public studentTeachers:Array<StudentTeacher>; public async getStudentTeachers():Promise<Array<StudentTeacher>>{ return this['studentTeachers']?this['studentTeachers']:await EntityProxy.get(this,'studentTeachers'); } }
@Entity("teacher") export class Teacher extends BaseEntity{ // student 中使用 OneToMany 映射 @OneToMany({entity:'StudentTeacher',mappedBy:'teacher'}) public studentTeachers:Array<StudentTeacher>; public async getStudentTeachers():Promise<Array<StudentTeacher>>{ return this['studentTeachers']?this['studentTeachers']:await EntityProxy.get(this,'studentTeachers'); } } S
@Entity("student_teacher") export class StudentTeacher extends BaseEntity{ @Id() @Column({ name:'id', type:'int', nullable:false}) public id:number; // 对 student 使用 ManyToOne @ManyToOne({entity:'Student'}) @JoinColumn({name:'student_id',refName:'id',nullable:true}) public student:Student; // 对 teacher 使用 ManyToOne @ManyToOne({entity:'Teacher'}) @JoinColumn({name:'teacher_id',refName:'id',nullable:true}) public teacher:Teacher; public async getStudent():Promise<Student>{ return this['student']?this['student']:await EntityProxy.get(this,'student'); } public async getTeacher():Promise<Teacher>{ return this['teacher']?this['teacher']:await EntityProxy.get(this,'teacher'); } }
至此,我们完成了@OneToMany、@ManyToOne代替@ManyToMany。
Relaen 中的实体继承了 BaseEntity 类,使得开发者可以直接对实体使用 save、find、delete 等内置方法简单进行数据库的 CRUD 操作。在 Relaen 中所用实体方法会隐式调用了 EntityManager 中的方法,来创建关闭连接、CRUD操作,也可以显示的创建EntityManager。
对于本文,假定以下实体设置
@Entity("t_shop") export class Shop extends BaseEntity{ @Id() @Column({ name:'shop_id', type:'int', nullable:false }) public shopId:number; @ManyToOne({entity:'UserInfo'}) @JoinColumn({ name:'owner_id', refName:'user_id', nullable:true }) public userInfoForOwnerId:UserInfo; @ManyToOne({entity:'UserInfo'}) @JoinColumn({ name:'manager_id', refName:'user_id', nullable:true }) public userInfoForManagerId:UserInfo; @Column({ name:'shop_name', type:'string', nullable:true, length:32 }) public shopName:string; @Column({ name:'address', type:'string', nullable:true, length:128 }) public address:string; constructor(idValue?:number){ super(); this.shopId = idValue; } public async getUserInfoForOwnerId():Promise<UserInfo>{ return this['userInfoForOwnerId']?this['userInfoForOwnerId']:await EntityProxy.get(this,'userInfoForOwnerId'); } public async getUserInfoForManagerId():Promise<UserInfo>{ return this['userInfoForManagerId']?this['userInfoForManagerId']:await EntityProxy.get(this,'userInfoForManagerId'); } }
实体是一个类,你可以使用new来创建一个实例。
由于实体 Shop 继承了 BaseEntity 类,你可以使用 save 方法将其保存到数据库中(即持久保存)。
let shop:Shop = new Shop(); shop.shopName = 'yang的店铺'; shop.address = '四川绵阳'; // 设置外键 shop.userInfoForOwnerId = new UserInfo(1); await shop.save();
显示创建 EntityManager 并使用它的 save 方法。显示创建的好处在于获取一次数据库连接,多次执行,减少连接请求次数。
let shop:Shop = new Shop(); shop.shopName = 'field的店铺'; shop.address = '四川绵阳'; shop.userInfoForOwnerId = new UserInfo(1); // 显式创建 entity manager,并调用 save 方法 let em:EntityManager = await getEntityManager(); await em.save(shop); // 显式创建的 entitymanager,必须手动关闭 await em.close();
使用 find 方法,通过主键条件进行查询。
// 查询 id 为1的实例 let shop:Shop = <Shop> await Shop.find(1);
显示创建 EntityManager 方式。
// 查询 id 为1的实例 let em:EntityManager = await getEntityManager(); let shop:Shop = <Shop> await em.find(Shop.name, 1); await em.close();
使用对象关系构建查询条件参数,进行数据过滤查询。params 条件对象中的关键字如下:
// 构造以下参数,查询条件为" (owner.realName like '%relaen%' OR owner.age >= 20) AND address='四川绵阳' " let params = { "userInfoForOwnerId.realName":{ value:'relaen', rel:'like', before:'(' }, "userInfoForOwnerId.age":{ value:20, rel:'>=', logic:'OR', after:')' }, "address":'四川绵阳' }
使用上述条件参数查询适用于findOne、findMany方法获取条件查询结果。
findOne 方法:获取单个条件查询实体
let shop:Shop = <Shop>await Shop.findOne(params); // 传入查询参数
let em:EntityManager = await getEntityManager(); let shop:Shop = <Shop> await em.findOne(Shop.name, params); await em.close();
findMany 方法:获取多个条件查询结果集实体
let shops:Array<Shop> = <Array<Shop>> await Shop.findMany(params);
let em:EntityManager = await getEntityManager(); let shop:Shop = <Shop> await em.findMany(Shop.name, params); await em.close();
findMany 方法的详细参数如下
Relaen 新增和更新实例都采用 save 方法,由框架执行时自动判断当前是新增还是更新操作,使得开发者只需关注实例本身。
// 使用 find 方法查询到数据库中id为1的实例 let shop:Shop = <Shop> await Shop.find(1); // 更新其 address 属性 shop.address = '四川 成都'; // 参数为 true,表示仅对 address 进行修改 await shop.save(true);
显示创建 EntityManager :
let em:EntityManager = await getEntityManager(); let shop:Shop = <Shop> await em.find(Shop.name, 1); shop.address = '四川 成都'; // 使用 save 方法 await em.save(shop, true); await em.close();
save 方法:保存实例数据
在更新情况 save 方法策略
可使用实体类或实例对象进行删除操作
// 方法一:通过实例对象进行删除 let shop:Shop = <Shop> await Shop.find(1); await shop.delete(); // 方法二:通过实体类进行删除,传入参数主键id await Shop.delete(1);
显示创建 EntityManager :
let em:EntityManager = await getEntityManager(); // 方法一:传入实例对象删除 let shop:Shop = <Shop> await em.find(Shop.name, 1); await em.delete(shop); // 方法二:传入主键id、实体名 await em.delete(1, Shop.name); await em.close();
也可以使用 deleteMany 方法进行条件删除相关实体数据,使用条件参数 params 格式如上述条件参数查询详情
// 删除以下条件的数据 let params = { shopName: { value: 'yang', rel: 'like' } } // 通过实体类执行条件删除 await Shop.deleteMany(params);
let em:EntityManager = await getEntityManager(); // 传入参数:实体类名,条件参数 await em.deleteMany(Shop.name,params); await em.close();
在实际开发中涉及到较多的SQL查询,为了开发者较为灵活方便的查询需求,Relaen 提供 Query 和 NativeQuery 两种查询构造器方便开发者查询使用。其中 Query 为 Relaen 内部封装提供的查询构造方法,即SQL构造器。NativeQuery 为 Relaen 提供开发者执行原生SQL,以便开发者自己需求。本章将对 Relane 中使用 Query 的复杂查询以及 NativeQuery 简单使用进行说明。
Query 提供了多种方法来协助查询数据中的数据,并采用链式方法进行操作。
在Relaen中,通过 EntityManager 类创建 Query 对象,并使用其中提供的一些方法,你可以通过组合他们来进行便捷的 SQL 查询(链式查询)。
EntityManager
Query 提供两种运行查询结果方法:
let em:EntityManager = await getEntityManager(); let query:Query = em.createQuery(Shop.name); let r = await query .select("*") .getResult(); // 查询结果集(分页) //.getResultList(1,10) await em.close();
getResult 方法:获取单一查询结果
getResultList 方法:获取多查询结果集(分页)
select 限定查询结果集中查询字段,可查询本对象和关联对象的属性(本对象为 createQuery 中传入对象 )。
select
let em:EntityManager = await getEntityManager(); let query:Query = em.createQuery(Shop.name); let r = await query // * 查询shop全部属性,userInforForManagerId.realName 查询关联对象的属性,userInfoForManagerId.user.username 关联对象可嵌套 .select(["*","userInfoForManagerId.realName","userInfoForManagerId.user.userName"]) .getResultList(); await em.close(); // 查询结果如下,结果为Shop对象数组 [ { shopId:1, address:'四川 成都', shopName:'Relaen的铺子', userInfoForManagerId:{ relaName:'NOOMI', user:{ userName:'noomi' } } } //...... ]
where 方法用于过滤条件。你可以在 where 方法中传入参数对象 params。具体的参数对象构造请参考上文查询实例中使用参数对象。
where
let em:EntityManager = await getEntityManager(); let query:Query = em.createQuery(Shop.name); // 条件最后结果为 shopName like '%filed%' and address = '四川 成都' let params = { "shopName":{ value:'field', rel:'like' }, "address":'四川 成都' }; let r = await query .select("*") .where(params) .getResultList(); await em.close();
orederBy 方法对获取数据进行排序,接受一个形如 { paramName1: ‘desc’, paramName2: ‘asc’,…} 的排序对象。其中 desc 为降序,asc 为升序。
orederBy
desc
asc
let em:EntityManager = await getEntityManager(); let query:Query = em.createQuery(Shop.name); let r = await query .select("*") .orderBy({'shopName':'asc'}) // 对 shopName 进行升序排序 .getResultList(); await em.close();
distinct 方法为查询添加 distinct 关键词,用于返回唯一不同的值。
distinct
// 查询所有店铺的位置(不重复) let em:EntityManager = await getEntityManager(); let query:Query = em.createQuery(Shop.name); let r = await query .select("address") .distinct() // 添加distinct 关键字 .getResultList(); await em.close();
Relaen 提供执行原生SQL操作,以满足开发者执行自定义SQL语句或当前数据库的某些特性语句。
NativeQuery 由 EntityManager 中的 createNativeQuery 方法创建原生SQL操作对象。该方法接受两个参数,第一个参数为 SQL 语句;第二个参数为实体类名(可选),传入该参数时,会将查询结果转换为实体对象。
createNativeQuery
let em:EntityManager = await getEntityManager(); // sql语句占位可通用 ? 替代,relaen会转换为对应数据库占位符格式;或直接使用当前数据库占位符格式 let sql = "select * from t_shop where shop_id = ?"; // 创建原生查询对象,第二个参数,会将查询结果转实体Shop,如果为空,则不转换 let query:NativeQuery = em.createNativeQuery(sql, Shop.name); // 设置占位符参数值 query.setParameter(0, 1); // 前五条数据,因为传入了实体名,将会转换成实体对象 let r = await query.getResultList(0,5); await em.close();
createNativeQuery 方法:创建原生查询
setParameter 方法:按照占位符下标设置参数
setParameters 方法:设置多个参数值,默认下标0开始
事务在保证一组 SQL 语句执行的一致性,要么全部成功,要么全部失败。
在 Relaen 中事务由 Connection 中 createTransaction 方法进行显示创建,并通过 Transaction 类中的 begin 、 commit 、 rollback 方法来完成事务的开始、提交、回滚 等操作。
Transaction
begin
commit
rollback
开始
提交
回滚
// 显式获取connection let conn:Connection = await getConnection(); // 创建事务 let tx:Transaction = conn.createTransaction(); // 开始事务 await tx.begin(); // 调用删除 shop 方法 await delShops(); // 其它SQL操作...... // 成功则提交事务 await tx.commit(); // 出错则回滚事务,不会删除数据 // await tx.rollback(); // 显式创建的connection,一定要显示关闭 await conn.close();
begin 方法参数
Mysql, Postgres, Mssql 支持标准隔离级别(READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE)
Oracle 仅支持 READ COMMITTED 和 SERIALIZABLE 隔离级别
Sqlite 数据库默认 SERIALIZABLE,但可使用 immediate 和 exclusive 进行加锁
为了保证数据访问的排他性,需要通过一些机制来保证数据在某一访问操作时不会被外界其它操作修改。
Relaen 提供乐观锁(Optimistic)和悲观锁(Pessimistic)两种锁机制来保证数据访问时排他性。乐观锁是使用更新语句进行逻辑判断保证数据的排他性,悲观锁是基于数据库的锁机制对访问数据进行加锁保证数据的排他性。
乐观锁相对悲观锁采用更加宽松的加锁机制,基于数据版本Version 记录机制实现。即表增加一个数据版本标识字段,每次在更新数据时,对应的数据版本Version 也加1处理,在提交时比对数据版本Version 值是否改变。如Version 值符合,正常更新数据,否则更新失败。
实体配置,新增version:true配置,标识该字段为数据版本号
@Entity('t_shop') export class Shop extends BaseEntity { @Id() @Column({ name: 'shop_id', type: 'int', nullable: false, identity: true }) public shopId: number; @Column({ name: 'shop_name', type: 'string', nullable: true, length: 32 }) public shopName: string; @Column({ name: 'version', type: 'int', nullable: true, //标识当前属性为数据版本号 version: true }) public version: number; constructor(idValue?: number) { super(); this.shopId = idValue; } public async getUserInfoForOwnerId(): Promise<UserInfo> { return this['userInfoForOwnerId'] ? this['userInfoForOwnerId'] : await EntityProxy.get(this, 'userInfoForOwnerId'); } public async getUserInfoForManagerId(): Promise<UserInfo> { return this['userInfoForManagerId'] ? this['userInfoForManagerId'] : await EntityProxy.get(this, 'userInfoForManagerId'); } }
在实体 save 方法,第二参数传入 optimistic 参数开启乐观锁
let shop: Shop = <Shop>await Shop.find(1); shop.shopName = 'frank的店铺修改'; //开启乐观锁 return await shop.save(true, 'optimistic');
悲观锁是基于数据库本身的加锁机制实现,真正意义上的加锁,以保证访问时的数据排他性,但会带来性能的开销。Relaen 默认采用行加写锁实现悲观锁,sqlite不支持。
let conn: Connection = await getConnection(); let tx: Transaction = conn.createTransaction(); //开启事务 await tx.begin(); let em: EntityManager = await getEntityManager(); let query: Query = em.createQuery(Shop.name); let shop = await query.select('*') .where({ shopId: 1 }) //设置悲观锁:pessimistic .setLock('pessimistic') //执行查询加锁 .getResult(); shop.shopName = '悲观锁测试'; await shop.save(true); //提交事务,释放锁 await tx.commit(); await em.close();
Relean 中的日志使用通用的 log4js 日志工具,为了方便初学者使用,Relean 内置两种日志模式:
目的:调试模式是为开发调试程序时使用,通过控制台打印 Relaen 执行的 SQL 语句和执行过程帮助检测和定位开发中的问题。
使用:在连接配置参数中 debug 设置为 true,即可开启调试模式(默认为:false)。
目的:文件模式是将框架运行的日志写入指定文件中,便于日常程序运行核对信息。
使用:在连接配置参数中 fileLog 设置 true,即可开启文件日志模式(默认为:false)。默认日志文件位置在工程根目录,文件名为 relaen.log。为了提供开发者自定义日志文件相关,fileLog 可传入 log4js 相关的文件 Appenders,例如:file 和 datefile。
node环境下基于typescript的ORM框架
©Copyright 2023 CCF 开源发展委员会 Powered by Trustie& IntelliDE 京ICP备13000930号
简介
relaen是 noomi 团队打造的一套node环境下基于typescript的 ORM 框架。支持 Active Record 和 Data Mapper模式,支持实体类操作、创建查询构造器、原生sql、一级缓存和日志文件等。relaen 参考了很多其他优秀的ORM实现,实现更加清晰便捷的开发方式。并且提供实体自动生成工具 relaen-cli,将数据库表快速自动生成对应开发实体模型。
目前支持 MySQL、Oracle Database 12c +、Microsoft SQL Server 2012 +、PostgreSQL 10+、MariaDB、Sqlite 数据库。
快速入手
为了方便开发者能快速了解并且使用 Relean,本章节将提供一个简单完整的快速入手例子。
本例子 IDE 使用 vscode 工具,数据库采用 MySQL,运行环境为 node.js。具体步骤如下:
新建数据库
首先新建名为 relaen 数据库,再新建一个 t_user 的测试表,建表语句如下:
relaen-cli 生成实体
由于 Relean 的工作是以 entity 为基础,所以完成数据库建立以后,需要生成数据库中表对应 entity 实体。Relaen 为开发者提供 relaen-cli 工具快速构建数据库中映射的 entity 实体。工具使用步骤如下:
在指定工程文件夹中,全局下载安装 relaen-cli 工具包;
执行指令生成 config.json 文件,用于配置数据库连接参数;
配置数据连接 config.json 文件
执行生成实体指令,在当前工程文件目录生成相应实体类。
Relaen 项目构建
将数据库映射实体生成完毕,就可以构建 Relaen 工程项目,下载 relaen 依赖和相关数据库 mysql 依赖,配置相关数据库连接参数,引入生成好的实体。
新建 Relean 工程文件,下载 relaen 依赖和 mysql 依赖
创建实体文件夹entity,数据库连接配置文件relaen.json,测试文件test.ts和tsconfig.json
TODO tsconfig.json 需要target 2015,打开experimentalDecorators,emitDecoratorMetadata
将上一步 relaen-cli 工具生成的实体文件拷贝至 relaen 工程 entity 文件夹中,引入实体
配置数据库连接 relaen.json 配置文件,简单配置如下:
简单案例
数据库连接
Relean 通用配置
pool 连接池配置
mysql/mariadb 可选配置
any
:带有 ssl 参数的对象或包含 ssl 配置文件名称的字符串string
:连接的字符集(默认值:UTF8_GENERAL_CI)string
:服务器上配置的时区(默认:local)boolean
: 强制日期类型作为字符串返回boolean
:处理数据库中的大数字boolean
: 同时启用supportBigNumbers和bigNumberStrings会强制将大数字(BIGINT和DECIMAL)作为 JavaScript String 对象返回boolean
:允许每个查询多个mysql语句。要小心,这可能会增加SQL注入攻击的范围(默认:false)string
:使用非默认连接标志的连接标志oracle 可选配置
number
:在建立到数据库的连接时使用的特权number
:池中每个连接的语句缓存中要缓存的语句数string
:当从连接池获取连接时使用,指示从连接池返回的连接应该具有的标记number
:当连接请求超过当前打开的连接数时打开的连接数(默认值:1)mssql 可选配置
number
:请求超时时间,毫秒为单位 (默认:15000
)。注意: msnodesqlv8 驱动不支持 timeouts < 1 秒postgres 可选配置
any
:带有 ssl 参数的对象,详见 TLS/SSLsqlite 可选配置
sqlite 的可选配置为 Relaen 对数据库并发优化配置
number
:SQLITE_BUSY 重复执行时间number
:SQLITE_BUSY 重复执行超时时间实体
实体是一个映射到数据库表的类,是对象和关系数据库之间的一种映射关系。
实体类
Relaen 中可以通过定义一个类来创建一个实体,并用
@Entity
注解标记来声明实体映射的数据库表信息。实体继承 Relaen 提供 BaseEntity 类,使得实体可以直接使用,实现 Active Record 模式。@Entity 注解参数如下:
实体列
数据库表由字段组成,Relaen 中映射的实体类也由字段映射的属性组成。通过
@Column
注解将表字段映射到实体属性中,通过列注解参数标识,将表字段的具体类型等信息映射到实体属性类型中。@Column 注解参数如下:
主列
每一个实体必须要有一个主键。在relaen中,可以通过
@Id
注解来表示该属性对应实体的主键,relaen目前只支持单主键。@Id 注解参数如下:
关系列
实体列中一种特殊的属性,配合关系注解使用,标识实体关系中对应的具体列属性,即数据库中外键关系的具体表字段名和属性。使用 @JoinColumn 注解标识实体中的关系列,具体参数如下,案例在实体关系中展示:
@JoinColumn 注解参数
关系
Relaen 中实体之间的关系采用常见关系型数据库的规范,主要分为以下类型:
一对一关系:@OneToOne
一对多关系:@OneToMany
多对一关系:@ManyToOne
备注:常见多对多关系在 Relean 中使用一对多和多对一进行替代,这样使得关系实体可以更加灵活,方便添加其他额外字段属性。
在 Relaen 中配合上述关系注解,还使用到 @JoinColumn 关系列注解,来详细描述关系中实体对应的列信息。
Relaen 在实体关系中还涉及到关系实体的加载,常见 ORM 使用懒加载属性配置,我们使用实体代理获取关系实体来替代配置懒加载属性,简化配置。
一对一
一对一关系是指实体中实例一一对应,例:一个实例 A 只包含另一个实例 B,同理实例 B 也只包含实例 A。
在 Relean 中使用 @OneToOne 注解来标识实体一对一关系,当前实体类 @Entity 注解表示关系依赖对象,@OneToOne 注解中实体表示被依赖对象。
其中配合使用 @JoinColumn 注解表示详细依赖对象和被依赖对象的具体列名,此注解在依赖方对象存在(即数据库表外键引入表)。
@OneToOne 注解参数
一对多/多对一
一对多/多对一是指一个实例包含多个其他实例的关系,例:一个实例 A 包含多个实例 B,相对实例 A 即为一对多,相反实例 B 即为多对一。
在 Relaen 中使用 @OneToMany、@ManyToOne 注解来标识实体一对多、多对一关系。其中 @OneToMany 用于一方,没有 @JoinColumn 引入列;@ManyToOne 用于多方,有 @JoinColumn 引入列。
@OneToMany 注解参数:
@ManyToOne 注解参数:
多对多
多对多关系是指一类实例包含多个另一类实例,并且也被另一类实例多个包含。例:A 类多个实例包含 B 类多个实例,反之 B 类多个实例也包含多个 A 类实例。
常见其它 ORM 会使用 ManyToMany 来标识多对多关系,但在 Relaen 中采用一对多和多对一来代替多对多关系,并将多对多关系的数据库表映射为实体类,方便开发者更加灵活的创建和设置多对多关系数据库表字段。本章节将具体举例说明,如下:
以
student
和teacher
实体为例。student
可以选择多个teacher
,teacher
可以有多个student
。一般情况下我们创建一个
student_teacher
表,并使用两者的主键作为联合主键,来记录两个实体的多对多关系。Relaen 中只允许唯一主键,所以不能使用联合主键,需要在
student_teacher
表中单独创建一个主键,然后将student
和teacher
中主键作为该表的外键。这样我们可以在该表中对上述字段使用@OneToMany
、@ManyToOne
来代替@ManyToMany
注解。数据库中的表如下:
去掉一些必要的形式代码,三个实体的关键代码如下
至此,我们完成了
@OneToMany
、@ManyToOne
代替@ManyToMany
。使用
Relaen 中的实体继承了 BaseEntity 类,使得开发者可以直接对实体使用 save、find、delete 等内置方法简单进行数据库的 CRUD 操作。在 Relaen 中所用实体方法会隐式调用了 EntityManager 中的方法,来创建关闭连接、CRUD操作,也可以显示的创建EntityManager。
对于本文,假定以下实体设置
创建实例
实体是一个类,你可以使用new来创建一个实例。
由于实体 Shop 继承了 BaseEntity 类,你可以使用 save 方法将其保存到数据库中(即持久保存)。
显示创建 EntityManager 并使用它的 save 方法。显示创建的好处在于获取一次数据库连接,多次执行,减少连接请求次数。
查询实例
根据主键查询
使用 find 方法,通过主键条件进行查询。
显示创建 EntityManager 方式。
根据条件参数查询
使用对象关系构建查询条件参数,进行数据过滤查询。params 条件对象中的关键字如下:
使用上述条件参数查询适用于findOne、findMany方法获取条件查询结果。
findOne 方法:获取单个条件查询实体
显示创建 EntityManager 方式。
findMany 方法:获取多个条件查询结果集实体
显示创建 EntityManager 方式。
findMany 方法的详细参数如下
更新实例
Relaen 新增和更新实例都采用 save 方法,由框架执行时自动判断当前是新增还是更新操作,使得开发者只需关注实例本身。
显示创建 EntityManager :
save 方法:保存实例数据
在更新情况 save 方法策略
删除实例
根据主键删除
可使用实体类或实例对象进行删除操作
显示创建 EntityManager :
根据条件删除
也可以使用 deleteMany 方法进行条件删除相关实体数据,使用条件参数 params 格式如上述条件参数查询详情
显示创建 EntityManager :
查询
在实际开发中涉及到较多的SQL查询,为了开发者较为灵活方便的查询需求,Relaen 提供 Query 和 NativeQuery 两种查询构造器方便开发者查询使用。其中 Query 为 Relaen 内部封装提供的查询构造方法,即SQL构造器。NativeQuery 为 Relaen 提供开发者执行原生SQL,以便开发者自己需求。本章将对 Relane 中使用 Query 的复杂查询以及 NativeQuery 简单使用进行说明。
Query 链式查询
Query 提供了多种方法来协助查询数据中的数据,并采用链式方法进行操作。
在Relaen中,通过
EntityManager
类创建 Query 对象,并使用其中提供的一些方法,你可以通过组合他们来进行便捷的 SQL 查询(链式查询)。运行查询
Query 提供两种运行查询结果方法:
getResult 方法:获取单一查询结果
getResultList 方法:获取多查询结果集(分页)
查询字段
select
限定查询结果集中查询字段,可查询本对象和关联对象的属性(本对象为 createQuery 中传入对象 )。条件
where
方法用于过滤条件。你可以在 where 方法中传入参数对象 params。具体的参数对象构造请参考上文查询实例中使用参数对象。排序
orederBy
方法对获取数据进行排序,接受一个形如 { paramName1: ‘desc’, paramName2: ‘asc’,…} 的排序对象。其中desc
为降序,asc
为升序。去重
distinct
方法为查询添加 distinct 关键词,用于返回唯一不同的值。NativeQuery 原生查询
Relaen 提供执行原生SQL操作,以满足开发者执行自定义SQL语句或当前数据库的某些特性语句。
NativeQuery 由 EntityManager 中的
createNativeQuery
方法创建原生SQL操作对象。该方法接受两个参数,第一个参数为 SQL 语句;第二个参数为实体类名(可选),传入该参数时,会将查询结果转换为实体对象。createNativeQuery 方法:创建原生查询
setParameter 方法:按照占位符下标设置参数
setParameters 方法:设置多个参数值,默认下标0开始
事务
事务在保证一组 SQL 语句执行的一致性,要么全部成功,要么全部失败。
在 Relaen 中事务由 Connection 中 createTransaction 方法进行显示创建,并通过
Transaction
类中的begin
、commit
、rollback
方法来完成事务的开始
、提交
、回滚
等操作。begin 方法参数
Mysql, Postgres, Mssql 支持标准隔离级别(READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE)
Oracle 仅支持 READ COMMITTED 和 SERIALIZABLE 隔离级别
Sqlite 数据库默认 SERIALIZABLE,但可使用 immediate 和 exclusive 进行加锁
锁机制
为了保证数据访问的排他性,需要通过一些机制来保证数据在某一访问操作时不会被外界其它操作修改。
Relaen 提供乐观锁(Optimistic)和悲观锁(Pessimistic)两种锁机制来保证数据访问时排他性。乐观锁是使用更新语句进行逻辑判断保证数据的排他性,悲观锁是基于数据库的锁机制对访问数据进行加锁保证数据的排他性。
乐观锁
乐观锁相对悲观锁采用更加宽松的加锁机制,基于数据版本Version 记录机制实现。即表增加一个数据版本标识字段,每次在更新数据时,对应的数据版本Version 也加1处理,在提交时比对数据版本Version 值是否改变。如Version 值符合,正常更新数据,否则更新失败。
使用
实体配置,新增version:true配置,标识该字段为数据版本号
在实体 save 方法,第二参数传入 optimistic 参数开启乐观锁
悲观锁
悲观锁是基于数据库本身的加锁机制实现,真正意义上的加锁,以保证访问时的数据排他性,但会带来性能的开销。Relaen 默认采用行加写锁实现悲观锁,sqlite不支持。
使用
日志
Relean 中的日志使用通用的 log4js 日志工具,为了方便初学者使用,Relean 内置两种日志模式:
debug
目的:调试模式是为开发调试程序时使用,通过控制台打印 Relaen 执行的 SQL 语句和执行过程帮助检测和定位开发中的问题。
使用:在连接配置参数中 debug 设置为 true,即可开启调试模式(默认为:false)。
fileLog
目的:文件模式是将框架运行的日志写入指定文件中,便于日常程序运行核对信息。
使用:在连接配置参数中 fileLog 设置 true,即可开启文件日志模式(默认为:false)。默认日志文件位置在工程根目录,文件名为 relaen.log。为了提供开发者自定义日志文件相关,fileLog 可传入 log4js 相关的文件 Appenders,例如:file 和 datefile。