如何使用Play框架与Anorm实现模型关联?
优雅实现Play+Anorm中用户与评论的关联模型
作为曾经从Ruby生态转过来的开发者,我太懂你现在的感受了——从ActiveRecord的"开箱即用关联"切换到Anorm这种更偏向SQL-first的持久层,还要适应Play的依赖注入,确实得有个过渡过程。下面我会结合你的现有结构(User.scala、UserRepository.scala),一步步实现用户与评论的关联,尽量贴合Play+Anorm的最佳实践:
1. 先定义评论模型(Comment.scala)
首先创建评论的模型类,明确和用户的外键关联(userId),保持和User类一致的风格:
case class Comment( id: Long, content: String, userId: Long, // 关联用户的外键 createdAt: java.time.LocalDateTime ) // 用于创建评论的DTO(不需要id和createdAt,由数据库生成) case class CommentCreateRequest(content: String, userId: Long)
2. 实现评论的持久层(CommentRepository.scala)
按照你现有UserRepository的模式,创建评论的仓库类,注入Database(Play依赖注入核心),同时实现和用户关联的查询方法:
import anorm._ import anorm.SqlParser._ import javax.inject.Inject import play.api.db.Database import java.time.LocalDateTime class CommentRepository @Inject()(db: Database) { // 定义Comment的SQL解析器,和User的解析器风格统一 private val commentParser: RowParser[Comment] = { long("id") ~ str("content") ~ long("user_id") ~ get[LocalDateTime]("created_at") map { case id ~ content ~ userId ~ createdAt => Comment(id, content, userId, createdAt) } } // 创建评论 def create(request: CommentCreateRequest): Option[Comment] = db.withConnection { implicit conn => SQL""" INSERT INTO comments (content, user_id, created_at) VALUES (${request.content}, ${request.userId}, ${LocalDateTime.now()}) RETURNING id, content, user_id, created_at """.as(commentParser.singleOpt) } // 根据用户ID查询该用户的所有评论 def findByUserId(userId: Long): List[Comment] = db.withConnection { implicit conn => SQL""" SELECT * FROM comments WHERE user_id = $userId ORDER BY created_at DESC """.as(commentParser.*) } // 其他常用方法:根据ID查评论、更新评论、删除评论... }
3. 整合用户与评论的关联查询
现在可以在UserRepository里添加查询"用户+关联评论"的方法,这里有两种常见方式,根据你的需求选择:
方式一:先查用户,再查关联评论(N+1查询,适合简单场景)
如果业务场景不需要复杂的关联查询,这种方式最直观,和Ruby里user.comments的逻辑类似:
// 在UserRepository中注入CommentRepository class UserRepository @Inject()(db: Database, commentRepo: CommentRepository) { // 原有的User解析器和方法保持不变... // 查询用户及其所有评论 def findUserWithComments(userId: Long): Option[(User, List[Comment])] = { findById(userId).map { user => val comments = commentRepo.findByUserId(userId) (user, comments) } } }
方式二:用SQL JOIN一次性查询(性能更优,适合复杂场景)
如果要避免N+1问题,可以用SQL JOIN一次性获取用户和评论数据,再在代码里组装关联关系:
// 在UserRepository中添加 def findUserWithCommentsViaJoin(userId: Long): Option[(User, List[Comment])] = db.withConnection { implicit conn => // 定义联合查询的解析器,注意给字段加表别名避免冲突 val userWithCommentParser: RowParser[(User, Comment)] = { long("u.id") ~ str("u.name") ~ str("u.email") ~ long("c.id") ~ str("c.content") ~ long("c.user_id") ~ get[LocalDateTime]("c.created_at") map { case uId ~ uName ~ uEmail ~ cId ~ cContent ~ cUserId ~ cCreatedAt => (User(uId, uName, uEmail), Comment(cId, cContent, cUserId, cCreatedAt)) } } val results = SQL""" SELECT u.*, c.* FROM users u LEFT JOIN comments c ON u.id = c.user_id WHERE u.id = $userId """.as(userWithCommentParser.*) results.headOption.map { case (user, _) => val comments = results.map(_._2).filter(_.id != 0) // 过滤LEFT JOIN带来的空评论 (user, comments) } }
4. 依赖注入的关键注意事项
作为Ruby开发者,你可能对Play的依赖注入有点陌生,这里划几个重点:
- 所有Repository类都要加上
@Inject()注解,这样Play的DI容器才能自动实例化并注入依赖(比如Database、其他Repository) - 在控制器里使用Repository时,要通过构造函数注入,而非手动
new:class UserController @Inject()( userRepo: UserRepository, commentRepo: CommentRepository, val controllerComponents: ControllerComponents ) extends BaseController { // 控制器方法里直接使用注入的repo def getUserWithComments(userId: Long) = Action { userRepo.findUserWithComments(userId) match { case Some((user, comments)) => Ok(Json.obj("user" -> user, "comments" -> comments)) case None => NotFound("User not found") } } }
5. 与Ruby ActiveRecord的思路对比
最后给你个小对比,帮你快速对齐概念:
- Ruby里的
has_many :comments→ 在Scala+Anorm里需要手动实现findByUserId或者JOIN查询 - Ruby里的
user.comments→ 对应上面的findUserWithComments方法 - ActiveRecord的自动关联加载 → Anorm更偏向显式SQL,你需要自己控制查询逻辑,但灵活性更高
这样实现下来,既符合Play+Anorm的设计哲学,又能让你从Ruby的思维模式平滑过渡,结构也清晰易维护。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

