3.3 使用virtual优化

目前我们有4张表(集合),User是最基础的,其它3张表都有个userId字段表示与它的关联。

我们可以使用deno_mongo_schema的virtual功能进行优化。

代码

posts.schema.ts

src/posts/posts.schema.ts,新增:

typescript 复制代码
import { Comment } from "../comments/comments.schema.ts";
import {
  BaseSchema,
  Prop,
  Schema,
  SchemaFactory,
  VirtualTypeOptions,
} from "deno_mongo_schema";
import { User, UserInfo } from "../user/user.schema.ts";

export const PostSchema = SchemaFactory.createForClass(Post);

const userVirtual: VirtualTypeOptions = {
  ref: User,
  localField: "userId", //本表字段
  foreignField: "_id", //user表中字段
  isTransformLocalFieldToObjectID: true,
  justOne: true,
};
PostSchema.virtual("author", userVirtual);

const commentCountVirtual: VirtualTypeOptions = {
  ref: Comment,
  localField: "_id", //本表字段
  foreignField: "postId", //comment表中字段
  isTransformLocalFieldToString: true,
  count: true,
};
PostSchema.virtual("commentsCount", commentCountVirtual);

VirtualTypeOptions的几个属性也容易理解:

  • ref指向的是关联表。
  • localField是使用本表哪个字段进行关联。
  • foreignField是被关联表的哪个字段。
  • isTransformLocalFieldToObjectID表示是否把本表的字段转换成mongodb的id进行关联。_id是mongodb的默认id,格式是一个复杂对象ObjectId,并不是纯粹的字符串,如果不进行转换,二者是关联不上的。相应的,如果要把本表的ObjectId转换为字符串,设置属性isTransformLocalFieldToString。
  • justOne,关联是一对多的关系,默认会返回数组,如果justOne,则只取第一个。
  • count,指的是统计关联的数量,性能比取到所有数据要好,如果只用数量不用具体数据,推荐使用。这里的commentsCount就用到了count属性,只会统计与博客相关联的留言数量。

PostSchema.virtual的第一个参数表示关联后会挂载到博客对象上的哪个字段。

为了性能考虑,deno_mongo_schema设定为这里只是将关联信息注册,真正是否使用这些字段进行关联,仍要在具体方法里配置使用,后文会介绍。

session.schema.ts

修改src/session/session.schema.ts,新增以下代码:

typescript 复制代码
import {
  BaseSchema,
  Prop,
  Schema,
  SchemaFactory,
  VirtualTypeOptions,
} from "deno_mongo_schema";
import { User } from "../user/user.schema.ts";

export const SessionSchema = SchemaFactory.createForClass(Session);

const userVirtual: VirtualTypeOptions = {
  ref: User,
  localField: "userId", //本表字段
  foreignField: "_id", //user表中字段
  isTransformLocalFieldToObjectID: true,
  justOne: true,
};
SessionSchema.virtual("user", userVirtual);

comments.schema.ts

src/comments/comments.schema.ts新增:

typescript 复制代码
import { User, UserInfo } from "../user/user.schema.ts";
import {
  BaseSchema,
  Prop,
  Schema,
  SchemaFactory,
  VirtualTypeOptions,
} from "deno_mongo_schema";

export const CommentSchema = SchemaFactory.createForClass(Comment);

const userVirtual: VirtualTypeOptions = {
  ref: User,
  localField: "userId", //本表字段
  foreignField: "_id", //user表中字段
  isTransformLocalFieldToObjectID: true,
  justOne: true,
};
CommentSchema.virtual("author", userVirtual);

comments.service.ts

src/comments/comments.service.ts修改findByPostId方法:

typescript 复制代码
async findByPostId(postId: string) {
    const arr = await this.model.findMany({
      postId,
    }, {
      populates: {
        author: true,
      },
    });
    arr.forEach((comment) => {
      comment.createdAt = format(comment.createTime, "zh_CN");
      const html = Marked.parse(comment.content).content;
      comment.contentHtml = html;
    });
    return arr;
  }

从下图对比看,删掉了获取author信息的3行代码: 这样,本篇中引入的userService就没用了,也可以删掉了。

session.service.ts

src/session/session.service.ts与之类似:

posts.service.ts

src/posts/posts.service.ts稍微复杂些。 先给PopulateOptions接口添加一个属性isWithCommentsCount:

typescript 复制代码
interface PopulateOptions {
  isWithUserInfo?: boolean;
  isWithComments?: boolean;
  isWithCommentsCount?: boolean; // 新增 
  isIncrementPv?: boolean;
}

为什么呢?上文提过,只获取数量时性能高些,比如我们首页时,并不展示具体的留言信息,只展示留言数量,这时没有必要把关联的留言都取到。所以,我们增加一个字段,表明二者目的的不同。

在开发时,需要时刻铭记一点,我们的应用与数据库的交互是昂贵的,所以一要尽可能减少交互的次数,比如不允许在循环中调用交互方法,二要尽可能减少数据库返回的数据大小,降低网络传输造成的损耗。

typescript 复制代码
export class PostsService {
  async findById(id: string, options: PopulateOptions = {}) {
    const post = await this.model.findById(id, {
      populates: this.getPopulates(options),
    });
    if (!post) {
      return;
    }
    if (options.isWithComments) {
      post.comments = await this.commentsService.findByPostId(id);
      post.commentsCount = post.comments.length;
    }
    // 增加浏览次数
    if (options.isIncrementPv) {
      this.incrementPvById(id).catch(this.logger.error);
    }
    this.format(post);
    return post;
  }

  private incrementPvById(id: string) {
    return this.model.findByIdAndUpdate(id, {
      $inc: {
        pv: 1,
      },
    });
  }

  private incrementPvByIds(ids: string[]) {
    return this.model.updateMany({
      _id: { // 注意此处是_id。底层虽然默认将_id转换为id让我们使用,但这种批量处理仍是需要使用_id,因为并没有禁止Schema中定义id字段的情况。
        $in: ids,
      },
    }, {
      $inc: { // $inc是自增的处理
        pv: 1,
      },
    });
  }

  private getPopulates(options?: PopulateOptions) {
    if (!options) {
      return;
    }
    const populates: Record<string, boolean> = {};
    if (options.isWithUserInfo) {
      populates["author"] = true;
    }
    if (options.isWithCommentsCount) {
      populates["commentsCount"] = true;
    }
    return populates;
  }

  async findAll(options: PopulateOptions = {}) {
    const posts = await this.model.findMany({}, {
      populates: this.getPopulates(options),
      sort: {
        createTime: -1,
      },
    });
    this.formatPosts(posts, options);
    return posts;
  }

  private formatPosts(
    posts: Required<Post>[],
    options: PopulateOptions = {},
  ) {
    // 增加浏览次数
    if (options.isIncrementPv) {
      this.incrementPvByIds(posts.map((post) => post.id)).catch(
        this.logger.error,
      );
    }
    posts.forEach((post) => {
      this.format(post);
    });
  }

  async findByUserId(userId: string, options: PopulateOptions = {}) {
    const posts = await this.model.findMany({
      userId,
    }, {
      populates: this.getPopulates(options),
      sort: {
        createTime: -1,
      },
    });
    this.formatPosts(posts, options);
    return posts;
  }
}

从代码对比能看出,formatPosts的代码删除掉很多:

findMany参数

要使用上面的表关联,重点是增加populates这个参数,它的key值必须与virtual的第一个参数保持一致,否则将被忽略。

另一个需要注意的是还有个sort参数,它可以在数据库底层进行排序,-1表示倒序,新的博客排在前面。

incrementPvByIds

incrementPvByIds是批量将数据的pv字段,使用$inc操作符在原来基础上加1,这样我们就不必拿到原来的pv再设置,也避免了并发错误的发生。比如原来的逻辑,假设pv是1,一个用户正在访问页面,我们进行更新,pv变成2,但更新完成前又有一个用户访问,拿到的pv就还是1,上一个更新完成(pv变成2)后,我们新的更新开始了,本来pv应该变成3,但仍设置成2了。

你可能也观察到了,调用incrementPvByIds和incrementPvById这两个方法的地方前面并没有加await,而是使用catch保障其稳定。这是因为增加浏览量这个业务逻辑与查询博客并没有直接的关系,不必等待它完成,应该尽快把数据返回给页面。同样的场景如增加日志、记录热值等运维数据记录,都应该同样处理。

findById

你可能会问,为什么findById里不同样使用表关联把comments的数据关联出来,仍要调用commentsService呢?

原因是我们展示的留言里,用到了对应的用户信息:

表关联只是把comments关联出来,并没有更深一级关联用户。所以不如调用commentsService,在它里面关联用户方便些。

如果是单纯的接口开发,博客的信息和留言的信息将是两个接口,千万别整合成一个。由于我们做的服务端渲染,所以稍微复杂些。

posts.controller.ts

src/posts/posts.controller.ts

将getAll中isWithComments修改isWithCommentsCount。

注意一点是,我并没有在findPostById中增加isWithCommentsCount属性,这样会浪费一次查找,因为我们没有对留言做分页处理。后期你添加分页时需要考虑这点。

验证

请在页面上自行验证。

作业

这次使用MongoDB重构就告一段落了。是不是很简单?deno_mongo_schema的API也是语义化的,在开发过程中使用TypeScript的类型推断查看参数即可。

下一节,我们将学习如何对我们的项目写测试。

相关推荐
良许Linux4 分钟前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
求知若饥16 分钟前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
gb42152871 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
颜淡慕潇2 小时前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
向前看-9 小时前
验证码机制
前端·后端
超爱吃士力架10 小时前
邀请逻辑
java·linux·后端
AskHarries12 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion13 小时前
Springboot的创建方式
java·spring boot·后端