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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 04:35:42