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

Spring项目中基于多对多实现点赞及Answer实体点赞数统计问询

实现用户单次点赞回答+回答点赞数统计的Spring方案

嘿,这个需求太常见了!我来给你一步步拆解怎么在Spring项目里实现用户单次点赞回答,同时在Answer实体里维护点赞统计数的功能。咱们从实体映射开始,再到业务逻辑,最后聊聊高并发场景下的优化点。

1. 实体类配置(以JPA为例)

首先咱们把三个表对应的实体类配置好,重点是给Answer实体新增likeCount字段,同时处理好关联关系。

User实体

@Entity
@Table(name = "USER")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;

    // 可选:关联用户的所有点赞记录(双向关联)
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private Set<UserAnswerLike> userAnswerLikes = new HashSet<>();

    // getter、setter、构造器省略
}

Answer实体(核心:新增likeCount字段)

@Entity
@Table(name = "ANSWER")
public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String message;

    // 新增的点赞统计字段,默认初始为0
    private Integer likeCount = 0;

    // 关联该回答的所有点赞记录
    @OneToMany(mappedBy = "answer", fetch = FetchType.LAZY)
    private Set<UserAnswerLike> userAnswerLikes = new HashSet<>();

    // getter、setter、构造器省略
}

UserAnswerLike实体(中间关联表)

这个表是用户和回答的关联表,用来实现“单次点赞”的约束,咱们用嵌入式主键来处理联合主键:

@Entity
@Table(name = "user_answer_likes", 
       uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "answer_id"})) // 强制一个用户只能给同一回答点一次赞
public class UserAnswerLike {
    @EmbeddedId
    private UserAnswerLikeId id;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("userId") // 映射主键中的userId字段
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("answerId") // 映射主键中的answerId字段
    @JoinColumn(name = "answer_id")
    private Answer answer;

    // 嵌入式主键类,必须实现Serializable
    @Embeddable
    public static class UserAnswerLikeId implements Serializable {
        private Integer userId;
        private Integer answerId;

        // 必须重写equals和hashCode,否则JPA无法识别主键
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            UserAnswerLikeId that = (UserAnswerLikeId) o;
            return Objects.equals(userId, that.userId) && Objects.equals(answerId, that.answerId);
        }

        @Override
        public int hashCode() {
            return Objects.hash(userId, answerId);
        }

        // getter、setter、构造器省略
    }

    // getter、setter、构造器省略
}

2. 点赞/取消点赞的业务逻辑实现

接下来咱们实现核心的点赞切换逻辑,先定义Repository接口,再写Service层代码。

Repository接口

首先需要三个Repository,其中UserAnswerLikeRepository需要自定义方法来检查点赞状态和删除点赞记录:

// UserRepository和AnswerRepository直接继承JpaRepository即可
public interface UserAnswerLikeRepository extends JpaRepository<UserAnswerLike, UserAnswerLike.UserAnswerLikeId> {
    // 检查用户是否已经点赞过该回答
    boolean existsByUserIdAndAnswerId(Integer userId, Integer answerId);
    // 根据用户ID和回答ID删除点赞记录
    void deleteByUserIdAndAnswerId(Integer userId, Integer answerId);
}

Service层实现

在Service里处理点赞/取消点赞的逻辑,同时更新Answer的likeCount:

@Service
@Transactional
public class AnswerLikeService {
    private final UserAnswerLikeRepository userAnswerLikeRepository;
    private final AnswerRepository answerRepository;

    // 构造器注入(推荐)
    public AnswerLikeService(UserAnswerLikeRepository userAnswerLikeRepository, AnswerRepository answerRepository) {
        this.userAnswerLikeRepository = userAnswerLikeRepository;
        this.answerRepository = answerRepository;
    }

    // 点赞/取消点赞切换方法
    public void toggleLike(Integer userId, Integer answerId) {
        boolean hasLiked = userAnswerLikeRepository.existsByUserIdAndAnswerId(userId, answerId);
        Answer answer = answerRepository.findById(answerId)
                .orElseThrow(() -> new RuntimeException("目标回答不存在"));

        if (hasLiked) {
            // 已点赞,执行取消操作
            userAnswerLikeRepository.deleteByUserIdAndAnswerId(userId, answerId);
            answer.setLikeCount(Math.max(answer.getLikeCount() - 1, 0)); // 确保点赞数不会变成负数
        } else {
            // 未点赞,执行点赞操作
            UserAnswerLike likeRecord = new UserAnswerLike();
            UserAnswerLike.UserAnswerLikeId likeId = new UserAnswerLike.UserAnswerLikeId();
            likeId.setUserId(userId);
            likeId.setAnswerId(answerId);
            likeRecord.setId(likeId);
            // 可选:关联User和Answer实体(如果需要的话)
            // likeRecord.setUser(userRepository.findById(userId).orElseThrow(...));
            // likeRecord.setAnswer(answer);
            userAnswerLikeRepository.save(likeRecord);
            answer.setLikeCount(answer.getLikeCount() + 1);
        }
        answerRepository.save(answer);
    }
}

3. 高并发场景优化建议

如果你的项目有高并发点赞的场景,上面的基础实现可能会有线程安全问题,这里给两个优化方向:

原子更新点赞数

直接用数据库的原子操作来更新点赞数,避免多线程下的计数错误。可以在AnswerRepository里定义自定义SQL:

public interface AnswerRepository extends JpaRepository<Answer, Integer> {
    @Modifying
    @Query("UPDATE Answer a SET a.likeCount = a.likeCount + 1 WHERE a.id = :answerId")
    int incrementLikeCount(@Param("answerId") Integer answerId);

    @Modifying
    @Query("UPDATE Answer a SET a.likeCount = a.likeCount - 1 WHERE a.id = :answerId AND a.likeCount > 0")
    int decrementLikeCount(@Param("answerId") Integer answerId);
}

然后修改Service里的逻辑,调用这两个方法代替直接设置likeCount,这样数据库会保证操作的原子性。

缓存优化

如果点赞数的访问频率很高,可以用Redis缓存Answer的点赞数。点赞/取消点赞时,先更新缓存,再异步更新数据库;或者更新数据库后失效缓存,下次查询时从数据库加载并写入缓存,减少数据库压力。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 04:19:01