Facebook Twitter LinkedIn E-mail
magnify
Home 2014 七月

Slick 编程(9): 直接使用SQL语句

如果你有需要直接使用SQL语句,Slick也支持你直接使用SQL语句。
首先你需要引入一些引用包:

import scala.slick.jdbc.{GetResult, StaticQuery => Q}
import scala.slick.jdbc.JdbcBackend.Database
import Q.interpolation

其中最重要的一个相关类似StaticQuery ,为简洁起见,我们使用Q作为它的别名。连接数据库还是和以前一样Slick 编程(4): 数据库连接和事务处理
DDL和DML语句
StaticQuery的方法updateNA,(NA代表无参数),它返回DDL指令影响的行数。,比如使用H2数据库

连接数据库:

case class Supplier(id:Int, name:String, street:String, city:String, state:String, zip:String)
case class Coffee(name:String, supID:Int, price:Double, sales:Int, total:Int)

Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withDynSession {

}

创建数据库表:

// Create the tables, including primary and foreign keys
Q.updateNA("create table suppliers("+
  "id int not null primary key, "+
  "name varchar not null, "+
  "street varchar not null, "+
  "city varchar not null, "+
  "state varchar not null, "+
  "zip varchar not null)").execute
Q.updateNA("create table coffees("+
  "name varchar not null, "+
  "sup_id int not null, "+
  "price double not null, "+
  "sales int not null, "+
  "total int not null, "+
  "foreign key(sup_id) references suppliers(id))").execute

你可以使用字符串和一个StaticQuery 对象相加(+)构成一个新的StaticQuery 对象,一个简单的方法是使用Q.u 和一个字符串相加,Q.u代表一个相当与StaticQuery.updateNA(“”)

例如我们在表中插入一些数据:

// Insert some suppliers
(Q.u + "insert into suppliers values(101, 'Acme, Inc.', '99 Market Street', 'Groundsville', 'CA', '95199')").execute
(Q.u + "insert into suppliers values(49, 'Superior Coffee', '1 Party Place', 'Mendocino', 'CA', '95460')").execute
(Q.u + "insert into suppliers values(150, 'The High Ground', '100 Coffee Lane', 'Meadows', 'CA', '93966')").execute

在SQL查询语句中使用字面量不是一种推荐的方法,尤其是当用户提供数据时(不十分安全), 此时你可以使用 +? 操作符为查询语句绑定一个参数,比如:

def insert(c: Coffee) = (Q.u + "insert into coffees values (" +? c.name +
  "," +? c.supID + "," +? c.price + "," +? c.sales + "," +? c.total + ")").execute

// Insert some coffees
Seq(
  Coffee("Colombian", 101, 7.99, 0, 0),
  Coffee("French_Roast", 49, 8.99, 0, 0),
  Coffee("Espresso", 150, 9.99, 0, 0),
  Coffee("Colombian_Decaf", 101, 8.99, 0, 0),
  Coffee("French_Roast_Decaf", 49, 9.99, 0, 0)
).foreach(insert)

这段代码相对于 insert into coffees values (?,?,?,?,?)

查询语句
和updateNA类似, StaticQuery还有一个queryNA方法,它支持一个类型参数(代表表的一行),比如:

Q.queryNA[AlbumRow]("select * from Album") foreach { a => 
		println(" " + a.albumid + " " + a.title + " " + a.artistid)
}

这段代码之所以能工作,是因为Tables.scala中定义了

 /** GetResult implicit for fetching AlbumRow objects using plain SQL queries */
  implicit def GetResultAlbumRow(implicit e0: GR[Int], e1: GR[String]): GR[AlbumRow] = GR{
    prs => import prs._
    AlbumRow.tupled((<<[Int], <<[String], <<[Int]))
  }

定义了从JDBC类型到GetResult[T] 的隐含转换,GetResult[T] 为函数PositionedResult => T的一个封装。<<[T] 返回指定位置上期望的值。 和queryNA对应的带参数的query定义了两个类型参数,一个是参数的类型,另外一个是返回的结果的每行的类型,例如: [sourcecode lang="scala"] val q2 = Q.query[Int,(Int,String,Int)] ( """ select albumid,title,artistid from Album where artistid < ? """) val l2 = q2.list(10) for(t <- l2) println( " " + t._1 + " " + t._2 + " " + t._3) [/sourcecode] 返回结果如下: [sourcecode lang="scala"] 1 For Those About To Rock We Salute You 1 4 Let There Be Rock 1 2 Balls to the Wall 2 3 Restless and Wild 2 5 Big Ones 3 6 Jagged Little Pill 4 7 Facelift 5 8 Warner 25 Anos 6 34 Chill: Brazil (Disc 2) 6 9 Plays Metallica By Four Cellos 7 10 Audioslave 8 11 Out Of Exile 8 271 Revelations 8 12 BackBeat Soundtrack 9 [/sourcecode] Q.interpolation 支持字符串插值,比如: [sourcecode lang="scala"] def albumByTitle(title: String) = sql"select * from Album where title = $title".as[AlbumRow] println("Album: " + albumByTitle("Let There Be Rock").firstOption) [/sourcecode] 使用sql 做为前缀的字符串,可以将以$开始的变量替换成该变量的值,此外对于update/delete语句,可以使用sqlu前缀

 

Slick 编程(8): 查询(三)

Slick的查询实际上是执行由Invoker(无参数时为UnitInvoker)Trait定义的方法, Slick定义了一个从Query隐含的变换,使得你可以直接执行查询操作,最常用的一个情况是把整个查询结果存放到一个Scala集合类型中(比如使用list方法)

val l = q.list
val v = q.buildColl[Vector]
val invoker = q.invoker
val statement = q.selectStatement

所有的查询方法都定义了一个隐含参数Session,如果你愿意,你也可以直接传入一个session参数:

val l = q.list()(session)

如果你只需要单个查询结果,你可以使用first或firstOption方法,而方法foreach, foldLeft,和elements方法可以用来遍历查询结果而不需要先把结果复制到另外一个Scala集合对象中。

Deleting
删除数据和查询很类似,你首先写一个选择查询,然后调用它的delete方法,同样Slick也定义一个从Query到DeleteInvoker的隐含转换,DeleteInvoker定义了delete方法

val affectedRowsCount = q.delete
val invoker = q.deleteInvoker
val statement = q.deleteStatement

定义用来删除记录的查询时只能使用单个表格。

Inserting
插入操作基于单个表定义的字段映射,当你直接使用某个表来插入数据时,这个操作基于表类型中定义的“*”,如果你省略某些字段,那么插入这些省略的字段会使用缺省值,所有的插入操作方法定义在InsertInvoker和FullInsertInvoker。

coffees += ("Colombian", 101, 7.99, 0, 0)

coffees ++= Seq(
  ("French_Roast", 49, 8.99, 0, 0),
  ("Espresso",    150, 9.99, 0, 0)
)

// "sales" and "total" will use the default value 0:
coffees.map(c => (c.name, c.supID, c.price)) += ("Colombian_Decaf", 101, 8.99)

val statement = coffees.insertStatement
val invoker = coffees.insertInvoker

// compiles to SQL:
//   INSERT INTO "COFFEES" ("COF_NAME","SUP_ID","PRICE","SALES","TOTAL") VALUES (?,?,?,?,?)

如果你的插入操作定义了自动增一的字段,该字段会自动忽略,由数据库本身来插入该字段的值。缺省情况+=返回受影响的行数(通常总为1),而++操作给出总计的行数(以Option类型给出),你可以使用returning修改返回的值,比如返回插入的行的主键:

val userId =
  (users returning users.map(_.id)) += User(None, "Stefan", "Zeiger")

要注意的是很多数据库只支持返回自动增一的作为主键的那个字段,如果想返回其它字段,可能会抛出SlickException 异常。

除了上面的插入记录的方法,还可以使用服务器端表达式的方发插入数据:

class Users2(tag: Tag) extends Table[(Int, String)](tag, "users2") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def * = (id, name)
}
val users2 = TableQuery[Users2]

users2.ddl.create

users2 insert (users.map { u => (u.id, u.first ++ " " ++ u.last) })

users2 insertExpr (users.length + 1, "admin")

Updating
更新记录也是先写查询,然后调用update方法,比如:

val q = for { c <- coffees if c.name === "Espresso" } yield c.price
q.update(10.49)

val statement = q.updateStatement
val invoker = q.updateInvoker

update方法定义在UpdateInvoker Trait中。

Compiled Queries
数据库查询时,通常需要定义一些查询参数,比如根据ID查找对应的记录。你可以定义一个带参数的函数来定义查询对象,但每次调用该函数时都要重新编译这个查询语句,系统消耗有些大,Slick支持预编译这个带参数的查询函数,例如:

def userNameByIDRange(min: Column[Int], max: Column[Int]) =
  for {
    u <- users if u.id >= min && u.id < max
  } yield u.first

val userNameByIDRangeCompiled = Compiled(userNameByIDRange _)

// The query will be compiled only once:
val names1 = userNameByIDRangeCompiled(2, 5).run
val names2 = userNameByIDRangeCompiled(1, 3).run

这种方法支持查询,更新和删除数据。

 

Slick 编程(7): 查询(二)

Union
两个查询的结果可以通过 ++ (或者 unionAll) 和union 操作联合起来:

val q1= Album.filter(_.artistid <10)
val q2 = Album.filter(_.artistid > 15)
val unionQuery  = q1 union q2
val unionAllQuery = q1 ++ q2

union操作会去掉重复的结果,而unionAll 只是简单的把两个查询结果连接起来(通常来说比较高效)。

Aggregation
和SQL一样,Slick也有 min, max ,sum, avg等集合操作

val q = Album.map(_.artistid)
val q1 = q.max
val q2 = q.min 
val q3 = q.avg 
val q4 = q.sum

注意:这里q.max ,min,avg,sum返回结果类型为Column[Option[T]],要得到最好的scalar类型的值T,可以调用run,得到Option[T],然后再调用Option的get或getOrDefault,
比如:

val q = Album.map(_.artistid)
val q1 = q.max 
println(q1.run.get)

得到打印的结果:
275

其它的Aggregation操作还有 length, exists ,比如:

val q1 = Album.length
val q2 = Album.exists

分组使用groupBy操作,类似于Scala集合类型的groupBy操作:

val q= (for {
	 a <- Album
	 b <- Artist
	 if a.artistid === b.artistid
   } yield (b.artistid,b.name)
).groupBy(_._2)
val q1 = q.map { case (name, records) =>
		(records.map(_._1).avg, name,records.length)}
q1 foreach println 

这段代码使用两个查询,给出Album根据艺术家出的专辑的统计,其中中间查询q,包含一个嵌套的Query,目前Scala不支持直接查询嵌套的Query,因此我们需要分两次查询,打印出的部分结果如下:

(Some(230),Some(Aaron Copland & London Symphony Orchestra),1)
(Some(202),Some(Aaron Goldberg),1)
(Some(1),Some(AC/DC),2)
(Some(214),Some(Academy of St. Martin in the Fields & Sir Neville Marriner),1)
(Some(215),Some(Academy of St. Martin in the Fields Chamber Ensemble & Sir Neville Marriner),1)
(Some(222),Some(Academy of St. Martin in the Fields, John Birch, Sir Neville Marriner & Sylvia McNair),1)
(Some(257),Some(Academy of St. Martin in the Fields, Sir Neville Marriner & Thurston Dart),1)
(Some(2),Some(Accept),2)
(Some(260),Some(Adrian Leaper & Doreen de Feis),1)
(Some(3),Some(Aerosmith),1)
(Some(197),Some(Aisha Duo),1)
(Some(4),Some(Alanis Morissette),1)
(Some(206),Some(Alberto Turco & Nova Schola Gregoriana),1)
(Some(5),Some(Alice In Chains),1)
(Some(252),Some(Amy Winehouse),2)
...
 

Akka 编程(4): Actor管理和监视

本篇介绍Actor管理相关的一些基本概念
管理意味着什么
我们在前面介绍了Actor系统,描述了Actor之间存在的依赖关系,管理员Actor把任务托管给其下属Actor,因此必须在其从属Actor出错时作出反应。 但一个下属Actor检测到错误发生时(比如抛出异常),它将暂停其运行,同时也停止它的下属Actor的运行,然后给其管理员Actor发送信息表明其运行中出现了错误。 根据托管的任务以及错误的类型,管理员可以有如下四种选择来应对:

  • 恢复其下属Actor的运行,并保持其当前的运行状态。
  • 重启其下属Actor的运行,恢复其状态为初始状态。
  • 终止其下属Actor的运行。
  • 向上传递这个错误,从而自己报错,由其上级管理员来应对。

每个Actor都是整个actor系统层次中的一环,这也解释了第四个选项的来历,前三种选项暗示了:恢复运行Actor,同时也恢复它所有子actor的运行。重启一个Actor,也表示重启其子Actor。同样终止一个Actor,同时也终止了其子Actor的运行。
需要注意的是,Actor的preRestart的缺省实现是在重启Actor之前,终止其所有子Actor的运行,这个确实行为是可以修改的。
每个管理员可以使用一个函数来配置,将所有可能出现的错误(比如异常)转换为上面四种选择。值得注意的是这个函数不使用出错Actor的标识符作为输入参数。
Akka的实现采用了一种称为“父亲管理机制”的模式,也就是说,一个Actor只能由其它Actor来创建,最根一级的Actor由Akka库提供,每个Actor由其父Actor来管理,这也意味着actor不可能成为“孤儿”,或者由Actor系统外部创建然后添加到Actor系统中。
注:这种父子Actor之间的通信使用了特定的系统消息机制来实现(也就是说使用其专用的Mailbox,和User Mailbox不同)。

根Actor管理员
每个Actor系统在启动时,至少会启动三个根管理员,如下图所示:
20140726001

  • /user: The Guardian Actor 这是最常用的一个跟Actor,它是所有用户创建的Actor的父Actor。 所有由system.actorOf()创建的Actor都是该根Actor的子Actor。这也意味着当该Actor终止时,所有用户Actor都将终止。
  • /system: The System Guardian 这是个系统根Actor,用来实现正确的系统终止顺序。
  • /: The Root Guardian 这是最上级管理员

 

重启意味着什么
当一个Actor在处理消息时出现错误,造成错误的原因可以分成如下三种类型:

  • 收到某种类型消息时造成系统错误(程序出错)
  • 在处理消息是由于外部资源造成的暂时性错误
  • Actor本身内部状态遭到破坏

除非该错误是可识别的,第三种错误是不可解决的,这也意味着Actor的内部状态必须清除。 如果管理员判断出这个错误不影响其它子Actor和其本身的运行,因此可以做出重启该子Actor的决定。这是通过创建一个新的Actor的实例来替代出错的那个子Actor,这个新的Actor恢复处理邮箱中的消息,这也就意味着从外部看不出系统出现了错误。
重启时的具体步骤如下:

  1. 暂停该Actor(意味着它在恢复运行前不再处理正常的消息),同时递归暂停其所有子Actor的运行。
  2. 调用原来Actor对象的preRestart回调函数(缺省向其子Actor发送终止消息,并调用它们的postStop回调函数)
  3. 在preRestart中等待所有要求终止的子Actor的回应(使用context.stop),这是个非阻塞操作,最后一个子Actor的终止通知会直接影响是否进行下一步
  4. 调用原先的Factory方法创建一个新的actor的实例。
  5. 调用新的Actor的对象的postRestart回调函数(其缺省也调用preStart方法)
  6. 向所有在第三步中没有被杀死的子Actor发送重启请求,重启的子Actor也会使用同样的步骤(从步骤2开始)
  7. 恢复Actor的运行

监视Actor生命周期意味着什么
除了前面所说的actor之间的父子关系之外,每个Actor也可以去监视其它的Actor对象。对于不是管理员来说,不是所有的Actor生命周期都可以被监视,对于外部Actor来说,只有Actor从Live到Dead的状态变化是可以被监视的,因此这种生命周期监视也称为DeathWatch。
因此一个Actor可以对另外一个Actor终止消息做出回应,这是通过发送Terminated消息来实现的,其缺省行为是抛出一个特殊的异常DeathPactException。为了能够监听Terminated消息,可以调用ActorContext.watch(targetActorRef)。 取消监听,可以调用ActorContext.unwatch(targetActorRef).值得注意的是,接受到这个消息的顺序和其监视的Actor终止事件发生不相关,也就是当你接受到Terminated消息时,你监视的Actor可能早就终止了。

一对一和所有对一的策略对比
Akka自带了两种管理策略,OneForOneStrategy和AllForOneStrategy.两种策略都可以把Actor出错匹配到之前说到的四种处理方法并给出在重启子Actor之前允许子Actor出错的频度。 所不同的是,其中只针对出错的那个Actor使用预设的错误处理方法,而后者则针对所有的子Actor。通常情况下,你应该使用OneForOneStrategy。

 

Slick 编程(6): 查询(一)

本篇介绍Slick的基本查询,比如选择,插入,更新,删除记录等。
排序和过滤
Slick提供了多种方法可以用来排序和过滤,比如:

val q = Album.filter(_.albumid === 101)

//select `AlbumId`, `Title`, `ArtistId` 
//from `Album` where `AlbumId` = 101


val q = Album.drop(10).take(5)
//select .`AlbumId` as `AlbumId`, .`Title` as `Title`,
// .`ArtistId` as `ArtistId` from `Album`  limit 10,5


val q = Album.sortBy(_.title.desc)
//select `AlbumId`, `Title`, `ArtistId` 
//from `Album` order by `Title` desc

Join和Zipping
Join指多表查询,可以有两种不同的方法来实现多表查询,一种是通过明确调用支持多表连接的方法(比如innerJoin方法)返回一个多元组,另外一种为隐含连接(implicit join),它不直接使用这些连接方法(比如LeftJoin方法)。
一个隐含的cross-Join 为Query的flatMap操作(在for表达式中使用多个生成式),例如:

val q = for{a <- Album
			b <- Artist
		} yield( a.title, b.name)

//select x2.`Title`, x3.`Name` from `Album` x2, `Artist` x3

如果添加一个条件过滤表达式,它就变成隐含的inner join,例如:

val q = for{a <- Album
			b <- Artist
		    if a.artistid === b.artistid
		} yield( a.title, b.name)

//select x2.`Title`, x3.`Name` from `Album` x2, `Artist` x3 
//where x2.`ArtistId` = x3.`ArtistId`

明确的多表连接则使用innerJoin , leftJoin ,rightJoin,outerJoin 方法,例如:

val explicitCrossJoin = = for {
			 (a,b) <- Album innerJoin Artist  
			 } yield( a.title, b.name)


//select x2.x3, x4.x5 from (select x6.`Title` as x3 from `Album` x6) 
//x2 inner join (select x7.`Name` as x5 from `Artist` x7) x4 on 1=1


 val explicitInnerJoin  = for {
		 (a,b) <- Album innerJoin Artist on (_.artistid === _.artistid)
		 } yield( a.title, b.name)
//select x2.x3, x4.x5 from (select x6.`Title` as x3, x6.`ArtistId` as x7 from `Album` x6) x2 
//inner join (select x8.`ArtistId` as x9, x8.`Name` as x5 from `Artist` x8) x4 on x2.x7 = x4.x9


val explicitLeftOuterJoin   = for {
		 (a,b) <- Album leftJoin Artist on (_.artistid === _.artistid)
		 } yield( a.title, b.name.?)
//select x2.x3, x4.x5 from (select x6.`Title` as x3, x6.`ArtistId` as x7 from `Album` x6) x2 
//left outer join (select x8.`ArtistId` as x9, x8.`Name` as x5 from `Artist` x8) x4 on x2.x7 = x4.x9


val explicitRightOuterJoin   = for {
		 (a,b) <- Album rightJoin Artist on (_.artistid === _.artistid)
		 } yield( a.title.?, b.name)
//select x2.x3, x4.x5 from (select x6.`Title` as x3, x6.`ArtistId` as x7 from `Album` x6) x2 
//right outer join (select x8.`ArtistId` as x9, x8.`Name` as x5 from `Artist` x8) x4 on x2.x7 = x4.x9

注意leftJoin 和 rightJoin中的 b.name.?和 a.title.? 的”.?” 这是因为外部查询时会产生额外的NULL值,你必须保证返回Option类型的值。
除了通常的InnerJoin ,LeftJoin,RightJoin之外,Scala还提供了Zip 方法,它的语法类似于Scala的集合类型,比如:

val zipJoinQuery  = for {
	   (a,b) <- Album zip Artist
	 } yield( a.title.?, b.name)

此外,还有一个zipWithIndex,可以把一个表的行和一个从0开始的整数序列Zip操作,相当于给行添加序号,比如

val zipWithIndexJoin  = for {
	   (a,idx) <- Album.zipWithIndex 
	 } yield( a.title, idx)
 

Play Framework Web开发教程(11):第一个应用(七):产品详细信息页面

前面我们显示了产品列表页面,对于单个产品,我们再设计一个详细信息页面,并可以显示产品条码。
生成条码图像
生成条码的代码,我们使用一个第三方开发包,我们可以在project/build.scala 中添加对barcode4j的引用:

val appDependencies = Seq(
"net.sf.barcode4j" % "barcode4j" % "2.0"
)

最新的Play可能缺省不含有project/build.scala ,可以修改build.sbt

libraryDependencies ++= Seq(
  ...
  "net.sf.barcode4j" % "barcode4j" % "2.0"
) 

下面我们添加一个Barcodes控制器,定义两个函数,一个是ean13BarCode 用来帮助生成EAN 13 条码,返回一个PNG图像。 另外一个为barcode Action 它使用ean13BarCode 函数给HTTP请求返回图像。

package controllers
import play.api.mvc.{Action, Controller}
object Barcodes extends Controller {
  val ImageResolution = 144
  def barcode(ean: Long) = Action {
    val MimeType = "image/png"
    try {
      val imageData = ean13BarCode(ean, MimeType)
      Ok(imageData).as(MimeType)
    }
    catch {
      case e: IllegalArgumentException =>
        BadRequest("Couldn’t generate bar code. Error: " + e.getMessage)
    }
  }
  def ean13BarCode(ean: Long, mimeType: String): Array[Byte] = {
    import java.io.ByteArrayOutputStream
    import java.awt.image.BufferedImage
    import org.krysalis.barcode4j.output.bitmap.BitmapCanvasProvider
    import org.krysalis.barcode4j.impl.upcean.EAN13Bean
    val output: ByteArrayOutputStream = new ByteArrayOutputStream
    val canvas: BitmapCanvasProvider =
      new BitmapCanvasProvider(output, mimeType, ImageResolution,
        BufferedImage.TYPE_BYTE_BINARY, false, 0)
    val barcode = new EAN13Bean()
    barcode.generateBarcode(canvas, String valueOf ean)
    canvas.finish
    output.toByteArray
  }
}

然后在route添加一个路由配置

GET /barcode/:ean controllers.Barcodes.barcode(ean: Long)

在这个路由配置中,我们需要添加一个ean参数,在Play中,你可以在参数其添加一个“:”,然后这个参数在调用响应Action时,和作为参数传给Action,比如本例的barcode动作。
然后我们来测试一下:
在浏览器中输入 http://localhost:9000/barcode/5010255079763
20140725001

为DataModel 添加一个查找方法
我们在Product.scala中添加一个根据ean查找Product的方法:

def findByEan(ean: Long) = products.find(_.ean == ean)

定义产品详细页面模板
这个页面模板用来显示产品的详细信息,包括产品名称,描述,条码
我们定义这个模板在app/views/products/details.scala.html

@(product: Product)(implicit flash: Flash,lang: Lang)
@main(Messages("products.details", product.name)) {
    <h2>
        @tags.barcode(product.ean)
        @Messages("products.details", product.name)
    </h2>
    <dl class="dl-horizontal">
        <dt>@Messages("ean"):</dt>
        <dd>@product.ean</dd>
        <dt>@Messages("name"):</dt>
        <dd>@product.name</dd>
        <dt>@Messages("description"):</dt>
        <dd>@product.description</dd>
    </dl>
}

这个模板本身没有什么特别需要提的,只有其中我们定义了一个tag, 它其实是另外一个显示模板,用来显示条码的图像,
我们定义它在 app/views/tags/barcode.scala.html中

@(ean: Long)
<img class="barcode" alt="@ean" src="@routes.Barcodes.barcode(ean)">

定义本地化语言

...conf/messages
ean = EAN
name = Name
description = Description
products.details = Product: {0}

...conf/messages.es
ean = EAN
name = Nombre
description = Descripción
products.details = Producto: {0}

...conf/messages.fr
ean = EAN
name = Nom
description = Descriptif
products.details = Produit: {0}

...conf/messages.nl
ean = EAN
name = Naam
description = Omschrijving
products.details = Product: {0}

定义show 动作
show动作也需要一个参数ean

def show(ean: Long) = Action { implicit request =>
    Product.findByEan(ean).map { product =>
      Ok(views.html.products.details(product))
    }.getOrElse(NotFound)
  }

同样,我们需要定义一个路由

GET         /products/:ean        controllers.Products.show(ean: Long)

这样我们就完成了产品的详细显示页面
20140725002

注意:我们需要修改list.scala.html 为产品列表添加链接:

...
<dt>
	<a href="@controllers.routes.Products.show(product.ean)">
	@product.name
	</a>
</dt>
...