在 scala play 2.5 framework 中,要將資料儲存在 DB 中有好幾種方式,都是以整合第三方套件的方式實作,我們測試了原始的 JDBC 以及 slick 兩種,以下是 JDBC 的部分。
準備資料庫
雖然很多範例都是以 H2 memory database 展示,不過我們還是以 Maria DB 進行測試,首先要準備資料庫。
- create mysql database: playdb
CREATE DATABASE playdb DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
- create table: user
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`login_id` varchar(45) NOT NULL,
`password` varchar(50) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`dob` bigint(20) DEFAULT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `login_id_UNIQUE` (`login_id`),
UNIQUE KEY `id_UNIQUE` (`id`)
)
BEGIN;
INSERT INTO `user` (login_id, password, name) VALUES ('test1', 'test1', 'test1'), ('test2', 'test2', 'test2');
COMMIT;
準備 scala play project
- 以 activator 產生一個 scala play project
activator new test5 play-scala
- 修改 build.sbt
增加 jdbc 及 "mysql" % "mysql-connector-java" % "5.1.36",
name := """test5"""
version := "1.0-SNAPSHOT"
lazy val root = (project in file(".")).enablePlugins(PlayScala)
scalaVersion := "2.11.7"
libraryDependencies ++= Seq(
jdbc,
cache,
ws,
filters,
"mysql" % "mysql-connector-java" % "5.1.36",
"org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test
)
resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases"
- application.conf 增加 default db 的設定
scala play 是以 HikariCP 作為 DB connection pool library,可以在 application.conf 中調整 DB 及 connection pool 設定。
play.db {
config = "db"
default = "default"
prototype {
# Sets a fixed JDBC connection pool size of 2
hikaricp.minimumIdle = 2
hikaricp.maximumPoolSize = 5
}
}
db {
default.driver=com.mysql.jdbc.Driver
default.url="jdbc:mysql://localhost:3306/playdb?useUnicode=true&characterEncoding=utf-8"
default.username = "root"
default.password = "password"
default.logSql=true
}
要確認 connection pool 有沒有作用,可以直接在 MySQL client 中,用以下的指令查看 DB 的資訊。
# 查看資料庫狀態
show status;
# 列出連線的數量
show status where `variable_name` = 'Threads_connected';
# 列出有哪些session
show processlist;
# 列出所有的session
show full processlist;
# 列出目前連線最多的 IP
mysql -uroot -p -e "show processlist"|awk '{print $3}' |awk -F: '{print $1}' |sort |uniq -c |sort -nr
- 修改 routes
因為是測試而已,簡單將 Application 的 Action 各自對應到獨立的 URI。
GET / controllers.Application.index
GET /dbUser controllers.Application.fetchDBUser
GET /dbUser2 controllers.Application.fetchDBUser2
GET /addUser/:name controllers.Application.addUser(name:String)
- 修改 controllers/Application.scala
注意 Application 的 constructor,因為 Play 2.5 已經使用 Inject 的方式取得資源,我們要使用的 DB 要在 constructor 中引用進來: @NamedDatabase("default") db: Database。
後面使用 db 的部分,就跟一般的 JDBC 差不多,createStament 以後,再執行 query 或是 update。
package controllers
import javax.inject.Inject
import play.api.mvc._
import play.api.db._
class Application @Inject()(@NamedDatabase("default") db: Database) extends Controller {
def index = Action {
Ok(views.html.main())
}
def fetchDBUser = Action {
var result = "DB User:\n"
val conn = db.getConnection()
try {
val rs = conn.createStatement().executeQuery("SELECT * from user")
while (rs.next()) {
result += rs.getString("login_id") + "\n"
}
} finally {
conn.close()
}
Ok(result)
}
// without try blocks,db.withConnection 的 connection 會在結束時自動關閉
def fetchDBUser2 = Action {
var result = "DB User:" + "\n"
db.withConnection { conn =>
val rs = conn.createStatement().executeQuery("SELECT * from user")
while (rs.next()) {
result += rs.getString("login_id") + "\n"
}
}
Ok(result)
}
def addUser(name: String) = Action {
db.withTransaction { conn =>
val rs = conn.createStatement().executeUpdate(s"insert into user (login_id, password, name) values('$name', '$name', '$name')")
}
Ok
}
}
測試
> curl 'http://localhost:9000/dbUser'
DB User:
test1
test2
> curl 'http://localhost:9000/dbUser2'
DB User:
test1
test2
> curl -v 'http://localhost:9000/addUser/test3'
* Trying ::1...
* Connected to localhost (::1) port 9000 (#0)
> GET /addUser/test4 HTTP/1.1
> Host: localhost:9000
> User-Agent: curl/7.50.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-ExampleFilter: foo
< Content-Length: 0
< Date: Thu, 11 Aug 2016 07:58:02 GMT
<
* Connection #0 to host localhost left intact
JDBC 的 logback 設定
如果要讓 log 記錄 SQL statement,就修改 logback.xml,增加這三行,但不建議在 production 環境加上這個設定
<logger name="org.jdbcdslog.ConnectionLogger" level="OFF" /> <!-- Won' log connections -->
<logger name="org.jdbcdslog.StatementLogger" level="INFO" /> <!-- Will log all statements -->
<logger name="org.jdbcdslog.ResultSetLogger" level="OFF" /> <!-- Won' log result sets -->
完整的 logback.xml 內容如下
<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->
<configuration>
<conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />
<!--
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home:-.}/logs/application.log</file>
<encoder>
<pattern>%date [%level] from %logger in %thread\n\t%message%n%xException</pattern>
</encoder>
</appender>
-->
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<append>true</append>
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<param name="FileNamePattern"
value="${application.home:-.}/logs/application.%d{yyyy-MM-dd}.log.zip">
</param>
</rollingPolicy>
<encoder>
<!-- <pattern>%d %-5p %c %L%n %m%n</pattern> -->
<!-- <charset class="java.nio.charset.Charset">UTF-8</charset> -->
<pattern>%date [%level] from %logger in %thread\n\t%message%n%xException</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%coloredLevel %logger{15} in %thread\n\t%message%n%xException{10}</pattern>
</encoder>
</appender>
<appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE" />
</appender>
<appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="STDOUT" />
</appender>
<logger name="play" level="INFO" />
<logger name="application" level="DEBUG" />
<logger name="org.jdbcdslog.ConnectionLogger" level="OFF" /> <!-- Won' log connections -->
<logger name="org.jdbcdslog.StatementLogger" level="INFO" /> <!-- Will log all statements -->
<logger name="org.jdbcdslog.ResultSetLogger" level="OFF" /> <!-- Won' log result sets -->
<!-- Off these ones as they are annoying, and anyway we manage configuration ourselves -->
<logger name="com.avaje.ebean.config.PropertyMapLoader" level="OFF" />
<logger name="com.avaje.ebeaninternal.server.core.XmlConfigLoader" level="OFF" />
<logger name="com.avaje.ebeaninternal.server.lib.BackgroundThread" level="OFF" />
<logger name="com.gargoylesoftware.htmlunit.javascript" level="OFF" />
<root level="WARN">
<appender-ref ref="ASYNCFILE" />
<appender-ref ref="ASYNCSTDOUT" />
</root>
</configuration>
這樣就可以在 log 中看到 SQL statement,當然這個設定只能用在 Dev 環境,不適合用在 Production。
[info] o.j.StatementLogger in application-akka.actor.default-dispatcher-2
java.sql.Statement.executeQuery: SELECT * from user;
[info] o.j.StatementLogger in application-akka.actor.default-dispatcher-3
java.sql.Statement.executeUpdate: insert into user (login_id, password, name) values('test4', 'test4', 'test4');
Database Evolution
scala play 內建了 db 升降版本的功能,如果要使用,必須先準備一個全新的空白的資料庫,但先不要加上 tables。
- build.sbt 中增加 libraryDependencies += evolutions
name := """test5"""
version := "1.0-SNAPSHOT"
lazy val root = (project in file(".")).enablePlugins(PlayScala)
scalaVersion := "2.11.7"
libraryDependencies ++= Seq(
jdbc,
evolutions,
cache,
ws,
filters,
"mysql" % "mysql-connector-java" % "5.1.36",
"com.typesafe.play" %% "anorm" % "2.5.0",
//"com.typesafe.play" %% "play-slick" % "2.0.0",
//"com.typesafe.play" %% "play-slick-evolutions" % "2.0.0",
"org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test
)
resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases"
application.conf 增加 ply.evolutions 這個部分的設定
play.evolutions {
# You can disable evolutions for a specific datasource if necessary
db.default.enabled = true
autoApply = true
autoApplyDowns = true
}
- 新增兩個 sql file
DB evolution 是以 sql file 的方式,進行 DB 版本升降,要注意 sql file 裡面規定一定要有 Ups 以及 Downs 這兩個部分。
/conf/evolutions/default/1.sql
# Users schema
# --- !Ups
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`login_id` varchar(45) NOT NULL,
`password` varchar(50) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`dob` bigint(20) DEFAULT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `login_id_UNIQUE` (`login_id`),
UNIQUE KEY `id_UNIQUE` (`id`)
);
INSERT INTO `user` (login_id, password, name) VALUES ('test1', 'test1', 'test1'), ('test2', 'test2', 'test2');
# --- !Downs
DROP TABLE user;
/conf/evolutions/default/2.sql
# Users schema
# --- !Ups
CREATE TABLE temp (
id bigint(20) NOT NULL AUTO_INCREMENT,
email varchar(255) NOT NULL,
password varchar(255) NOT NULL,
fullname varchar(255) NOT NULL,
isAdmin boolean NOT NULL,
PRIMARY KEY (id)
);
# --- !Downs
DROP TABLE temp;
- 測試 evolution
啟動 play,如果有了第一個 DB 連線,就會進行 1.sql, 2.sql 建立兩個 tables,如果想要降版,就把 2.sql 改成 2.sql.bak,server code reload 後,有了第一個 DB 連線,就會自動降版。
mysql DB 裡面會自動產生一個 table: play_evolutions,他會記錄每一次 DB 升降版本執行的 db script。
DROP TABLE IF EXISTS `play_evolutions`;
CREATE TABLE `play_evolutions` (
`id` int(11) NOT NULL,
`hash` varchar(255) NOT NULL,
`applied_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`apply_script` mediumtext,
`revert_script` mediumtext,
`state` varchar(255) DEFAULT NULL,
`last_problem` mediumtext,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
沒有留言:
張貼留言