You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeORM/PostgreSQL博客点赞约束问题:单用户不可重复点赞

TypeORM + PostgreSQL 博客点赞功能问题解决方案

问题背景

我正在用TypeORM和PostgreSQL开发博客,需要实现文章、评论的点赞功能,要求单用户对同一文章/评论只能点赞一次。目前编写的Like实体代码如下:

@Entity()
@Unique(["user", "post", "comment"])
export default class Like {
  @PrimaryGeneratedColumn()
  id: number;

  // @Column()
  // likeCount: number;

  @ManyToOne(() => User, (user) => user.likes, { nullable: false })
  user: User;

  @ManyToOne(() => Post, (post) => post.likes)
  post: Post;

  @ManyToOne(() => Comment, (comment) => comment.likes)
  comment: Comment;
}

但使用上述@Unique约束后无法实现预期效果,需要解决约束失效问题、表结构设计选择以及点赞数统计的问题。

约束失效原因

你用的@Unique(["user", "post", "comment"])是三个字段的联合唯一约束,但PostgreSQL中NULL值不参与唯一校验:当用户点赞文章时,comment字段为NULL;点赞评论时,post字段为NULL。由于NULL不算重复值,同一个用户可以多次给同一篇文章点赞(每次的comment都是NULL,联合约束不会判定重复),导致约束失效。

正确实现方式

方案一:单表优化(使用部分唯一索引)

不需要拆分表,通过PostgreSQL的**部分唯一索引(Partial Unique Index)**分别约束用户-文章、用户-评论的唯一性。TypeORM的@Unique不支持部分索引,需要用@Index自定义:

@Entity()
export default class Like {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => User, (user) => user.likes, { nullable: false })
  user: User;

  @ManyToOne(() => Post, (post) => post.likes)
  post: Post;

  @ManyToOne(() => Comment, (comment) => comment.likes)
  comment: Comment;

  // 用户-文章唯一约束:仅当post非空时生效
  @Index({ unique: true, where: "post_id IS NOT NULL" })
  @Column({ name: "post_id", nullable: true })
  postId: number;

  // 用户-评论唯一约束:仅当comment非空时生效
  @Index({ unique: true, where: "comment_id IS NOT NULL" })
  @Column({ name: "comment_id", nullable: true })
  commentId: number;
}

注意:这里显式声明postId和commentId是为了在索引中直接使用字段(TypeORM的关联字段会自动生成对应的外键列,但直接用关联对象的话索引定义可能不生效),也可以直接在@Index中使用关联字段的实际列名(比如postId或post_id,根据TypeORM配置调整)。

方案二:拆分两张表(逻辑更清晰)

如果觉得单表的NULL逻辑容易混淆,可以拆分出PostLike和CommentLike两个独立实体,各自维护用户与目标的唯一约束:

// PostLike 实体
@Entity()
@Unique(["user", "post"])
export class PostLike {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => User, (user) => user.postLikes, { nullable: false })
  user: User;

  @ManyToOne(() => Post, (post) => post.postLikes, { nullable: false })
  post: Post;
}

// CommentLike 实体
@Entity()
@Unique(["user", "comment"])
export class CommentLike {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => User, (user) => user.commentLikes, { nullable: false })
  user: User;

  @ManyToOne(() => Comment, (comment) => comment.commentLikes, { nullable: false })
  comment: Comment;
}

这种方式逻辑更直观,避免了单表中字段为NULL的情况,唯一约束可以直接生效。

点赞数统计方法

单表方案统计

使用TypeORM的QueryBuilder分组统计:

// 统计所有文章的点赞数
const postLikeStats = await getRepository(Like)
  .createQueryBuilder("like")
  .select("like.postId", "postId")
  .addSelect("COUNT(like.id)", "likeCount")
  .where("like.postId IS NOT NULL")
  .groupBy("like.postId")
  .getRawMany();

// 统计单篇文章的点赞数
const postId = 1;
const postLikeCount = await getRepository(Like)
  .createQueryBuilder("like")
  .where("like.postId = :postId", { postId })
  .getCount();

如果追求查询效率,可以在Post和Comment实体中添加likeCount字段,点赞/取消点赞时更新该字段:

// Post实体新增字段
@Column({ default: 0 })
likeCount: number;

// 点赞时更新(用数据库表达式避免并发问题)
await getRepository(Post).update(postId, { likeCount: () => "likeCount + 1" });

注意:高并发场景下可以配合乐观锁(@Version())进一步避免数据不一致。

拆分表方案统计

直接查询对应表的数量即可:

// 统计单篇文章点赞数
const postLikeCount = await getRepository(PostLike)
  .createQueryBuilder("postLike")
  .where("postLike.postId = :postId", { postId })
  .getCount();

// 统计单条评论点赞数
const commentLikeCount = await getRepository(CommentLike)
  .createQueryBuilder("commentLike")
  .where("commentLike.commentId = :commentId", { commentId })
  .getCount();

内容的提问来源于stack exchange,提问作者vfreis09

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 07:53:28