Play2 for Scala中文文档 – 10. 使用anorm操作数据库

Play2 for Scala中文文档 – 10. 使用anorm操作数据库

时间 2013-01-01 17:03:09Freewind.me

原文http://freewind.me/blog/20130101/1284.html

主题SQLScala

(本文译者是爱国者)

Anorm, simple SQL data access

Play包含一个叫Anorm的数据库存取层。该数据库存取层使用原生SQL与数据库交互,并提供了一组API来解析和转换查询结果集.

Anorm不是一个对象关系映射框架

我们在接下来的例子中使用MySQL官方提供的样本数据库([[ world sample database ||http://dev.mysql.com/doc/world-setup/en/world-setup.html]]) 。请参照这个[[ 文档 | ScalaDatabase ]] 配置好数据源..

概述

改用原生SQL操作数据库可能会让人觉得很奇怪,尤其习惯于ORM框架的java开发者. 虽然我们承认在Java中使用这些工具几乎是有必要的,但我们也认为使用像Scala这种表达能力很强的高级编程语言开发的应用完全没有必要使用这些ORM工具, 使用了反而会产生反效果.

提供更好的API减轻使用JDBC的痛苦

我们同意,尤其在Java中,直接使用JDBC编程是一件很乏味的事, 因为你不得不处理各种受检查异常(checked exception), 不得不反复将原始结果集转换成你需要的数据结构。

我们为JDBC提供了一套简单易用的API。如果你使用Scala语言编写应用,你无需为各种异常处理而烦恼, 使用函数式语言转换数据是十分简单的。事实上,我们希望通过提供一组API让JDBC和Scala之间的数据结构转换变得更有效率。

你不需要另一种DSL操作关系型数据库

SQL是操作数据库最好的领域特定语言(Domain Specific Language, DSL)。 我们不需要发明一种新的DSL, 而且SQL的语法和数据库特性在不同的供应商之间存在差异。如果你尝试抽象出一种新的DSL,你不得不像Hibernate那样处理几种数据库的方言, 而且使用一些有趣的特性会受到限制。

Play提供了一些预定义的SQL语句,但目的不是为了隐藏某些操作细节, 只是为一些常用的查询减少编码量。而且你总是可以改回原生SQL.

使用类型安全的DSL生成SQL是错误的

有些观点认为,使用类型安全的DSL是更好的做法,因为所有的查询都可以经过编译器检查。不幸的是,编译器只基于你编写的对象关系映射的元数据模型定义来检验你的查询。 元数据模型无法保证是正确的。即使查询代码被编译器判定为类型正确,但仍有可能由于不匹配的关系对象映射而导致运行时出错。

掌控SQL代码

对象关系映射在一般情况下会工作得很好, 但当要处理复杂的schemas或者遗留的数据库,你就得在ORM框架上花大量时间生成你需要的SQL。为Hello world这种简单应用编写原生的SQL查询也许会很乏味,但对于实际应用来说,最终会节省时间并简化代码.

执行SQL查询

开始之前,你需要知道如何执行SQL查询。

首先,导入anorm._,然后使用SQL对象创建查询。你需要一个Connection对象运行查询。Connection对象可以从play.api.db.DB.withConnection函数取得

importanorm._DB.withConnection {implicitc =>valresult:Boolean=SQL("Select 1").execute()}

execute()函数返回一个表示查询是否成功的布尔值。

如果要执行更新查询,使用executeUpdate()函数, 这个函数会返回记录被更新的个数

val result: Int = SQL("deletefromCitywhereid=99").executeUpdate()

如果向一个拥有Long类型的自增主键的表插入数据,使用executeInsert().如果该表有联合主键或主键不是Long类型,那么可以向executeInsert()传入一个ResultSetParser返回正确的主键。

val id: Int = SQL("insertintoCity(name, country)values({name}, {country}")

.on("Cambridge", "NewZealand").executeInsert()

由于Scala支持多行文本的写法,因此可以将复杂的SQL表达式写成:

val sqlQuery = SQL("""

select * from Country c

join CountryLanguage l on l.CountryCode = c.Code

where c.code = 'FRA';

""")

如果SQL查询包含动态参数,可以使用占位符表示,如{name}, 然后在使用on()函数赋值.

SQL("""

select * from Country c

join CountryLanguage l on l.CountryCode = c.Code

where c.code = {countryCode};

""").on("countryCode"->"FRA")

使用Stream API获取数据

获取查询结果的第一种方法是使用Stream API. 调用SQL表达式对象上的apply()方法会返回一个Stream集合,Stream集合中的每个元素代表一个Row实例,每个Row实例可以看成一个映射表(dictionary)

// Create an SQL queryval selectCountries = SQL("Select * from Country")// Transform the resulting Stream[Row] to a List[(String,String)]val countries = selectCountries().map(row =>  row[String]("code") -> row[String]("name")).toList

在下面的例子中,我们统计一下Country表共有有多少条记录, 因此查询结果只会返回包含一个字段的一条记录.

// First retrieve the first rowval firstRow = SQL("Select count(*) as c from Country").apply().head// Next get the content of the 'c' column as Longval countryCount = firstRow[Long]("c")

使用模式匹配

可以使用模式匹配的方法从Row抽取出所需的内容。在这种情况下,字段名不再重要,只要求顺序和参数类型匹配即可。

下面的例子使用模式匹配的方法将每条记录匹配到合适的Scala类型中:

caseclassSmallCountry(name:String)caseclassBigCountry(name:String)caseclassFranceval countries = SQL("Select name,population from Country")().collect {caseRow("France", _) => France()caseRow(name:String, pop:Int)if(pop >1000000) => BigCountry(name)caseRow(name:String, _) => SmallCountry(name)}

由于collect(_)会忽略偏应用函数没有定义的情况, 因此你可以忽略某些记录。

特殊数据类型

Clob类型

可以这样抽取CLOBs/TEXTs类型的值:

SQL("Select name,summary from Country")().map {caseRow(name:String, summary: java.sql.Clob) => name -> summary}

这里我们特地使用map函数,使得当出现我们非预期的记录时抛出异常。

二进制类型

抽取二进制类型的值也是类似的做法:

SQL("Select name,image from Country")().map {caseRow(name:String, image:Array[Byte]) => name -> image}

数据库互操作(Database interoperability)

对于记录的某个字段,不同的数据库引擎会返回不同的数据类型. 比如一个‘smallint’类型字段, org.h2.Driver会返回一个short,org.postgresql.Driver会返回一个Integer. 一个简单的解决办法是为每种数据库编写特定的版本。(比如一个版本用于开发数据库,一个版本用于生产数据库)

处理空值(null)的列

如果列含有空值(Null),那么最好处理成Option类型。 例如Country表的indepYear列允许空值,那么使用Option[Int]匹配该列:

SQL("Select name,indepYear from Country")().collect {caseRow(name:String, Some(year:Int)) => name ->year}

或者用这种写法:

SQL("Select name,indepYear from Country")().map { row =>  row[String]("name") -> row[Option[Int]]("indepYear")}

如果将indepYear列直接匹配为Int, 那么当遇到null值会抛出UnexpectedNullableFound(COUNTRY.INDEPYEAR)异常:

SQL("Select name,indepYear from Country")().map { row =>  row[String]("name") -> row[Int]("indepYear")}

使用parser API

你可以使用parser API创建通用的可重用的解析器解析任意查询结果集。

备注:这种做法是有用的,因为大部分we应用的查询都返回相似的结果集。例如,你可以定义一个能用于解析Country表查询结果集的解析器和一个用于解析Language表查询结果集的解析器。然后在用到连接查询的地方将两个解析器组合起来使用。 记得导入anorm.SqlParser._

单一结果集

首先你需要一个RowParser将一条记录解析成一个Scala对象。下面定义一个将单一列的记录转换成Long值对象的记录解析器(row parser):

valrowParser = scalar[Long]// row parser that parse a record into scala Long

再将它转换成ResultSetParser(结果集解析器):

valrsParser = rowParser.single// resultset parser that transform the result set into an object.

然后可以使用rsParser将select count产生的结果集转换成Long值对象:

valcount:Long= SQL("select count(*) from Country").as(scalar[Long].single)

单一可选结果集

假如要根据国家名查找country_id, 但表中可能不存在该国家名(或输入了一个错误的国家名),这种情况下查询会返回一个空值。 这时应该使用singleOpt解析器:

valcountryId: Option[Long] = SQL("select country_id from Country C where C.country='France'").as(scalar[Long].singleOpt)

更复杂的结果集

下面我们编写一个更复杂的解析器. 首先,str("name") ~ int("population")会创建一个记录解析器。这个解析器能解析一条包含类型为String的name字段和类型为Integer的population字段的记录。然后在此基础上使用*创建结果集解析器(ResultSetParser),

val populations:List[String~Int] = {  SQL("select * from Country").as(str("name") ~int("population") * )}

这个查询结果集最终被转换成List[String~Int]类型 ———— 一个包含国家名和该国人口数量的列表。

你可以将上面的例子改成:

val result:List[String~Int] = {  Sql("select * from Country").as(get[String]("name")~get[Int]("population")*)}

可能大家已经注意到String~Int这个类型。这是一个Anorm类型,但用起来不会很方便, 相信改成(String, Int)这样的二元组会更好。你可以在RowParser上使用map函数将一条记录转换成合适的类型:

str("name") ~int("population")map{casen~p => (n,p) }

备注:这里我们将一条记录转换成一个二元组(String,Int),你可以换成你想要的类型,比如样本类(case class)

由于将A~B~C转换成(A,B,C)是很普遍的, Play提供了一个flatten函数实现同样的功能。上面的例子可以改成:

val result:List[(String,Int)] = {  SQL("select * from Country").as(str("name") ~int("population") map(flatten) *  )}

更复杂的例子

现在尝试更复杂的例子。如何根据国家代码查出国家名以及该国所使用的所有语言?

selectc.name, l.languagefromCountry cjoinCountryLanguage lonl.CountryCode = c.Codewherec.code ='FRA'

我们将查询结果集解析成List[(String,String)](一个包含name和language二元组的列表)

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))  }}

接下来,将官方语言和其他语言分开:

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)    )  }}

如果在Mysql world sample数据库上运行上述的查询,会得到:

$ spokenLanguages("FRA")>Some(    SpokenLanguages(France,Some(French),List(        Arabic, Italian, Portuguese, Spanish, Turkish    )))

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容