使用NULL和case类进行一对多外连接的规范方法是什么?

时间:2014-09-03 21:19:24

标签: scala anorm

这是一个非常常见的用例 - 两个表之间的简单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

这一切确实有效,但这是典型的惯用方式吗?

1 个答案:

答案 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 { ... }这样的解析(在这种情况下不确定它会更好)。

  

不仅更新了最新版本的流媒体支持语法,还更新了性能(内存使用情况)。