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
相關文章
相關標籤/搜索