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

