上一次在使用paly时,使用的还是14年发布的2.2.x版本,现在Play已经发布到了2.6.x版本,使用方式发生了很大变化,API也有很大变化,并且在集成JPA上,网络上没有发现基于Scala2.6.x新版本的可用教程和说明,这里对这个版本的集成过程做了一个探索和梳理,也当是对play的一次回顾。

添加依赖

play采用的是sbt构建工具,因此需在sbt中加入JPA所需的依赖,打开项目根目录下的build.sbt文件,末尾加入以下配置:

libraryDependencies ++= Seq(
  javaJpa,  //jpa依赖
  ehcache,  //缓存管理,可以提供Hibernate的缓存实现,此依赖不是必需的
  "org.hibernate" % "hibernate-entitymanager" % "5.1.0.Final",    //hibernate提供JPA规范实现,目前最新版本为5.1
  "mysql" % "mysql-connector-java" % "8.0.11"    //数据库连接依赖,目前最新版本为8.0.11
)

对项目进行构建,下载相关依赖包:

.\sbt compile #若用的本地sbt环境,命令为:sbt compile

配置XML文件

JPA需要有有自己的XML配置文件,JPA规范要求,配置文件要位于配置目录的META-INF文件夹下,且默认名称为persistence.xml,因此,在项目的conf目录下新建一个名为META-INF的目录,再在META-INF目录下新建一个名为persistenceXML文件,文件配置内容如下:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
    <persistence-unit name="defaultPersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>    <!-- 采用hibernate的jpa规范实现 -->
        <non-jta-data-source>DefaultDS</non-jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
        </properties>
    </persistence-unit>
</persistence>

配置application.conf

application.confplay的全局配置文件,位于conf目录下,打开该文件加入以下配置:

db {    #数据库配置
  default.jndiName=DefaultDS
  default.driver=com.mysql.cj.jdbc.Driver    #这是最新mysql驱动的名称,旧版本mysql驱动配置名为com.mysql.jdbc.Driver
  default.url="jdbc:mysql://localhost/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false" #数据库连接,其中test是数据库名称
  default.username="root"
  default.password="root"
}
jpa {    #jpa配置
  default=defaultPersistenceUnit    #采用默认的jpa配置,会自动默认寻找读取persistence.xml文件中的配置
}

配置工作到此便结束,下面开始进行开发工作。

新建数据库表格及其对应的实体类

mysql数据库中新建用于测试的数据库及表格,这里新建一个名为test的数据库,在其中建立一个名为post的表,表中只有idname两个字段,类型为bigintvarchar,然后编写其对应的实体类。在app目录下新建目录(包)model,用于存放实体类,在model下新建scalaPost,代码如下:

//Post.scala
package model;

import javax.persistence._;

@Entity @Table(name = "post")    //jpa注解,对应post表
class Post {
    @Id @Column(name = "id")    //主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)    //指定主键策略为自动递增
    var id: Long = 0L
    
    @Column(name = "name") var name:String = ""
    
    override def toString: String = "id = " + id + ", " + "name = " + name    //重写toString方法
}

创建数据库访问层接口及其实现

app目录下新建新建repository目录(包),在此文件夹下新建scala特质PostRep,代码如下:

//PostRep.scala
package repository

import com.google.inject.ImplementedBy

import model.Post
import repository.impl.PostRepImpl

@ImplementedBy(classOf[PostRepImpl])    //指定特质的实现类
trait PostRep {
    def findById(id: Long): Post    //根据id查询
    def findAll(): List[Post]    //查询所有记录
    def addPost(post: Post): Post    //添加一条记录
}

repository文件夹下新建文件夹impl,用于存放特质的实现,在impl目录下新建scalaPostRepImpl,实现PostRep特质,代码如下:

//PostRepImpl.scala
package repository.impl

import javax.inject.Inject
import javax.persistence.EntityManager
import javax.inject._
import collection.JavaConverters._

import play.db.jpa.JPAApi;

import repository.PostRep
import model.Post

class PostRepImpl @Inject()(jpaApi: JPAApi) extends PostRep{    //依赖注入JPAApi

    override def findById(id: Long): Post = {
        return wrap(em => {
            //调用EntityManager的find方法,第一个参数是实体类的类型,第二个参数是主键
            return em.find(classOf[Post], id)
        })
    }
    override def findAll(): List[Post] = {
        return wrap(em => {
            //创建查询语句并执行,返回的结果类型为java.util.List,这里手动转换成scala的List返回
            return em.createQuery("select p from Post p", classOf[Post]).getResultList().asScala.toList
        })
    }
    override def addPost(post: Post): Post = {
        return wrap(em => {
            em.persist(post)  //EntityManager的persist方法,即insert,取名persist是为了更符合持久性的特性
            em.getTransaction().commit()    //若是不进行手动提交,可能存在虽然EntityManager托管了实体,但不持久化到数据库的问题
            return post
        })
    }
    //这里利用了jdk8的函数式编程接口
    private def wrap[T](function: java.util.function.Function[EntityManager, T]): T= {
        return jpaApi.withTransaction(function);
    }
}

测试

app/controller目录下新建HomeController类,用于验证jpa集成的正确性(当然,用单元测试也是更好的),代码如下:

//HomeController.scala
package controllers

import javax.inject._
import play.api._
import play.api.mvc._

import repository.PostRep
import model.Post

@Singleton
class HomeController @Inject()(cc: ControllerComponents, postRep: PostRep) extends AbstractController(cc) {
     def index() = Action { implicit request: Request[AnyContent] =>
         val post1 = new Post
         post1.name = "post1"
         val post2 = new Post
         post2.name = "post2"

         postRep.addPost(post1)
         postRep.addPost(post2)

         val post = postRep.findById(1L)
         println(post.toString)

         val posts = postRep.findAll
         for (p <- posts) println(p.toString)
         Ok(views.html.index())
    }
}

其中的views.html.index()对应了app/views目录下的一个模板,对应的文件名为index.scala.html,这里模板中没有任何有效内容,只是为了让程序能跑起来,模板代码如下:

@()

<h1>Hello</h1>

当然,在conf目录下的routes文件中有以下一条路由定义:

GET     /index                           controllers.HomeController.index

输入命令运行项目:

.\sbt run  #若用的本地sbt环境,命令为:sbt run

浏览器访问localhost:9000/index,可看到控制台打印消息如下:

play_jpa.png