Play 2.0 用户指南 - 访问SQL数据库 -- 针对Scala开发者

配置JDBC链接池

 
    Play 2.0 提供了一个内置插件来管理链接池。你能够配置多个数据库。

    为了使用数据库插件,在conf/application文件中配置链接池。依照惯例,默认的JDBC数据源命名为 default:
# Default database configuration
db.default.driver=org.h2.Driver
db.default.url=jdbc:h2:mem:play


    配置多个数据源
# Orders database
db.orders.driver=org.h2.Driver
db.orders.url=jdbc:h2:mem:orders

# Customers database
db.customers.driver=org.h2.Driver
db.customers.url=jdbc:h2:mem:customers


    若是发生任何配置错误,你將会在浏览器中直接看到:


    配置JDBC驱动


    除了H2这种内存数据库,在开发环境下有用外,Play 2.0 不提供任何的数据库驱动。所以,部署到生产环境中,你须要加入所需的驱动依赖。

    例如,你若是使用MySQL5,你须要为connector加入依赖:
val appDependencies = Seq(
"mysql" % "mysql-connector -java" % "5.1.18"
)

    访问JDBC数据源

    play.api.db 包提供了访问配置数据源的方法:java

import play.api.db._

val ds = DB.getDatasource()


    获取JDBC链接

    有几种方式可获取JDBC链接,如第一种最常使用的:
val connection = DB.getConnection()

    可是,你须要在某个地方调用close方法关闭链接。另外一种方式是让Play自动管理链接:
DB.withConnection { conn =>
  // do whatever you need with the connection
}


    该链接將会在代码块结束后自动关闭。
    提示:每一个被该链接建立的Statement and ResultSet也都会被关闭。

    一个变种方式是將auto-commit设为false,并在代码块中管理事务:
DB.withTransaction { conn =>
  // do whatever you need with the connection
}

    Anorm, 简单的SQL数据访问层


    Play包括了一个轻量的数据访问层,它使用旧的SQL与数据库交互,并提供了一个API解析转换数据结果集。

    Anorm不是一个ORM工具


    接下来的文档中,咱们將使用MySQL作为示例数据库。
    若是你想使用它,依照MySQL网站的介绍,并將下列代码加入conf/application.conf中:
db.default.driver= com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://localhost/world"
db.default.user=root
db.default.password=secret


    概述


    现现在,退回到使用纯SQL访问数据库会让人感受很奇怪,特别是对于那些习惯使用如Hibernate这类彻底隐藏底层细节的ORM工具的Java开发者。
   
    尽管咱们赞成这类工具使用Java开发几乎是必须的,但咱们也认为借助像Scala这类强大的高阶语言,它就不那么迫切了。相反,ORM工具可能会拔苗助长。

    使用JDBC是痛苦的,但咱们提供了更友好的API

    咱们赞成使用纯JDBC很糟糕,特别是在Java中。你不得不四到处理异常检查,一遍又一遍的迭代ResultSet来將原始的行数据转成自定义结构。

    咱们提供了比JDBC更简单的API;使用Scala,你再也不四处与异常为伴,函数式特性使得转换数据也异常简单。事实上,Play Scala SQL层的目标就是提供一些將JDBC数据转换成
    Scala结构的API。

    你不须要另外一种DSL语言访问关系数据库

    SQL已是访问关系数据库的最佳DSL。咱们毋需自做聪明的搞发明创造。此外,SQL的语法和特性也使得不一样的数据库厂商间存在差别。

    若是你试图使用某种类SQL的DSL去抽象这些差别,那么你不得不提供多个针对不一样数据库的代理(如Hibernate),这会限制你充分发挥特定数据库特性的能力。

   Play 有时候会提供预编译SQL statement, 但并非想隐藏SQL的底层细节. Play 只想节约大段查询的打字时间,你彻底能够返回来使用纯的SQL.mysql


    经过类型安全的DSL生成SQL是错误的


    存在一些争论的观点,认为使用类型安全的DSL更好,理由是你的查询可被编译器检查。不幸的是,编译器是基于你定义的元素据检查的,你一般都会本身编写自定义数据结构到数据库
    数据的“映射”。

    这种元素据正确性没法保证。即便编译器告诉你代码和查询是类型正确的,运行时依然会由于实际的数据库定义不匹配而惨遭失败。

    全权掌控你的SQL代码

    ORM工具在有限的用例中工具得很好,但当你须要处理复杂的数据库定义或已存在的数据库时,你將花费大量时间使得ORM工具为你产生正确的SQL代码。编写SQL查询你可能会认为像开发
    个“Hello World“那么乏味无趣,但任何真实的应用,你终將会经过彻底的掌控SQL和编写简单的代码而节省时间。

    执行SQL查询


    你將经过学习怎样执行SQL查询起步。

    首先导入 anorm._,而后使用简单的SQL对象建立查询。你须要一个链接来运行查询,你能够经过play.api.db.DB取得链接:
import anorm._ 

DB.withConnection { implicit c =>
  val result: Boolean = SQL("Select 1").execute()    
}


    execute方法返回一个Boolean值标识查询是否成功。

    为了执行更新,使用executeUpdate()方法,它將返回被更新的行数。
val result: Int = SQL("delete from City where id = 99").executeUpdate()


    既然Scala支持多行字符串形式,你能够自由的编写复杂的SQL块:
val sqlQuery = SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = 'FRA';
  """
)


    若是你的SQL查询须要动态参数,你能够在sql串中使用形如 {name}的声明,稍后给它赋值:
SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = {countryCode};
  """
).on("countryCode" -> "FRA")


    使用Stream API检索数据


    访问select查询结果的第一种方式是使用 Stream API。
   
    当你在任何SQL结果集中调用 apply() 方法时,你將会得到一个懒加载的 Stream 或 Row 实例,它的每一行能像字典同样的查看:
// Create an SQL query
val selectCountries = SQL("Select * from Country")
 
// Transform the resulting Stream[Row] as a List[(String,String)]
val countries = selectCountries().map(row => 
  row[String]("code") -> row[String]("name")
).toList


    接下来的例子,咱们將计算数据库中Country实体的数量,所以结果將是单行单列的:
   
// First retrieve the first row
val firstRow = SQL("Select count(*) as c from Country").apply().head
 
// Next get the content of the 'c' column as Long
val countryCount = firstRow[Long]("c")


    使用模式匹配


    你也可使用模式匹配来匹配和提取 Row 内容。这种状况下,列名已可有可无。仅仅使用顺序和参数类型来匹配。

    下面的例子將每行数据转换成正确的Scala类型:
case class SmallCountry(name:String) 
case class BigCountry(name:String) 
case class France
 
val countries = SQL("Select name,population from Country")().collect {
  case Row("France", _) => France()
  case Row(name:String, pop:Int) if(pop > 1000000) => BigCountry(name)
  case Row(name:String, _) => SmallCountry(name)      
}


    注意,既然 collect(...) 会忽略未定义函数,那它就容许你的代码安全的那些你不指望的行.web


    处理 Nullable 列

    若是在数据库定义的列中能够包含 Null 值,你须要以Option类型操纵它。

    例如,Country表的indepYear列可为空,那你就须要以Option[Int]匹配它:
SQL("Select name,indepYear from Country")().collect {
  case Row(name:String, Some(year:Int)) => name -> year
}


    若是你试图以Int匹配该列,它將不能正解的解析 Null 的状况。假设你想直接从结果集中以Int取出列的内容:
SQL("Select name,indepYear from Country")().map { row =>
  row[String]("name") -> row[Int]("indepYear")
}


    若是遇到Null值,將致使一个UnexpectedNullableFound(COUNTRY.INDEPYEAR)异常,所以你须要正确的映射成Option[Int]:
SQL("Select name,indepYear from Country")().map { row =>
  row[String]("name") -> row[Option[Int]]("indepYear")
}

    对于parser API也是一样的状况,接下来会看到。

    使用 Parser API


    你可使用 parser api来建立通用解析器,用于解析任意select查询的返回结果。

    注意:大多数web应用都返回类似数据集,因此它很是有用。例如,若是你定义了一个能从结果集中解析出Country的Parser 和 另外一个 Language Parser,你就经过他们的组合从链接查询中解析出Country和Language。

    得首先导入 anorm.SqlParser._

    首先,你须要一个RowParser,如一个能將一行数据解析成一个Scala对象的parser。例如咱们能够定义將结果集中的单列解析成Scala Long类型的parser:
val rowParser = scalar[Long]

    接着咱们必须转成ResultSetParser。下面咱们將建立parser,解析单行数据:
val rsParser = scalar[Long].single

    所以,该parser將解析某结果集,并返回Long。这对于解析 select count 查询返回的结果颇有用:
val count: Long = SQL("select count(*) from Country").as(scalar[Long].single)

    让咱们编写一个更复杂的parser:
    str("name")~int("population"),將建立一个能解析包含 String name 列和Integer population列的parser。再接咱们能够建立一个ResultSetParser, 它使用 * 来尽可能多的解析这种类型的行:

    正如你所见,该结果类型是List[String~Int] - country 名称和 population 项的集合。
val populations:List[String~Int] = {
  SQL("select * from Country").as( str("name") ~ int("population") * ) 
}


    你也能够这样重写例子:
val result:List[String~Int] = {
  SQL("select * from Country").as(get[String]("name")~get[Int]("population")*) 
}

    那么,关于String~Int类型呢?它一个 Anorm 类型,不能在你的数据访问层外使用.
    你可能想用一个简单的 tuple (String, Int) 替代。你调用RowParser的map函数將结果集转换成更通用的类型:  
str("name") ~ int("population") map { case n~p => (n,p) }
    注意:咱们在这里建立了一个 tuple (String,Int),但没人能阻止你RowParser转成其它的类型,例如自定义case class。


    如今,鉴于將 A~B~C 类型转成 (A,B,C)是个常见的任务,咱们提供了一个flatten函数帮你准备的完成。所以最终版本为:
   
val result:List[(String,Int)] = {
  SQL("select * from Country").as(
    str("name") ~ int("population") map(flatten) *
  ) 
}

    接下来,让咱们建立一个更复杂的例子。怎样建立下面的查询, 使得能够获取国家名和全部的国所使用的语言记录呢?
select c.name, l.language from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = 'FRA'


    Letʼs start by parsing all rows as a List[(String,String)] (a list of name,language tuple):
    让咱们先开始 以一个List[(String,String)](a list of name,language tuple)解析全部的行:
var p: ResultSetParser[List[(String,String)] = {
  str("name") ~ str("language") map(flatten) *
}

    如今咱们获得如下类型的结果:
List(
  ("France", "Arabic"), 
  ("France", "French"), 
  ("France", "Italian"), 
  ("France", "Portuguese"), 
  ("France", "Spanish"), 
  ("France", "Turkish")
)


    咱们接下来能够用 Scala collection API,將他转成指望的结果:
case class SpokenLanguages(country:String, languages:Seq[String])

languages.headOption.map { f =>
  SpokenLanguages(f._1, languages.map(_._2))
}

    最后,咱们获得了下面这个适用的函数:
case class SpokenLanguages(country:String, languages:Seq[String])

def spokenLanguages(countryCode: String): Option[SpokenLanguages] = {
  val languages: List[(String, String)] = SQL(
    """
      select c.name, l.language from Country c 
      join CountryLanguage l on l.CountryCode = c.Code 
      where c.code = {code};
    """
  )
  .on("code" -> countryCode)
  .as(str("name") ~ str("language") map(flatten) *)

  languages.headOption.map { f =>
    SpokenLanguages(f._1, languages.map(_._2))
  }
}

    To continue, letʼs complicate our example to separate the official language from the others:
    为了继续,咱们复杂化咱们的例子,使得能够区分官方语言:
case class SpokenLanguages(
  country:String, 
  officialLanguage: Option[String], 
  otherLanguages:Seq[String]
)

def spokenLanguages(countryCode: String): Option[SpokenLanguages] = {
  val languages: List[(String, String, Boolean)] = SQL(
    """
      select * from Country c 
      join CountryLanguage l on l.CountryCode = c.Code 
      where c.code = {code};
    """
  )
  .on("code" -> countryCode)
  .as {
    str("name") ~ str("language") ~ str("isOfficial") map {
      case n~l~"T" => (n,l,true)
      case n~l~"F" => (n,l,false)
    } *
  }

  languages.headOption.map { f =>
    SpokenLanguages(
      f._1, 
      languages.find(_._3).map(_._2),
      languages.filterNot(_._3).map(_._2)
    )
  }
}

    若是你在world sample数据库尝试该例子,你將得到:
$ spokenLanguages("FRA")
> Some(
    SpokenLanguages(France,Some(French),List(
        Arabic, Italian, Portuguese, Spanish, Turkish
    ))
)


    集成其它数据库访问层


    你也能够在Play中使用任何你喜欢的SQL数据库访问层,而且也能够借助 play.api.db.DB 很容易的取得链接或数据源.

    与ScalaQuery集成


    从这里开始,你能够集成任何的JDBC访问层,须要一个数据源。例如与ScalaQuery集成:
import play.api.db._
import play.api.Play.current

import org.scalaquery.ql._
import org.scalaquery.ql.TypeMapper._
import org.scalaquery.ql.extended.{ExtendedTable => Table}

import org.scalaquery.ql.extended.H2Driver.Implicit._ 

import org.scalaquery.session._

object Task extends Table[(Long, String, Date, Boolean)]("tasks") {
    
  lazy val database = Database.forDataSource(DB.getDataSource())
  
  def id = column[Long]("id", O PrimaryKey, O AutoInc)
  def name = column[String]("name", O NotNull)
  def dueDate = column[Date]("due_date")
  def done = column[Boolean]("done")
  def * = id ~ name ~ dueDate ~ done
  
  def findAll = database.withSession { implicit db:Session =>
      (for(t <- this) yield t.id ~ t.name).list
  }
  
}


    从JNDI查找数据源:
    一些库但愿从JNDI中获取数据源。经过在conf/application.conf添加如下配置,你可让Play管理任何的JNDI数据源:
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
db.default.jndiName=DefaultDS
相关文章
相关标签/搜索