这是一个非常常见的用例 - 两个表之间的简单1:N连接。
表:
USER
user_id email
1 a@a.com
2 b@b.com
3 c@c.com
USER_ROLE
user_id role
1 user
1 admin
2 user
在这种情况下,它需要是外连接,因为用户可能没有任何角色。
外连接返回如下结果集:
user_id email role
1 a@a.com user
1 a@a.com admin
2 b@b.com user
3 c@c.com NULL
在这里你可以看到user_id 3没有角色。
型号:
case class User(userId: Long, email: String, roles: List[Role])
sealed trait Role
case object UserRole extends Role
case object AdminRole extends Role
从数据库中的角色名称字符串到相应的案例类对象需要映射:
object Role {
def apply(name: String): Role = name match {
case "user" => UserRole
case "admin" => AdminRole
}
}
数据访问:
object Users {
def mapper = {
get[Long] ("user_id" ) ~
get[String] ("email" ) ~
(get[String]("role_name")?) map {
case userId~email~role =>
((userId, email), role)
}
}
def findById(userId: Long) : Option[User] = {
DB.withConnection { implicit connection =>
SQL("""
SELECT
u.user_id,
u.email,
r.role
FROM
user u
LEFT OUTER JOIN
user_role r
ON
u.user_id = r.user_id
""")
.on('user_id -> userId)
.as(Users.mapper.*)
.groupBy(_._1)
.map {
case ((userId, email), rest)
=> User(userId, email, rest.unzip._2.map(role => Role(role.orNull)))
}.headOption
}
}
}
映射器返回一个元组,其中每个实例都包含重复的用户数据和角色。
这适用于除了没有角色的情况之外的所有内容,map函数最终会尝试执行Role(null)并失败。
因此,在角色为NULL的情况下,用户实例应该以空List()而不是角色列表结束。基本上,我不想执行最里面的地图功能。
所以,我把那个map子句更改为:
.map {
case ((userId, email), rest)
=> User(userId, email, {
val roles = rest.unzip._2
roles.head match {
case Some(role) => roles.map(role => Role(role.orNull))
case None => List()
}
})
}.headOption
这一切确实有效,但这是典型的惯用方式吗?
答案 0 :(得分:1)
直到Anorm 2.3,您可以使用以下流式传输来解析分层/自定义聚合行。
import anorm.{ Row, SQL }
@annotation.tailrec
def parse(res: Stream[Row], map: Map[Int, User]): Iterable[User] = res.headOption match {
case Some(row) => {
val id = row[Int]("user_id")
val user = map.lift(id).getOrElse(User(id, row[String]("email"), Nil))
val role = row[Option[String]]("role") flatMap {
case "user" => Some(UserRole)
case "admin" => Some(AdminRole)
case _ => None // Unsupported - Error handling
}
parse(res.tail, map + (
id -> role.fold(user)(r => user.copy(roles = roles :+ r))))
}
case _ => map.values
}
val res: Stream[Row] = SQL("SELECT ...").apply()
val users = parse(res, Map.empty[Int, User])
使用当前的Anorm主控(下一个流媒体支持),您可以使用withResult
。
import anorm.{ Cursor, SQL }
@annotation.tailrec
def parse(cur: Option[Cursor], map: Map[Int, User]): Iterable[User] = cur match {
case Some(cursor) => {
val row = cursor.row
val id = row[Int]("user_id")
val user = map.lift(id).getOrElse(User(id, row[String]("email"), Nil))
val role = row[Option[String]]("role") flatMap {
case "user" => Some(UserRole)
case "admin" => Some(AdminRole)
case _ => None // Unsupported - Error handling
}
parse(cursor.next, map + (
id -> role.fold(user)(r => user.copy(roles = roles :+ r))))
}
case _ => map.value
}
val users = SQL("SELECT ...").withResult(parse(_, Map.empty[Int, User]))
使用Anorm master,您也可以使用.fold
。
import anorm.SQL
val map: Map[Int, User] = SQL("SELECT ...").fold(Map.empty[Int, User]) { (map, row) =>
val id = row[Int]("user_id")
val user = map.lift(id).getOrElse(User(id, row[String]("email"), Nil))
val role = row[Option[String]]("role") flatMap {
case "user" => Some(UserRole)
case "admin" => Some(AdminRole)
case _ => None // Unsupported - Error handling
}
map + (id -> role.fold(user)(r => user.copy(roles = roles :+ r)))
}
val users = map.values
此流媒体方法也可以与SqlParser
用法结合使用。而不是使用row[T]("label")
来提取当前行上的每个值,可以在行上应用像SqlParser.int("user_id") ~ SqlParser.str("email") map { ... }
这样的解析(在这种情况下不确定它会更好)。
不仅更新了最新版本的流媒体支持语法,还更新了性能(内存使用情况)。