数据库
数据库使用postgresql,连接框架使用seqlize@7,相关配置在src/database.ts中,src/database.ts加载了env环境变量,如需配置数据库连接方式可直接更改.env文件的相关配置。注意:本项目自行封装使用的seqlize@7,未使用@midwayjs/sequelize@3组件。
相关使用方式如下:
模型定义
1、创建 Model(Entity)
我们通过模型和数据库关联,在应用中的模型就是数据库表,在 Sequelize 中,模型是和实体绑定的,每一个实体(Entity) 文件,即是 Model,也是实体(Entity)。 所有的实体存储在src/entities文件夹和数据库表想对应。 在示例中,需要一个实体,我们这里拿 ExampleBook 举例。在src/entities文件夹添加实体文件 exampleBook.entity.ts ,一个简单的实体如下。
// src/entities/exampleBook.entity.ts
import { ApiPropertyRule } from '@/decorators/index.js';
import { uuid } from '@/helper/snowflake.js';
import { RuleType } from '@/ruleType/index.js';
import { DataTypes } from '@sequelize/core';
import { Attribute, Default, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
import { AdminBaseModel } from './abstract/adminBase.entity.js';
@Table({ tableName: 'example_book', comment: '示例_书籍' })
export class ExampleBook extends AdminBaseModel<ExampleBook> {
//自动生成的主键
@Attribute({ type: DataTypes.STRING(20), allowNull: false })
@PrimaryKey
@Default(uuid)
id: string;
@Attribute({ type: DataTypes.STRING(20), comment: '名称', allowNull: false, defaultValue: '' })
name: string;
}- 管理后台的表需继承AdminBaseModel,会自动创建 和维护
createdAdminId、updatedAdminId、createdAt、updatedAt,相关内容是在Hooks钩子中维护的,如执行自定义sql需手动维护相关值。 - 前台表需继承IndexBaseModel,会自动创建 和维护
createdAdminId、updatedAdminId、createdAt、updatedAt,相关内容是在Hooks钩子中维护的,如执行自定义sql需手动维护相关值。 - @Table 装饰器可以在不传递任何参数的情况下使用,本项目约定,数据库表名使用下划线方式命名,
seqlize默认会使用复数形式命名表名,本项目推荐手动传入tableName、comment定义表名和备注。更多参数请查看seqlize 文档 。
2、 主键
@PrimaryKey 定义主键,postgresql不支持自增主键,如想实现需通过自增序列实现,但迁移时比较麻烦。本项目推荐使用自定义函数uuid维护string类型主键,方式为@Default(uuid)``@Attribute({ type: DataTypes.STRING(20), allowNull: false })。
// src/entities/exampleBook.entity.ts
import { ApiPropertyRule } from '@/decorators/index.js';
import { uuid } from '@/helper/snowflake.js';
import { RuleType } from '@/ruleType/index.js';
import { DataTypes } from '@sequelize/core';
import { Attribute, Default, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
import { AdminBaseModel } from './abstract/adminBase.entity.js';
@Table({ tableName: 'example_book', comment: '示例_书籍' })
export class ExampleBook extends AdminBaseModel<ExampleBook> {
//自动生成的主键
@Attribute({ type: DataTypes.STRING(20), allowNull: false })
@PrimaryKey
@Default(uuid)
id: string;
}3、时间列
主要指代的是 @CreatedAt, @UpdatedAt, @DeletedAt 单个装饰器标注的列。 比如:
import { InferCreationAttributes, InferAttributes, Model, CreationOptional } from '@sequelize/core';
import { CreatedAt, UpdatedAt, DeletedAt, Table } from '@sequelize/core/decorators-legacy';
@Table({ tableName: 'example_book', comment: '示例_书籍' })
export class ExampleBook extends Model<InferAttributes<ExampleBook>, InferCreationAttributes<ExampleBook>> {
@CreatedAt
declare createdAt: Date;
@UpdatedAt
declare updatedAt: Date;
@DeletedAt
declare deletedAt: Date | null;
}| 装饰器 | 描述 |
|---|---|
| @CreatedAt | 会设置 自动维护的创建时间字段 |
| @UpdatedAt | 会设置 自动维护的更新时间字段 |
| @DeletedAt | 会设置 自动维护的删除时间字段,值为非null的记录会被识别为逻辑删除 |
如果继承AdminBaseModel或IndexBaseModel会自动继承createdAt和updatedAt字段,无需手动维护。deletedAt按需求自行维护。
4、普通列
@Column 装饰器用于标注普通列,需要在参数中传入类型和备注。
// src/entities/exampleBook.entity.ts
import { ApiPropertyRule } from '@/decorators/index.js';
import { uuid } from '@/helper/snowflake.js';
import { RuleType } from '@/ruleType/index.js';
import { DataTypes } from '@sequelize/core';
import { Attribute, Default, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
import { AdminBaseModel } from './abstract/adminBase.entity.js';
@Table({ tableName: 'example_book', comment: '示例_书籍' })
export class ExampleBook extends AdminBaseModel<ExampleBook> {
//自动生成的主键
@Attribute({ type: DataTypes.STRING(20), allowNull: false })
@PrimaryKey
@Default(uuid)
id: string;
@Attribute({ type: DataTypes.STRING(20), comment: '名称', allowNull: false, defaultValue: '' })
name: string;
}更多类型请,参考seqlize 文档
数据源配置
我们支持多库数据源配置 在src/config/database.ts中配置
import { importModels, Options } from '@sequelize/core';
import { PostgresDialect } from '@sequelize/postgres';
import { appLogger } from '../logger.js';
export default async () => ({
dataSource: {
default: {
dialect: PostgresDialect,
host: process.env.DATABASE_HOST ?? '127.0.0.1',
port: process.env.DATABASE_PORT ?? 5342,
database: process.env.DATABASE_DB ?? 'meadmin',
user: process.env.DATABASE_USER ?? 'root',
password: process.env.DATABASE_PASSWORD ?? 'root',
client_encoding: 'utf8',
models: await importModels((import.meta.dirname + '/../**/*.entity.js').replace(/\\/g, '/')), // 支持如下的扫描形式,或实体形式[User,Admin]
options: `-c search_path=${process.env.DATABASE_SCHEMA}`, //设置模式查询顺序
define: {
underscored: true, //强制表名和列名转换为snake_case
freezeTableName: true, //强制模型名称不变换(取消表名的单词复数转换和snake_case转换)
timestamps: false, // 禁用createAt和updateAt的自动声明
noPrimaryKey: true, //禁止自动创建主键id
schema: process.env.DATABASE_SCHEMA ?? 'public',
timezone: 'Asia/Shanghai',
},
logging(sql, timing, seqlize?) {
appLogger.info('[sql]耗时 %d ms,%s', timing, sql, seqlize?.bind);
},
benchmark: true, //开启日志打印sql耗时参数传递
} as Options<PostgresDialect>,
},
// 第二个数据源
default2: {
// ...
},
});使用时
import { InjectRepository, Transaction } from '@/decorators/index.js';
import { Provide } from '@midwayjs/core';
import { BadRequestError } from '@midwayjs/core/dist/error/http.js';
import { User } from '../../../entities/user.entity.js';
//用户
@Provide()
export class UserService {
@InjectRepository(User)//声明entity,默认加载default数据源相当于@InjectRepository(User,'default')
userRepository: typeof User;
/**
* 根据主键获取一条信息
* @param id 主键
* @returns
*/
@Transaction()
async findOne(id: string) {
const entity = await this.userRepository.findByPk(id, {
include: [
'createdUser'
],
});
if (!entity) {
throw new BadRequestError('没有对应的信息');
}
return entity;
}
}模型关联
可以通过 @HasMany 、@HasOne 、@BelongsTo、@BelongsToMany 装饰器在模型中直接描述关系。
1、一对多或一对一从属
import { ApiPropertyRule } from '@/decorators/index.js';
import { uuid } from '@/helper/snowflake.js';
import { RuleType } from '@/ruleType/index.js';
import { BelongsToModel } from '@/types/entity.js';
import { DataTypes, NonAttribute } from '@sequelize/core';
import { Attribute, BelongsTo, Default, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
import { AdminBaseModel } from './abstract/adminBase.entity.js';
import { User } from './user.entity.js';
@Table({ tableName: 'example_demo', comment: '示例_Demo' })
//继承自DelParanoidModel则使用软删除。
export class ExampleDemo extends AdminBaseModel<ExampleDemo> {
//自动生成的主键
@Attribute({ type: DataTypes.STRING(20), allowNull: false })
@PrimaryKey
@Default(uuid)
@ApiPropertyRule({ description: 'ID', rule: RuleType.string() })
id: string;
//ApiPropertyRule对RuleType规则做了封面,对于非必填的number和string自动允许null值,如果不允许null请设置required()或者设置invalid(null)
//前端会根据rule生成表达校验,包括必填、类型(string、number)、mobile、email、min、max。
@Attribute({ type: DataTypes.STRING(20), comment: '名称', allowNull: false, defaultValue: '' })
name: string;
//反向BelongsTo关联从属, 文档 可参考https://sequelize.org/docs/v7/associations/belongs-to/
@Attribute({ type: DataTypes.STRING(20), comment: '关联前台用户id' })
userId: string;
@BelongsTo(() => User, {
foreignKey: 'userId', //外键名称
foreignKeyConstraints: false, //数据库不创建外键,外键应用层解决
})
user?: NonAttribute<User>;
}
//声明自动关联方法
export declare interface ExampleDemo extends BelongsToModel<'user', User> {}sequelize-typescript 会在内部进行关联,会自动查询出相关的依赖。
const entity = await this.userRepository.findByPk(id, {
include: ['user'],
});多对多
import { ApiPropertyRule } from '@/decorators/index.js';
import { uuid } from '@/helper/snowflake.js';
import { RuleType } from '@/ruleType/index.js';
import { BelongsManyModel } from '@/types/entity.js';
import { DataTypes, NonAttribute } from '@sequelize/core';
import { Attribute, BelongsToMany, Default, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
import { AdminBaseModel } from './abstract/adminBase.entity.js';
import { ExampleBook } from './exampleBook.entity.js';
//rule规则使用添加接口的校验规则,建议字符串的默认值统一使用空串,否则RuleType.string需要显示声明allow(null)允许传入null
@Table({ tableName: 'example_demo', comment: '示例_Demo' })
//继承自DelParanoidModel则使用软删除。
export class ExampleDemo extends AdminBaseModel<ExampleDemo> {
//自动生成的主键
@Attribute({ type: DataTypes.STRING(20), allowNull: false })
@PrimaryKey
@Default(uuid)
@ApiPropertyRule({ description: 'ID', rule: RuleType.string() })
id: string;
//ApiPropertyRule对RuleType规则做了封面,对于非必填的number和string自动允许null值,如果不允许null请设置required()或者设置invalid(null)
//前端会根据rule生成表达校验,包括必填、类型(string、number)、mobile、email、min、max。
@Attribute({ type: DataTypes.STRING(20), comment: '名称', allowNull: false, defaultValue: '' })
@ApiPropertyRule({ description: '名称', rule: RuleType.string().max(20).min(1).required() })
name: string;
//多对多关联 文档 可参考https://sequelize.org/docs/v7/associations/belongs-to-many/
@BelongsToMany(() => ExampleBook, {
through: 'example_demo_books', //中间表名称 或者 对应的Model,
foreignKeyConstraints: false, //数据库不创建外键,外键应用层解决
// 如果需要在ExampleBook定义反向关联可以添加参数 inverse: {as: 'demos',}, 并在 ExampleBook中添加 /** Declared by {@link Person.likedToots} */ declare demo?: NonAttribute<ExampleBook[]>;
})
@ApiPropertyRule({
description: '书籍',
type: 'array',
items: {
type: () => ExampleBook,
},
rule: RuleType.array().items(RuleType.object({ id: RuleType.string().required() })),
})
books?: NonAttribute<ExampleBook[]>;
}
//声明自动关联方法
export declare interface ExampleDemo extends BelongsManyModel<'books', 'book', 'books', ExampleBook> {}BelongsToMany 关联会自动在目标模型上创建逆关联,目标模型也是一个 BelongsToMany 关联。您可以通过使用反向选项自定义反关联:
import { ApiPropertyRule } from '@/decorators/index.js';
import { uuid } from '@/helper/snowflake.js';
import { RuleType } from '@/ruleType/index.js';
import { BelongsManyModel } from '@/types/entity.js';
import { DataTypes, NonAttribute } from '@sequelize/core';
import { Attribute, BelongsToMany, Default, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
import { AdminBaseModel } from './abstract/adminBase.entity.js';
import { ExampleBook } from './exampleBook.entity.js';
//rule规则使用添加接口的校验规则,建议字符串的默认值统一使用空串,否则RuleType.string需要显示声明allow(null)允许传入null
@Table({ tableName: 'example_demo', comment: '示例_Demo' })
//继承自DelParanoidModel则使用软删除。
export class ExampleDemo extends AdminBaseModel<ExampleDemo> {
//自动生成的主键
@Attribute({ type: DataTypes.STRING(20), allowNull: false })
@PrimaryKey
@Default(uuid)
@ApiPropertyRule({ description: 'ID', rule: RuleType.string() })
id: string;
//ApiPropertyRule对RuleType规则做了封面,对于非必填的number和string自动允许null值,如果不允许null请设置required()或者设置invalid(null)
//前端会根据rule生成表达校验,包括必填、类型(string、number)、mobile、email、min、max。
@Attribute({ type: DataTypes.STRING(20), comment: '名称', allowNull: false, defaultValue: '' })
@ApiPropertyRule({ description: '名称', rule: RuleType.string().max(20).min(1).required() })
name: string;
//多对多关联 文档 可参考https://sequelize.org/docs/v7/associations/belongs-to-many/
@BelongsToMany(() => ExampleBook, {
through: 'example_demo_books', //中间表名称 或者 对应的Model,
foreignKeyConstraints: false, //数据库不创建外键,外键应用层解决
inverse: {as: 'demos',},
// 如果需要在ExampleBook定义反向关联可以添加参数 inverse: {as: 'demos',}, 并在 ExampleBook中添加 /** Declared by {@link Person.likedToots} */ declare demo?: NonAttribute<ExampleBook[]>;
})
books?: NonAttribute<ExampleBook[]>;
}
//声明自动关联方法
export declare interface ExampleDemo extends BelongsManyModel<'books', 'book', 'books', ExampleBook> {}import { NonAttribute } from "@sequelize/core";
import { AdminBaseModel } from "./abstract/adminBase.entity.js";
import { ExampleDemo } from "./exampleDemo.entity.js";
export class ExampleBook extends AdminBaseModel<ExampleBook> {
/** Declared by {@link ExampleDemo.books} */
declare demos?: NonAttribute<ExampleDemo[]>;
}sequelize-typescript 会在内部进行关联,会自动查询出相关的依赖。
const entity = await this.exampleDemoRepository.findByPk(id, {
include: ['books'],
});更多文档请参考seqlize 文档
3、模型循环依赖
如果你使用了 @BelongsTo 装饰器定义双向依赖时,很容易触发一个模型循环依赖的错误,比如:
ReferenceError: Cannot access 'User' before initialization这是因为“emitDecoratorMetadata”在类初始化时会添加对另一个类的引用。 既然是循环导入,这样做必然会导致你遇到的错误。
解决办法是只在一侧声明关联。反面可以用 inverse property 性质声明 你还需要将用户导入标记为导入类型:
//joke.js
import { DataTypes, Model,InferAttributes,InferCreationAttributes,CreationOptional } from '@sequelize/core';
import { Attribute, PrimaryKey, AutoIncrement, NotNull, BelongsTo } from '@sequelize/core/decorators-legacy';
import type { User } from './user.js';//注意使用type引用
export class Joke extends Model<InferAttributes<Joke>, InferCreationAttributes<Joke>>
{
@Attribute(DataTypes.INTEGER)
@PrimaryKey
@AutoIncrement
declare id: CreationOptional<number>;
@Attribute(DataTypes.STRING)
@NotNull
declare content: string;
- @BelongsTo(() => User, 'userId')
user!: User;
// This is the foreign key
@Attribute(DataTypes.INTEGER)
@NotNull
declare userId: number;
declare createdAt: CreationOptional<Date>;
declare updatedAt: CreationOptional<Date>;
}//user.js
import { DataTypes,Model,InferAttributes,InferCreationAttributes,CreationOptional, NonAttribute } from '@sequelize/core';
import { Attribute, PrimaryKey, AutoIncrement, NotNull, HasMany } from '@sequelize/core/decorators-legacy';
import { Joke } from './joke.js';
export class User extends Model<InferAttributes<User>, InferCreationAttributes<User>>
{
@Attribute(DataTypes.INTEGER)
@PrimaryKey
@AutoIncrement
declare id: CreationOptional<number>;
@Attribute(DataTypes.STRING)
@NotNull
declare name: string;
@Attribute(DataTypes.STRING)
declare email: string;
declare createdAt: CreationOptional<Date>;
declare updatedAt: CreationOptional<Date>;
@HasMany(() => Joke, { foreignKey: 'userId', inverse: 'user' })
declare jokes?: NonAttribute<Joke[]>;
}相关参考github
数据库操作
import { InjectRepository } from '@/decorators/index.js';
import { Provide } from '@midwayjs/core';
import { BadRequestError } from '@midwayjs/core/dist/error/http.js';
import { Op } from '@sequelize/core';
import { User } from '../../../entities/user.entity.js';
//用户
@Provide()
export class UserService {
@InjectRepository(User) //声明entity,默认加载default数据源相当于@InjectRepository(User,'default')
userRepository: typeof User;
/**
* 根据主键获取一条信息
* @param id 主键
* @returns
*/
async findOne(id: string) {
// 查询
let result = await this.userRepository.findAll();
console.log(result);
// 新增
await this.userRepository.create({
nickname: '123',
});
// 删除
await this.userRepository.destroy({
where: {
nickname: '123',
},
});
// 联合查询
// SELECT * FROM photo WHERE name = "23" OR name = "34";
let result = await this.userRepository.findAll({
where: {
[Op.or]: [{ nickname: '23' }, { nickname: '34' }],
},
});
// => result
// 连表查询
const entity = await this.userRepository.findByPk(id, {
include: ['createdUser'],
});
if (!entity) {
throw new BadRequestError('没有对应的信息');
}
return entity;
}
}更多用法请查询:seqlize文档
事务
本项目封装了Transaction装饰器,只需要添加上,即可在函数执行时应用seqlize的托管事务,事务会在函数,执行完毕后自动提交 ,异常时自动回滚。默认情况下,Sequelize 使用 AsyncLocalStorage 在函数及子函数调用中开始的所有查询中自动使用该活跃事务。更多请参考seqlize文档
import { InjectRepository, Transaction } from '@/decorators/index.js';
import { Provide } from '@midwayjs/core';
import { User } from '../../../entities/user.entity.js';
//用户
@Provide()
export class UserService {
@InjectRepository(User) //声明entity,默认加载default数据源相当于@InjectRepository(User,'default')
userRepository: typeof User;
async create(){
// 新增
await this.userRepository.create({
nickname: '123',
});
}
/**
* 根据主键获取一条信息
* @param id 主键
* @returns
*/
@Transaction()
async findOne(id: string) {
// 查询
let result = await this.userRepository.findAll();
console.log(result);
await this.create();
}
}同步数据库结构
pnpm exec meadmin sync *同步所有- 或
pnpm meadmin sync exampleDemo,systemAdmin,exampleBook,user,file定向同步exampleDemo文件(定向同步文件时,需将关联依赖全部罗列出来,一起同步)
更多特性
连接池、自定义数据类型、读取复制、Hooks钩子等更多特性请参考seqlize 文档