背景
准备熟悉前任同事基于 Play framework 开发的后台项目,想着既然是 Java 语言,那么从数据层入手,将会事半功倍。
正文
这篇笔记参考的官方文档是:Play 2.6.x JavaDatabase。
要点:
- JDBC Driver
- JNDI
- SQL Log
- JPA and Hibernate
- ORM Ebean
JDBC Driver
在根目录下找到 build.sbt
文件,添加 JDBC 驱动依赖:
libraryDependencies += javaJdbc
接下来打开 conf/application.conf
配置文件,添加默认的 JDBC 数据源:
# 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"
SQLite 数据库:
# Default database configuration using SQLite database engine
db.default.driver=org.sqlite.JDBC
db.default.url="jdbc:sqlite:/path/to/db-file"
PostgreSQL 数据库:
# Default database configuration using PostgreSQL database engine
db.default.driver=org.postgresql.Driver
db.default.url="jdbc:postgresql://database.example.com/playdb"
这里为了数据统一性,还是安装了一个 MySQL Windows版本 数据库。
安装是挺简单的,遇到问题也都可以从百度找到答案,唯一需要注意的是,可能你下载的安装程序附带数据库管理工具,通常只需要选择 Server Only
进行安装。
配置MySQL 数据库:
# Default database configuration using MySQL database engine
# Connect to playdb as playdbuser
db.default.driver=com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://localhost/playdb"
db.default.username=playdbuser
db.default.password="a strong password"
需要注意的是,访问数据库需要统一编码并且禁用 SSL 方式:
db.default.url="jdbc:mysql://localhost/playdb?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false"
如果你开源了项目,请记得不要泄露数据库账户密码。
你也可以这样设置 JDBC 驱动的依赖和 MySQL 的连接依赖:
libraryDependencies ++= Seq(
javaJdbc,
"mysql" % "mysql-connector-java" % "5.1.41",
)
这和下面的写法是完全一样的:
libraryDependencies += javaJdbc
libraryDependencies += "mysql" % "mysql-connector-java" % "5.1.41"
配置 CustomExecutionContext
并不表示 Play 是一个同步框架(相反,它是纯异步的),只不过为了将内核线程专注于页面的渲染,把访问和操作数据库的线程独立出来:
# db connections = ((physical_core_count * 2) + effective_spindle_count)
fixedConnectionPool = 9
database.dispatcher {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = ${fixedConnectionPool}
}
}
JNDI
公开数据源给其他数据库操作接口,比如 JPA:
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
db.default.jndiName=DefaultDS
SQL Log
为了快速找到问题所在,以及跟踪数据库运行情况,可以在开发环境下,开放日志权限:
# Default database configuration using PostgreSQL database engine
db.default.driver=org.postgresql.Driver
db.default.url="jdbc:postgresql://database.example.com/playdb"
db.default.logSql=true
注意:这不是必须的操作,因为它将影响性能,如果你忘记在生产环境中关闭它的话,会有很大的麻烦。
JPA and Hibernate
JPA 只是一系列接口,具体实现还是要依赖 Hibernate
:
libraryDependencies ++= Seq(
javaJpa,
"org.hibernate" % "hibernate-entitymanager" % "5.1.0.Final" // replace by your jpa implementation
)
为了让 JPA 访问到数据源,请确认在 conf/application.conf
中配置:
db.default.jndiName=DefaultDS
然后在 conf
目录下,创建一个叫 META-INF
的目录,并新建文件 persistence.xml
:
org.hibernate.jpa.HibernatePersistenceProvider
DefaultDS
需要注意的是,由于我们使用的是 MySQL 数据库,需要将上面文件中:
改为:
接着在 build.sbt
中添加:
PlayKeys.externalizeResources := false
最后继续在 conf/application.conf
中添加配置:
jpa.default=defaultPersistenceUnit
以上操作都是为了让 JPA 正常访问数据源,接下来还要看如何使用 play.db.jpa.JPAApi
这个类。
如何使用 JPA 进行数据访问?
- 创建 Account 数据实体类:
@Entity(name = "account")
public class Account {
@Id
public String username;
public String password;
public long u_id;
public int type;
}
这里将 Account
设为 public
并不奇怪,文档说 Play framework
中的 Entity 在编译后,会自动生成 getter
和 setter
方法。
纠正:检查编译后产生的 class 文件并没有 getter 和 setter 方法,你需要安装一个 IDEA 插件,用来增强实体类,以便自动生成需要的方法。
- 创建 AccountDao 数据映射接口:
public interface AccountDao {
void save(Account entity);
void delete(String username);
Account findByName(String username);
Account findById(long userId);
List findAll();
}
- 实现 AccountDao 接口:
public class AccountDaoImp implements AccountDao {
private final JPAApi jpaApi;
@Inject
public AccountDaoImp(JPAApi jpaApi) {
this.jpaApi = jpaApi;
}
@Override
public void save(Account entity) {
jpaApi.em().merge(entity);
}
@Override
public void delete(String username) {
Account entity = findByName(username);
jpaApi.em().remove(entity);
}
@Override
public Account findByName(String username) {
return jpaApi.em().find(Account.class, username);
}
@Override
public Account findById(long userId) {
try {
return jpaApi.em().createQuery("SELECT entity FROM Account entity where entity.u_id = :userId", Account.class)
.setParameter("userId", userId)
.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
@Override
public List findAll() {
Query query = jpaApi.em().createQuery("SELECT entity FROM Account entity", Account.class);
try {
return query.getResultList();
} catch (NoResultException e) {
return null;
}
}
}
- Guice 框架的运行时依赖注入绑定:
@ImplementedBy(AccountDaoImp.class)
public interface AccountDao {
// ...
}
- 在控制器中使用 AccountDao:
public class AccountController extends RestController {
private final AccountDao accountDao;
@Inject
public AccountController(AccountDao accountDao) {
this.accountDao= accountDao;
}
@Transactional
public Result home() {
Account account = new Account();
// account的一些赋值操作已省略,请自行完成
accountDao.save(account);
return created();
}
// 其他代码省略...
}
需要注意的是,必须在使用了 Dao
接口的方法上,标记一个叫 @Transactional
的注解,表示启用事务来处理数据。
Hibernate
用起来还是不太方便,尽管已经用 JPA
把数据查询做到了完美,但 ORM
可以节省你一半的写 SQL 语句时间。
ORM Ebean
要使用 Ebean
框架,需要在 project/plugins.sbt
中添加:
addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "4.0.1")
然后修改 build.sbt
中的配置为:
lazy val myProject = (project in file(".")).enablePlugins(PlayJava, PlayEbean)
为了使 Ebean
得知数据实体所在,还需要在 conf/application.conf
中添加:
ebean.default = ["models.*"]
当然,其他数据源也可以这样添加:
ebean.orders = ["models.Order", "models.OrderItem"]
ebean.customers = ["models.Customer", "models.Address"]
完成以上操作后,接下来就可以稍微改动一下,前面的 Account
了:
@Entity
@Table(name = "account")
public class Account extends Model {
@Id
public String username;
public String password;
public long u_id;
public int type;
public static final Finder find = new Finder<>(Account.class);
}
事实上只要继承 Model
类就可以了,这里为了规范代码,将 Entity
注解和 Table
注解分开使用。
关于 Finder
类,实际上是参照了文档中的做法,可以看看它的使用效果:
List accounts = Account.find.all();
Account anyAccount = Account.find.byId("username");
Account.find.ref("username").delete();
List account007 = Account.find.query().where()
.ilike("u_id", "%007%")
.orderBy("type asc")
.setFirstRow(0)
.setMaxRows(25)
.findPagedList()
.getList();
由于原本同事的项目并没有使用 Ebean 框架,所以这部分内容只能到这里,未来若有实践机会,再逐步完善这里的细节。
另外:由于使用了数据库,Play framework 会自动启用演化这个功能,通过在 conf/evolutions/default
目录下生成的 1.sql
来跟踪数据库的演变历程,据官网介绍说,这是为了在不同机器下,以及不同开发者之间,来保持项目完整性。
演化功能会在数据库中自动建立一张表来记录演变历史,如果希望数据库是独立又纯粹的,可以在 conf/application.conf
中,使用下面的代码来禁掉演化功能:
play.evolutions.enabled=false
上面的禁用只能让数据库不再自动创建演化表,却由于使用了 Ebean
而导致 conf/evolutions/default
目录下,依然会记录数据库的演变历程。你需要手动执行这些演变历程,以保证项目在更新之后可以正常运行。
总结
数据库访问是后台的根本,掌握住这个套路,你将无所畏惧...啊?好吧,除了 Redis、Cassandra、Elasticsearch 以及它们的集群配置...