nestjs[typeorm学习之多对多表关系探究与使用]_typeorm 多对多-程序员宅基地

技术标签: nestJS  nestjs  

nestjs入门学习规划:https://blog.csdn.net/lxy869718069/article/details/114028195

多对多关系

多对多是一种 A 包含多个 B 实例,而 B 包含多个 A 实例的关系。

例如:
一个问题可能有多种类别,比如:问题A是既是一个地理类又是计算类问题,而一个类别也可以修饰多个问题,比如:地理类问题可以有问题A也可以有问题C。
数据库显示如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
多对多关系,实际上就是多用一张表来存放两种数据之间的关联关系。

常规的两种多对多方式及其项目解析

第一种方式:typeorm官方的示例进行的多对多关系。

目录结构:
在这里插入图片描述

1.建立两个entity并使用@ManyToMany装饰器绑定关系:

表1 category.entity.ts 内容如下:

import {
    
  Column,
  Entity,
  PrimaryGeneratedColumn,
  BaseEntity,
  ManyToMany,
} from 'typeorm';

import {
     Question } from './question.entity';

/* 类别表 */
@Entity()
export class Category extends BaseEntity {
    
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
     type: 'varchar', name: 'name', comment: '类别名称' })
  name: string;

  @ManyToMany(() => Question, (question) => question.categories)
  questions: Question[];
}

表2 question.entity.ts 内容如下:


import {
    
  Column,
  Entity,
  PrimaryGeneratedColumn,
  BaseEntity,
  JoinTable,
  ManyToMany,
  RelationId,
} from 'typeorm';

import {
     Category } from './category.entity';

/* 问题表---主表带有@JoinTable */
@Entity()
export class Question extends BaseEntity {
    
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
     comment: '标题' })
  title: string;

  @ManyToMany(() => Category, (category) => category.questions)
  @JoinTable()
  categories: Category[];
}

解析:这两个实体建立完成之后,将会默认在数据库中生成三张表,虽然只有两个实体,但是确实会生成三张表,其中第三张为关联表,只记录两表之间的关联关系。

@ManyToMany这个方法传递两个参数第一个参数为一个回调函数,直接指向绑定关联的实体类,第二个参数也是一个回调函数,但是包含一个参数,这个参数代指第一个参数中的实体类
因此可以:category.questions 和 question.categories

@JoinTable()通常放置在主要操作的那个表中

2.建立控制层和服务层,进行增删查改操作。

控制层 user.controller.ts 内容如下:


import {
    
  Controller,
  Get,
  Post,
  Delete,
  Body,
  Query,
  Put,
} from '@nestjs/common';

import {
     Transaction, TransactionManager, EntityManager } from 'typeorm';

import {
     UserService } from './user.service';

import {
     Question } from '../entities/question.entity';

@Controller('user')
export class UserController {
    
  constructor(private readonly userService: UserService) {
    }

  /* 
    查询所有列表 
    @Query 无
  */
  @Get('list')
  findAll(): Promise<Question[]> {
    
    return this.userService.findAll();
  }
  /* 
    查询单个详情
    @Query ?id=xxx
   */
  @Get('detail')
  findOne(@Query() query): Promise<Question> {
    
    return this.userService.findOne(query);
  }
  /* 
    新增账号和用户信息数据
    新增时候由于有可能出现两张表同时进行操作的情况
    因此开启事务事件:为了让同时进行的表操作要么一起完成,要么都失败
    @Transaction()和 @TransactionManager() manager: EntityManager 是事务的装饰器和对象
    @Body 
    {
      "title": "问题5",
      "categorylist": [
          1,
          3
      ]
    }
  */
  @Post('add')
  @Transaction()
  addOne(
    @Body() rUser,
    @TransactionManager() manager: EntityManager,
  ): Promise<String> {
    
    return this.userService.addOne(rUser, manager);
  }
  /* 
    修改账号和用户信息数据
    事务同上
    @Body 有数据id则修改否则新增,然后对比数据库数据进行多余的删除
    {
      "id":3,
      "title": "问题555",
      "categorylist": [
          1,
          2
      ]
    }
  */
  @Put('update')
  @Transaction()
  updateOne(
    @Body() uUser,
    @TransactionManager() manager: EntityManager,
  ): Promise<String> {
    
    return this.userService.updateOne(uUser, manager);
  }
  /* 
    删除数据
    事务同上
    @Query ?id=xxx
   */
  @Delete('del')
  @Transaction()
  delOne(
    @Query() query,
    @TransactionManager() manager: EntityManager,
  ): Promise<String> {
    
    return this.userService.delOne(query, manager);
  }
}


这里的内容非常简单,代码中描述应该已经到位了:就是加了四个接口而已。其中涉及到了事务的概念。可以参考文章:
https://editor.csdn.net/md?articleId=114012116

服务层user.service.ts内容如下:


import {
     Injectable } from '@nestjs/common';
import {
     getRepository } from 'typeorm';

import {
     Question } from './../entities/question.entity';
import {
     Category } from './../entities/category.entity';

/* 
  使用Repository<>对象执行增删查改的操作
*/
@Injectable()
export class UserService {
    
  constructor() {
    }
  /* 
    获取所有用户数据列表
  */
  async findAll(): Promise<Question[]> {
    
    /*
    构建QueryBuilder查询
    */
    let list = await getRepository(Question)
      .createQueryBuilder('question')
      .leftJoinAndSelect('question.categories', 'category')
      .getMany();
    return list;
  }
  /* 
    获取单个用户详情
  */
  async findOne(query): Promise<Question> {
    
    let list = await getRepository(Question)
      .createQueryBuilder('question')
      .leftJoinAndSelect('question.categories', 'category')
      .where('question.id = :id', {
     id: query.id })
      .getOne();
    return list;
  }
  /* 
    新增用户
    rUser格式
    {
      "title": "问题5",
      "categorylist": [
          1,
          3
      ]
    }
  */
  async addOne(rUser, manager): Promise<String> {
    
    let lists = []; // 用于保存拥有的categories
    // 目的用于获取关联信息相关的内容
    if (Object.keys(rUser.categorylist).length != 0) {
    
      for (let i = 0; i < rUser.categorylist.length; i++) {
    
        let listOne = await manager.findOne(Category, {
    
          id: rUser.categorylist[i],
        });
        lists.push(listOne);
      }
    }
    const question = new Question();
    question.title = rUser.title;
    // 此处为关联表内容新增的关键
    question.categories = lists;
    await manager.save(Question, question);
    return '新增成功!';
  }
  /* 
    修改用户
    uUser格式
    {
      "id":3,
      "title": "问题555",
      "categorylist": [
          1,
          2
      ]
    }
  */
  async updateOne(uUser, manager): Promise<String> {
    
    let lists = []; // 用于保存拥有的categories
    // 目的用于获取关联信息相关的内容
    if (Object.keys(uUser.categorylist).length != 0) {
    
      for (let i = 0; i < uUser.categorylist.length; i++) {
    
        let listOne = await manager.findOne(Category, {
    
          id: uUser.categorylist[i],
        });
        lists.push(listOne);
      }
    }
    const question = new Question();
    // 此处给了id并且categories赋了新内容,这样就会自动更新关联表中的数据(删除旧的新增新的)
    question.id = uUser.id;
    question.title = uUser.title;
    // 此处为关联表内容新增的关键
    question.categories = lists;
    await manager.save(Question, question);
    return '修改成功!';
  }
  /* 
    删除用户
  */
  async delOne(query, manager): Promise<String> {
    
    // 删除时候关联信息会自动删除
    await manager.delete(Question, {
     id: query.id });
    return '删除成功!';
  }
}

解读:单个查询和总体查询就不多做描述,仅仅只是用了关联查询而已。
新增和修改则是直接关联起来两者即可。如: question.categories = lists;,并且关联表会自动进行数据改变。
删除同样是如此,以带有@JoinTable()这个装饰器的实体类为主要操作对象。

注意:记得在user.module.ts中注册
user.module.ts内容如下:


import {
     Module } from '@nestjs/common';
import {
     UserController } from './user.controller';
import {
     UserService } from './user.service';
import {
     TypeOrmModule } from '@nestjs/typeorm';
import {
     Category } from './../entities/category.entity';
import {
     Question } from './../entities/question.entity';

@Module({
    
  imports: [TypeOrmModule.forFeature([Question, Category])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {
    }

第二种方式:主动建立关联实体类来记录两表的关联关系

目录结构:
注意:这里是有三个实体类的。
在这里插入图片描述

1.建立三个entity,通过第三方实体类来相互链接。

实体1 :category.entity.ts 内容为:

import {
     Column, Entity, PrimaryGeneratedColumn, BaseEntity } from 'typeorm';

/* 类别表 */
@Entity()
export class Category extends BaseEntity {
    
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
     type: 'varchar', name: 'type', comment: '类别名' })
  type: string;

  @Column({
     type: 'varchar', name: 'value', comment: '类别值' })
  value: string;
}

实体2 :question.entity.ts 内容为:

import {
     Column, Entity, PrimaryGeneratedColumn, BaseEntity } from 'typeorm';

/* 问题表 */
@Entity()
export class Question extends BaseEntity {
    
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    
    comment: '标题',
    name: 'title',
  })
  title: string;

  @Column({
    
    comment: '描述',
    name: 'description',
    nullable: true,
  })
  description: string;
}

实体3 :question_category.entity.ts 内容为:

import {
     Column, Entity, PrimaryGeneratedColumn, BaseEntity } from 'typeorm';

/* 问题和类别---关联表 */
@Entity()
export class QuestionCategory extends BaseEntity {
    
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    
    comment: '问题id',
    name: 'questionId',
  })
  questionId: string;

  @Column({
    
    comment: '类别id',
    name: 'categoryId',
  })
  categoryId: string;
}

仔细观察上面三个实体的关系会知道,question_category.entity就是前两者的关联内容,但是他们之间并没有用@ManyToMany来进行绑定,甚至category.entity和question.entity看不出有任何的关联关系。
但我们就是利用question_category.entity 来进行多对多的数据保存于处理。这样的好处是能够让实体类与表进行完整的一一对应。我更喜欢这种。

2.建立控制层和服务层,进行增删查改操作。

控制层 user.controller.ts 内容如下:

import {
    
  Controller,
  Get,
  Post,
  Delete,
  Body,
  Query,
  Put,
} from '@nestjs/common';

import {
     Transaction, TransactionManager, EntityManager } from 'typeorm';

import {
     UserService } from './user.service';

import {
     Question } from '../entities/question.entity';

@Controller('user')
export class UserController {
    
  constructor(private readonly userService: UserService) {
    }

  /* 
    查询所有列表 
    @params 无
  */
  @Get('list')
  findAll(): Promise<Question[]> {
    
    return this.userService.findAll();
  }
  /* 
    查询单个详情
    @Query ?id=xxx
   */
  @Get('detail')
  findOne(@Query() query): Promise<Question> {
    
    return this.userService.findOne(query);
  }
  /* 
    新增账号和用户信息数据
    新增时候由于有可能出现两张表同时进行操作的情况
    因此开启事务事件:为了让同时进行的表操作要么一起完成,要么都失败
    @Transaction()和 @TransactionManager() manager: EntityManager 是事务的装饰器和对象
    @Body :注意先给category表随便加三条数据
    {
      "title": "问题1",
      "description": "这是1号问题的描述",
      "list": [1,3]
    }
  }
  */
  @Post('add')
  @Transaction()
  addOne(
    @Body() rUser,
    @TransactionManager() manager: EntityManager,
  ): Promise<String> {
    
    return this.userService.addOne(rUser, manager);
  }
  /* 
    修改账号和用户信息数据
    事务同上
    @Body
    {
      "id": 1,
      "title": "问题1.111",
      "description": "问题1.111的描述",
      "list": [1,2]
    }
  */
  @Put('update')
  @Transaction()
  updateOne(
    @Body() uUser,
    @TransactionManager() manager: EntityManager,
  ): Promise<String> {
    
    return this.userService.updateOne(uUser, manager);
  }
  /* 
    删除数据
    事务同上
    @Query ?id=xxx
   */
  @Delete('del')
  @Transaction()
  delOne(
    @Query() query,
    @TransactionManager() manager: EntityManager,
  ): Promise<String> {
    
    return this.userService.delOne(query, manager);
  }
}

解析:控制层的内容和第一种方式基本上没什么区别,都是为了定义几个接口名称和事务而已。

服务层 user.service.ts 的内容如下:

import {
     Injectable } from '@nestjs/common';
import {
     getRepository } from 'typeorm';

import {
     Question } from './../entities/question.entity';
import {
     Category } from './../entities/category.entity';
import {
     QuestionCategory } from './../entities/question_category.entity';

/* 
  使用Repository<>对象执行增删查改的操作
*/
@Injectable()
export class UserService {
    
  constructor() {
    }
  /* 
    获取所有用户数据列表
  */
  async findAll(): Promise<Question[]> {
    
    // 构建QueryBuilder查询
    /* 
      1.Question、Category,QuestionCategory 指的是实体类的名称
      2.innerJoin()是对关联表进行关联,quescate,question,category都是别名 
      ------ 三个参数含义分别是:实体对象,别名,关联关系
      3.innerJoinAndMapMany()中的question.list是为了给question增加一个list字段用于保存Category的所有内容
      ------ 四个参数含义分别是:展示列表名,实体对象,别名,关联关系
    */
    let list = await getRepository(Question)
      .createQueryBuilder('question')
      .innerJoin(
        QuestionCategory,
        'quescate',
        'question.id = quescate.questionId',
      )
      .innerJoinAndMapMany(
        'question.list',
        Category,
        'category',
        'category.id = quescate.categoryId',
      )
      .getMany();
    return list;
    /*
      另一种使用getManager().query("sql语句")执行原生sql操作即可。
      补充:联表查询建议使用QueryBuilder自由构建出所需要的查询内容
    */
  }
  /* 
    获取单个用户详情
  */
  async findOne(query): Promise<Question> {
    
    let list = await getRepository(Question)
      .createQueryBuilder('question')
      .innerJoin(
        QuestionCategory,
        'quescate',
        'question.id = quescate.questionId',
      )
      .innerJoinAndMapMany(
        'question.list',
        Category,
        'category',
        'category.id = quescate.categoryId',
      )
      .where('question.id = :id', {
     id: query.id })
      .getOne();
    return list;
  }
  /* 
    新增用户
    rUser格式:注意先给category表随便加三条数据
    {
      "title": "问题1",
      "description": "这是1号问题的描述",
      "list": [1,3]
    }
  */
  async addOne(rUser, manager): Promise<String> {
    
    // 先保存问题的数据
    let question = new Question();
    question.title = rUser.title;
    question.description = rUser.description;
    const que = await manager.save(Question, question);
    if (Object.keys(rUser.list).length != 0) {
    
      // 后保存关联表的数据
      for (let i = 0; i < rUser.list.length; i++) {
    
        let qescat = new QuestionCategory();
        qescat.categoryId = rUser.list[i];
        qescat.questionId = que.id;
        await manager.save(QuestionCategory, qescat);
      }
      return '新增成功!';
    }
  }
  /* 
    修改用户:有数据id则修改否则新增,然后对比数据库数据进行多余的删除
    uUser格式
    {
      "id": 1,
      "title": "问题1.111",
      "description": "问题1.111的描述",
      "list": [1,2]
    }
  */
  async updateOne(uUser, manager): Promise<String> {
    
    // 先修改问题表的数据
    let question = new Question();
    question.id = uUser.id;
    question.title = uUser.title;
    question.description = uUser.description;
    await manager.update(Question, {
     id: question.id }, question);
    // 在根据问题的id删除关联表中对应的数据
    await manager.delete(QuestionCategory, {
     questionId: uUser.id });
    // 之后在将新的关联数据添加进关联表
    for (let i = 0; i < uUser.list.length; i++) {
    
      let qescat = new QuestionCategory();
      qescat.categoryId = uUser.list[i];
      qescat.questionId = uUser.id;
      await manager.save(QuestionCategory, qescat);
    }
    return '修改成功!';
  }
  /* 
    删除用户
  */
  async delOne(query, manager): Promise<String> {
    
    // 先删除关联表内容
    await manager.delete(QuestionCategory, {
     questionId: query.id });
    // 然后在删除主表内容
    await manager.delete(Question, {
     id: query.id });
    return '删除成功!';
  }
}


解析
查询使用了 innerJoin 和 innerJoinAndMapMany这两个方法,这是关联查询的意思,用于关联第三张表,然后紧接着查询第二张表,这样三张表都能进行联动。具体方式代码有说明。
新增则是根据用户提交的数据,先新增主要的表数据,然后循环保存到关联表中去。分成两步来做,
修改则是修改完主数据之后,删除该主数据id在关联表中的内容,然后再把全新的关联关系新增。
删除则是先删除关联表中的管理数据,然后再删除主表的数据。

而 user.module.ts 中的内容如下:


import {
     Module } from '@nestjs/common';
import {
     UserController } from './user.controller';
import {
     UserService } from './user.service';
import {
     TypeOrmModule } from '@nestjs/typeorm';
import {
     Category } from './../entities/category.entity';
import {
     Question } from './../entities/question.entity';

@Module({
    
  imports: [TypeOrmModule.forFeature([Question, Category])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {
    }


基本上和第一种方法一样,这里同样不需要注册第三方的关联表

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lxy869718069/article/details/113975443

智能推荐

【ARIMA-WOA-LSTM】差分自回归移动平均方法-鲸鱼优化算法-LSTM预测研究(python代码实现)-程序员宅基地

文章浏览阅读113次。1]岑威钧,王肖鑫,蒋明欢.基于EEMD-LSTM-ARIMA的土石坝渗压预测模型研究[J].水资源与水工程学报,2023,34(02):180-185.[2]沈露露,梁嘉乐,周雯.基于ARIMA-LSTM的能量预测算法[J].无线电通信技术,2023,49(01):150-156.(3)采用确定好阶数的ARIMA(p , d ,q)拟合时间序列,并根据预测后的数据和原时间序列进行结果统计和预测精度分析。# ===========主程序================dim = 5 # 鲸鱼的维度。

android model封装,Android项目基类封装ViewBinding、MVP、ViewModel-程序员宅基地

文章浏览阅读609次。都会需要有用到BaseActivity,从最开始的initData、initView,到后来需要承载监听推送、监听网络变化的广播、延迟锁定等等各种需求,BaseActivity的功能越来越杂,越来越密集。相对实际的页面上的功能需求,基类的封装经过这样长时间的无脑堆砌,到最后看起来会更匪夷所思。所以从一开始,Base的封装就要足够清晰、稳健、可扩展。AndroidBase我的思路是分层继承,每一层只..._android initviewcomponent initdatacomponent

基于SpringBoot+Vue的地方美食分享网站设计与实现(源码+LW+部署文档等)_springbootvue项目源码网站-程序员宅基地

文章浏览阅读1.2k次。系统主要功能模块:1)系统功能模块:地方美食分享网站,在网站首页可以查看首页,外国美食,中式美食,热门菜品,论坛,新闻资讯,留言板,个人中心,后台管理等内容,并进行详细操作;2)管理员功能模块:管理员登录,进入系统前在登录页面根据要求填写用户名和密码,选择角色等信息,点击登录进行登录操作;3)用户功能模块:用户登录进入地方美食分享网站可以对首页,个人中心,外国美食管理,中式美食管理,热门菜品管理,论坛管理,我的收藏管理,留言板管理等进行相应操作_springbootvue项目源码网站

fd_set 用法 socket_socket fd_set-程序员宅基地

文章浏览阅读901次。一、winsock中#include <winsock.h>原型intselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,const struct timeval*timeout);nfds:本参数忽略,仅起到兼容作用。readf..._socket fd_set

iOS 音视频专栏(一)视频流H264分析以及坑_ios15.1的缺陷 h264-程序员宅基地

文章浏览阅读2.2k次。引言前段时间进新公司,做视频会议,会诊方向的。不采用任何第三方框架,包括推流,拉流,编解码等视频处理,都是自己来编写,除了音频部分要用到webrtc来处理声音降噪,增益等。过程很艰辛,也是踩了无数的坑,总算项目算是整完了。在此,记录一下音视频这块所需要掌握的一些知识点,很多网上能找到的,在这里我就尽量不会太费笔墨。写的不好,轻拍,大家一起进步。1.视频流 H264 分析网上有很多的对裸流H264_ios15.1的缺陷 h264

Marvell车载以太网交换机芯片88Q5050-程序员宅基地

文章浏览阅读1.8w次,点赞13次,收藏75次。Marvell推出了四款车载用交换机芯片,88Q5050,88Q5050, 88Q5072和88Q6113。其中88Q5030有5 Port用于通信,88Q5050有8 Port用于通信,5072与6113有11 Port用于通信。由于项目中用到了88Q5050,所以本文中只涉及到88Q5050的内容。本文是对使用88Q5050的梳理和总结。_88q5050

随便推点

PyCharm + PyQt5 配置_can't open file 'c.pyuic-程序员宅基地

文章浏览阅读9.1k次。PyCharm: 4.0.5PyQ5: 5.4.1Python: 3.4.31.安装插件:Settings->Plugins->Browse Repositories, 找到 Native Neighbourhood, 点 install plugin。2. Settings-> External Tools 点 + 号,添加 QT DesignerWo_can't open file 'c.pyuic

(杂谈)攻击者与开发者的无形碰撞--逻辑漏洞的挖掘实战及反思(一)_记一次某运营商逻辑漏洞挖掘-程序员宅基地

文章浏览阅读958次,点赞10次,收藏25次。(杂谈)攻击者与开发者的无形碰撞–逻辑漏洞的挖掘实战及反思(一)​ 逻辑漏洞的挖掘 (本文约3000字看完大约20分钟)​ 本文只代表个人的经历与见解,写的不好请多多理解。前瞻​ 随着框架技术的不断发展,传统的漏洞,sql,xss,早已经被底层框架所消灭,(当然,不排除能够一一些其他方式利用 比如说“组合拳”),另外,加上各种“云waf”的_记一次某运营商逻辑漏洞挖掘

LD.exe undefined reference to XXX 错误的解决方法-程序员宅基地

文章浏览阅读2.4k次。NDK,JNI编程时,有时候会因为改了函数接口,导致出现LD.exe ...undefined reference to .XXX的错误,很可能只要把obj/local/armeabi-v7ma/objs/下面的目录删掉重新编译就ok了_ld.exe undefined reference to

区分点1 — Sigmoid 和 Softmax 区别_1-sigmoid-程序员宅基地

文章浏览阅读720次。sigmoid函数(也叫逻辑斯谛函数):其实逻辑斯谛函数也就是经常说的sigmoid函数,它的几何形状也就是一条sigmoid曲线。logistic曲线如下:softmax函数的定义:softmax是logistic函数的一般化,它将任意实数的k维向量z压缩(映射)为范围(0,1)内的实数的k维向量矩阵Z,向量Z中的所有元素的总和为1。这句话既表明了softmax函数与logistic函数的关系,也同时阐述了softmax函数的本质就是将一个K维的任意实数向量压缩(映射)成另一个K维的实数向量,其_1-sigmoid

解决sqlserver 2005安装时提示“SQL Server服务无法启动”_sql2005安装提示sql server服务无法启动-程序员宅基地

文章浏览阅读6.2k次。在windows server 2012操作系统上安装sqlserver 2005,会提示兼容性问题,不要理会她,点击“运行程序”即可:在安装过程中,如果提示“SQL Server服务无法启动”,如下:下载“SQL2005服务启动失败.zip”,里面含有文字说明,如下:..._sql2005安装提示sql server服务无法启动

go-cqhttp系列教程-go-cqhttp安装_go-cq怎么更新最新版-程序员宅基地

文章浏览阅读2.9k次。我尝试了好多机器人框架,大部分都开发前景不好,无意中发现了这个go-cqhttp机器人框架,使用http协议将消息推送到URL上面,也是使用URL对他回应,即可实现发信。_go-cq怎么更新最新版

推荐文章

热门文章

相关标签