Skip to content

最佳实践

本项目推荐最佳实践,在数据库实体entity文件中声明所有属性、校验规则、及api文档。dto继承自entity文件,做相应处理。

entity文件

typescript
import { ApiPropertyRule } from '@/decorators/index.js';
import { uuid } from '@/helper/snowflake.js';
import { RuleType } from '@/ruleType/index.js';
import { BelongsManyModel, BelongsToModel } from '@/types/entity.js';
import { DataTypes, NonAttribute, Op } from '@sequelize/core';
import { Attribute, BelongsTo, BelongsToMany, Default, DeletedAt, Index, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
import { AdminBaseModel } from './abstract/adminBase.entity.js';
import { ExampleBook } from './exampleBook.entity.js';
import { File } from './file.entity.js';
import { User } from './user.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;

  //唯一索引名称必须全库唯一,当两个null值时唯一索引会认为不是同一个值
  @Index({ unique: true, where: { deleted_at: { [Op.isNot]: null } } }) //局部唯一索引设置只有不删除的数据加索引
  @Attribute({ type: DataTypes.STRING(11), comment: '手机号' })
  @ApiPropertyRule({ description: '手机号', rule: RuleType.string().mobile().description('手机号').required() })
  mobile: string;

  //以“名称:值1=说明1;值2=说明2”,格式声明的备注会自动创建字典和下拉列表,并且支持number、string两种类型
  @Attribute({
    comment: '类型:0=书籍;1=电子产品;2=卡片',
    defaultValue: 0,
    allowNull: false,
    type: DataTypes.TINYINT.UNSIGNED,
  })
  @ApiPropertyRule({ description: '类型:0=书籍;1=电子产品;2=卡片', rule: RuleType.number().equal(0, 1, 2).required() })
  type: number;

  //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[]>;

  //反向BelongsTo关联从属, 文档 可参考https://sequelize.org/docs/v7/associations/belongs-to/
  @Attribute({ type: DataTypes.STRING(20), comment: '关联前台用户id' })
  userId: string;
  @ApiPropertyRule({ description: '用户', type: () => User, rule: RuleType.object({ id: RuleType.string().required() }).pattern(RuleType.string(), RuleType.any()) })
  @BelongsTo(() => User, {
    foreignKey: 'userId', //外键名称
    foreignKeyConstraints: false, //数据库不创建外键,外键应用层解决
  })
  user?: NonAttribute<User>;

  //反向BelongsTo关联从属,File类型创建单文件
  @Attribute({ type: DataTypes.STRING(20), comment: '头像附件id' })
  avatarFileId: string;
  @ApiPropertyRule({ description: '头像', type: () => File, rule: RuleType.object({ id: RuleType.string().required() }).pattern(RuleType.string(), RuleType.any()) })
  @BelongsTo(() => File, {
    foreignKey: 'avatarFileId', //外键名称
    foreignKeyConstraints: false, //数据库不创建外键,外键应用层解决
  })
  avatar?: NonAttribute<File>;

  //BelongsTo多对多关联从属,File类型创建多文件选择
  @BelongsToMany(() => File, {
    through: 'example_demo_files', //中间表名称 或者 对应的Model,
    foreignKeyConstraints: false, //数据库不创建外键,外键应用层解决
  })
  @ApiPropertyRule({
    description: '附件',
    type: 'array',
    items: {
      type: () => File,
    },
    rule: RuleType.array().items(RuleType.object({ id: RuleType.string().required() })),
  })
  files?: NonAttribute<File[]>;

  @DeletedAt //设置为软删除
  @Attribute({ comment: '删除时间' })
  declare deletedAt: Date | null;
}
//声明自动关联方法
export declare interface ExampleDemo extends BelongsManyModel<'books', 'book', 'books', ExampleBook> {}
export declare interface ExampleDemo extends BelongsToModel<'user', User> {}
export declare interface ExampleDemo extends BelongsToModel<'avatar', File> {}
export declare interface ExampleDemo extends BelongsManyModel<'files', 'file', 'files', File> {}

校验文件

import { OmitDtoType } from '@/helper/dto.js';
import { InferAttributesLoose } from '@/types/entity.js';
import { ExampleDemo } from '../../../../entities/exampleDemo.entity.js';

//dto参数校验继承 entity必须使用 PickDtoType|OmitDtoType|PartialType|RequiredType|IntersectionType 之一 否则不会生效
export class ExampleDemoCreateDto extends OmitDtoType(
  ExampleDemo as new () => InferAttributesLoose<ExampleDemo>, //只保留声明属性
  ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdAdminId', 'updatedAdminId', 'createdAdmin', 'updatedAdmin'], //排除自动创建的字段
) {}
import { ApiPropertyRule } from '@/decorators/index.js';
import { PageDto } from '@/dto/page.dto.js';
import { IntersectionType, PartialType } from '@/helper/dto.js';
import { RuleType } from '@/ruleType/index.js';
import { InferAttributesLoose } from '@/types/entity.js';
import { ExampleDemo } from '../../../../entities/exampleDemo.entity.js';

//dto参数校验继承 entity必须使用 PickDtoType|OmitDtoType|PartialType|RequiredType|IntersectionType 之一 否则不会生效
export class ExampleDemoQueryDto extends IntersectionType(PageDto, PartialType(ExampleDemo as new () => InferAttributesLoose<ExampleDemo>)) {
  @ApiPropertyRule({ description: '创建时间(起)', rule: RuleType.date() })
  startCreatedAt?: Date;

  @ApiPropertyRule({ description: '创建时间(止)', rule: RuleType.date() })
  endCreatedAt?: Date;
  @ApiPropertyRule({ description: '最后更新时间(起)', rule: RuleType.date() })
  startUpdatedAt?: Date;

  @ApiPropertyRule({ description: '最后更新时间(止)', rule: RuleType.date() })
  endUpdatedAt?: Date;
}
import { OmitDtoType, PartialType } from '@/helper/dto.js';
import { InferAttributesLoose } from '@/types/entity.js';
import { ExampleDemo } from '../../../../entities/exampleDemo.entity.js';

//dto参数校验继承 entity必须使用 PickDtoType|OmitDtoType|PartialType|RequiredType|IntersectionType 之一 否则不会生效
export class ExampleDemoUpdateDto extends PartialType(
  OmitDtoType(
    ExampleDemo as new () => InferAttributesLoose<ExampleDemo>, //只保留声明属性
    ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdAdminId', 'updatedAdminId', 'createdAdmin', 'updatedAdmin'], //排除自动创建的字段
  ),
) {}

控制器

import { AdminPermission, ApiOperationResponse } from '@/decorators/index.js';
import { Body, Controller, Get, Inject, Param, Post } from '@midwayjs/core';
import { ExampleDemo } from '../../../../entities/exampleDemo.entity.js';
import { ExampleDemoCreateDto } from '../../dto/example/demoCreate.dto.js';
import { ExampleDemoQueryDto } from '../../dto/example/demoQuery.dto.js';
import { ExampleDemoUpdateDto } from '../../dto/example/demoUpdate.dto.js';
import { ExampleDemoService } from '../../service/example/demo.service.js';
import { BaseController } from '../base.controller.js';

/**
 * 为了防止防火墙禁止PUT、DELETE请求,方便传参,除详情外统一使用post请求。
 * meadmin对controller做了装饰器继承封装,当以/开头时会使用当前controller前缀地址,不以/开头时会递归继承controller前缀地址
 */
@Controller('example/demo')
export class ExampleDemoController extends BaseController {
  @Inject()
  exampleDemoService: ExampleDemoService;

  //查询belongsTo关联模型user用户
  //获取用户信息
  @Post('/getUser')
  @ApiOperationResponse({
    responseType: ExampleDemo,
    summary: '查询用户信息',
  })
  @AdminPermission('ExampleDemoList')
  async getUser(@Body('id') id: string, @Body('username') username: string, @Body('page') page = 1, @Body('pageSize') pageSize = 10) {
    return this.success(await this.exampleDemoService.getUser(page, pageSize, id, username));
  }

  //查询belongsToMany关联模型books示例_书籍
  //获取示例_书籍信息
  @Post('/getExampleBook')
  @ApiOperationResponse({
    responseType: ExampleDemo,
    summary: '查询示例_书籍信息',
  })
  @AdminPermission('ExampleDemoList')
  async getExampleBook(@Body('id') id: string, @Body('name') name: string, @Body('page') page = 1, @Body('pageSize') pageSize = 10) {
    return this.success(await this.exampleDemoService.getExampleBook(page, pageSize, id, name));
  }

  //接口方法必须加async 方法的接口装饰器值必须/开头
  @Post('/add')
  @ApiOperationResponse({
    responseType: ExampleDemo,
    summary: '添加示例_Demo信息',
  })
  @AdminPermission('ExampleDemoAdd')
  async add(@Body() createDto: ExampleDemoCreateDto) {
    return this.success(await this.exampleDemoService.create(createDto));
  }

  //接口方法必须加async 方法的接口装饰器值必须/开头
  @Post('/')
  @ApiOperationResponse({
    responsePage: ExampleDemo,
    summary: '获取示例_Demo列表',
  })
  @AdminPermission('ExampleDemoList')
  async list(@Body() queryDto: ExampleDemoQueryDto) {
    return this.success(await this.exampleDemoService.list(queryDto));
  }

  //接口方法必须加async 方法的接口装饰器值必须/开头
  @Get('/info/:id')
  @ApiOperationResponse({
    responseType: ExampleDemo,
    summary: '根据id获取示例_Demo详情',
  })
  @AdminPermission('ExampleDemoEdit')
  async findOne(@Param('id') id: string) {
    const entity = await this.exampleDemoService.findOne(id);
    return this.success(entity);
  }

  //接口方法必须加async 方法的接口装饰器值必须/开头
  @Post('/up/:id')
  @ApiOperationResponse({
    responseType: ExampleDemo,
    summary: '根据id更新示例_Demo详情',
  })
  @AdminPermission('ExampleDemoEdit')
  async update(@Param('id') id: string, @Body() updateDto: ExampleDemoUpdateDto) {
    return this.success(await this.exampleDemoService.update(id, updateDto));
  }

  //接口方法必须加async 方法的接口装饰器值必须/开头
  @Post('/del/:id')
  @ApiOperationResponse({
    summary: '根据id删除示例_Demo信息',
  })
  @AdminPermission('ExampleDemoDel')
  async delete(@Param('id') id: string) {
    await this.exampleDemoService.remove(id);
    return this.success();
  }
}

service

import { InjectRepository, Transaction } from '@/decorators/index.js';
import { Inject, Provide } from '@midwayjs/core';
import { BadRequestError } from '@midwayjs/core/dist/error/http.js';
import { MidwayI18nService } from '@midwayjs/i18n';
import { Op } from '@sequelize/core';
import { ExampleBook } from '../../../../entities/exampleBook.entity.js';
import { ExampleDemo } from '../../../../entities/exampleDemo.entity.js';
import { User } from '../../../../entities/user.entity.js';
import { ExampleDemoCreateDto } from '../../dto/example/demoCreate.dto.js';
import { ExampleDemoQueryDto } from '../../dto/example/demoQuery.dto.js';
import { ExampleDemoUpdateDto } from '../../dto/example/demoUpdate.dto.js';

//示例_Demo
@Provide()
export class ExampleDemoService {
  @InjectRepository(ExampleDemo)
  exampleDemoRepository: typeof ExampleDemo;

  @Inject()
  i18nService: MidwayI18nService;

  //查询belongsTo关联模型user用户
  @InjectRepository(User)
  userRepository: typeof User;

  /**
   * 获取用户信息
   * @param queryDto
   * @returns
   */
  @Transaction()
  async getUser(page: number, pageSize: number, id: string, username: string = '') {
    const where = {};
    if (id) {
      where['id'] = id;
    }
    if (username) {
      where['username'] = { [Op.like]: '%' + username + '%' };
    }
    const { count, rows } = await this.userRepository.findAndCountAll({
      where,
      offset: (page - 1) * pageSize,
      limit: pageSize,
    });
    return {
      list: rows,
      total: count,
      page: page,
      pageSize: pageSize,
    };
  }

  //查询belongsToMany关联模型books示例_书籍
  @InjectRepository(ExampleBook)
  exampleBookRepository: typeof ExampleBook;

  /**
   * 获取示例_书籍信息
   * @param queryDto
   * @returns
   */
  @Transaction()
  async getExampleBook(page: number, pageSize: number, id: string, name: string = '') {
    const where = {};
    if (id) {
      where['id'] = id;
    }
    if (name) {
      where['name'] = { [Op.like]: '%' + name + '%' };
    }
    const { count, rows } = await this.exampleBookRepository.findAndCountAll({
      where,
      offset: (page - 1) * pageSize,
      limit: pageSize,
    });
    return {
      list: rows,
      total: count,
      page: page,
      pageSize: pageSize,
    };
  }

  /**
   * 创建数据
   * @param createDto
   * @returns
   */
  @Transaction()
  async create(createDto: ExampleDemoCreateDto) {
    const entity = await this.exampleDemoRepository.create(createDto);

    if (createDto.user) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setUser(createDto.user.id);
    }

    if (createDto.avatar) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setAvatar(createDto.avatar.id);
    }

    if (createDto.books) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setBooks(createDto.books.map((v) => v.id));
    }

    if (createDto.files) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setFiles(createDto.files.map((v) => v.id));
    }

    return entity;
  }

  /**
   * 列表分页查询
   * @param queryDto 查询条件
   * @returns
   */
  @Transaction()
  async list(queryDto: ExampleDemoQueryDto) {
    const where = {};
    Object.keys(queryDto).forEach((key) => {
      if (['page', 'pageSize'].includes(key)) {
        return;
      }
      if ([null, undefined, ''].includes(queryDto[key])) {
        return;
      }
      if (key === 'startCreatedAt') {
        where['createdAt'] = where['createdAt'] ?? {};
        where['createdAt'][Op.gte] = queryDto[key];
        return;
      }
      if (key === 'endCreatedAt') {
        where['createdAt'] = where['createdAt'] ?? {};
        where['createdAt'][Op.lte] = queryDto[key];
        return;
      }
      if (key === 'startUpdatedAt') {
        where['updatedAt'] = where['updatedAt'] ?? {};
        where['updatedAt'][Op.gte] = queryDto[key];
        return;
      }
      if (key === 'endUpdatedAt') {
        where['updatedAt'] = where['updatedAt'] ?? {};
        where['updatedAt'][Op.lte] = queryDto[key];
        return;
      }
      where[key] = queryDto[key];
    });
    const { count, rows } = await this.exampleDemoRepository.findAndCountAll({
      where,
      offset: (queryDto.page - 1) * queryDto.pageSize,
      limit: queryDto.pageSize,
      include: [
        'createdAdmin',
        'updatedAdmin',
        'books',
        'user',
        {
          association: 'avatar',
          attributes: { exclude: [] }, //必须设置attributes,否则file的附件属性 url属性返回给前端时没有,已提交[BUG反馈](https://github.com/sequelize/sequelize/issues/18059)
        },
        {
          association: 'files',
          attributes: { exclude: [] }, //必须设置attributes,否则file的附件属性 url属性返回给前端时没有,已提交[BUG反馈](https://github.com/sequelize/sequelize/issues/18059)
        },
      ],
      order: [['createdAt', 'DESC']],
    });
    return {
      list: rows,
      total: count,
      page: queryDto.page,
      pageSize: queryDto.pageSize,
    };
  }

  /**
   * 根据主键获取一条信息
   * @param id 主键
   * @returns
   */
  @Transaction()
  async findOne(id: string) {
    const entity = await this.exampleDemoRepository.findByPk(id, {
      include: [
        'createdAdmin',
        'updatedAdmin',
        'books',
        'user',
        {
          association: 'avatar',
          attributes: { exclude: [] }, //必须设置attributes,否则file的附件属性 url属性返回给前端时没有,已提交[BUG反馈](https://github.com/sequelize/sequelize/issues/18059)
        },
        {
          association: 'files',
          attributes: { exclude: [] }, //必须设置attributes,否则file的附件属性 url属性返回给前端时没有,已提交[BUG反馈](https://github.com/sequelize/sequelize/issues/18059)
        },
      ],
    });
    if (!entity) {
      throw new BadRequestError(this.i18nService.translate('没有对应的信息'));
    }
    return entity;
  }

  /**
   * 更新数据
   * @param id 主键
   * @param updateDto 数据对象
   * @returns
   */
  @Transaction()
  async update(id: string, updateDto: ExampleDemoUpdateDto) {
    const entity = await this.exampleDemoRepository.findByPk(id);
    if (!entity) {
      throw new BadRequestError(this.i18nService.translate('没有对应的信息'));
    }
    Object.assign(entity, updateDto);

    if (updateDto.user !== undefined) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setUser(updateDto.user?.id ?? null);
    }

    if (updateDto.avatar !== undefined) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setAvatar(updateDto.avatar?.id ?? null);
    }

    if (updateDto.books) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setBooks(updateDto.books.map((v) => v.id));
    }

    if (updateDto.files) {
      //关联模型用主键进行设置,用对象设置时必须确保对象为模型model的实例
      await entity.setFiles(updateDto.files.map((v) => v.id));
    }

    return await entity.save();
  }

  /**
   * 删除数据
   * @param id 主键
   * @returns
   */
  @Transaction()
  async remove(id: string) {
    const entity = await this.exampleDemoRepository.findByPk(id);
    if (!entity) {
      throw new BadRequestError(this.i18nService.translate('没有对应的信息'));
    }
    await entity.destroy();
  }
}