- Java からデータベース管理システムに接続する最も基本的なモジュール
- JDBC
- DriverでRDBMSの差を吸収して統一的なインターフェイスを提供する
- MySQL、PostgreSQL、H2、…
- JDBCでRDBMSとやり取りする最も素朴な方法
- プレースホルダ機能
- プリペアードステートメント機能
import java.sql.{Connection, ResultSet, Statement, PreparedStatement, DriverManager}
object Main {
def main(args: Array[String]) {
Class.forName("com.mysql.jdbc.Driver")
val conn: Connection = DriverManager.getConnection("jdbc:mysql://localhost/vocaloid", "nobody", "nobody")
val st: PreparedStatement = conn.prepareStatement("SELECT * FROM artist WHERE birthday < ? ORDER BY birthday ASC")
st.setString(1, "2008-01-01")
val rs: ResultSet = st.executeQuery()
while (rs.next()) {
println(s"""${rs.getInt("id")}, ${rs.getString("name")}, ${rs.getString("birthday")}""")
}
rs.close()
st.close()
conn.close()
}
}
// =>
// 1, 初音ミク, 2007-08-31
// 2, 鏡音リン, 2007-12-27
// 3, 鏡音レン, 2007-12-27
以下のようなモジュールを使います
- Slick
- Functional Relational Mapping Library
- Reactive
- サンプルコードでは簡単のため Reactive な書き方はしてない
- Plain SQL
- HikariCP
- コネクションプーリング
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext.Implicits.global
import slick.driver.MySQLDriver.api._
object Main {
def main(args: Array[String]) {
val db = Database.forConfig("db.default")
val name = "初音ミク"
Await.result(
db.run(
sql"""SELECT * FROM artist WHERE name = ${name} LIMIT 1""".as[(Int, String, String)]
).map(println),
Duration.Inf
)
}
}
- 対応するレコードを表すCase Class のインスタンスに変換すると便利
- 例:
- artistテーブルに対応するArtistケースクラス
- albumテーブルに対応するAlbumケースクラス
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext.Implicits.global
import slick.driver.MySQLDriver.api._
import org.joda.time.LocalDate
import slick.jdbc.GetResult
import com.github.tototoshi.slick.MySQLJodaSupport._
case class Artist(id: Long, name: String, birthday: LocalDate)
def main(args: Array[String]) {
val db = Database.forConfig("db.default")
val name = "初音ミク"
Await.result(
db.run(
sql"""SELECT * FROM artist WHERE name = ${name} LIMIT 1""".as[Artist]
).map(println),
Duration.Inf
)
}
implicit val userGetResult = GetResult(r => Artist(r.<<, r.<<, r.<<))
//implicit val userGetResult = GetResult(r => Artist(r.nextInt, r.nextString, new LocalDate(r.nextString)))
- Future を使ったコードを書くのは面倒
- 今回はSQLに集中してもらうため 同期的に使えるようにした ラッパー を通して使う
def find(userId: Long)(implicit ctx: Context): Option[User] = run(
sql"""SELECT * FROM user WHERE id = $userId LIMIT 1""".as[User].map(_.headOption)
)
- ここから説明する方法を使ってクエリを発行しよう
val name = "初音ミク"
val artist: Option[Artist] = run(
sql"""SELECT * FROM artist
WHERE name = ${name}
LIMIT 1""".as[Artist]
).headOption
println artist
SELECT * FROM artist WHERE name = '初音ミク' LIMIT 1;
id | name | birthday |
---|---|---|
1 | 初音ミク | 2007-08-31 |
val name = "鏡音%"
val limit = 10
val offset = 0
val artists: Seq[Artist] = run(
sql"""SELECT * FROM artist
WHERE name LIKE ${name}
ORDER BY id ASC
LIMIT ${limit}
OFFSET ${offset}""".as[Artist]
)
artist.foreach(println)
SELECT * FROM artist WHERE name LIKE '鏡音%' ORDER BY id ASC LIMIT 10 OFFSET 0;
id | name | birthday |
---|---|---|
2 | 鏡音リン | 2007-12-27 |
3 | 鏡音レン | 2007-12-27 |
val id = 5
val name = "重音テト"
val birthday = "2008-04-02"
run(
sqlu"""
INSERT INTO artist
SET
id = ${id},
name = ${name},
birthday = ${birthday}"""
)
INSERT INTO artist (id, name, birthday)
VALUES (5, '重音テト', '2008-04-01');
id | name | birthday |
---|---|---|
1 | 初音ミク | 2007-08-31 |
2 | 鏡音リン | 2007-12-27 |
3 | 鏡音レン | 2007-12-27 |
4 | 巡音ルカ | 2009-01-30 |
5 | 重音テト | 2008-04-01 |
val name = "弱音ハク"
val id = 1
run(
sqlu"""
UPDATE artist
SET
name = ${name}
WHERE
id = ${id}"""
)
UPDATE artist SET name = '弱音ハク' WHERE id = 1;
id | name | birthday |
---|---|---|
1 | 2007-08-31 | |
2 | 鏡音リン | 2007-12-27 |
3 | 鏡音レン | 2007-12-27 |
4 | 巡音ルカ | 2009-01-30 |
5 | 重音テト | 2008-04-01 |
val id = 1
run(
sqlu"""
DELETE FROM artist
WHERE
id = ${id}"""
)
DELETE FROM artist WHERE id = 1;
id | name | birthday |
---|---|---|
2 | 鏡音リン | 2007-12-27 |
3 | 鏡音レン | 2007-12-27 |
4 | 巡音ルカ | 2009-01-30 |
5 | 重音テト | 2008-04-01 |
- データベースの脆弱性は致命的
- データの漏洩、損失
- 気をつけましょう
val name = "..." // ユーザの入力
val st: PreparedStatement = conn.prepareStatement(
s"SELECT * FROM artist WHERE name = $name",
)
...
SELECT * FROM artist WHERE name = '初音ミク';
- ユーザの入力は安全ではない!
- 名前に "
''; DROP TABLE artist
" と入力されると…? - ref. SQLインジェクション脆弱性
- 対策として、必ずプレースホルダを使うこと
SELECT * FROM artist WHERE name = ''; DROP TABLE artist;
- 実践編です
- 小さなブックマークアプリを書いていく過程を見ていきます
- ユーザは URL (エントリ) を個人のブックマークに追加し、コメントを残せる
- エントリはユーザに共通の情報を持つ (ページタイトルなど)
- とりあえず一人用で (マルチユーザも視野にいれつつ)
- run add <url> [コメント]
- ブックマークを追加
> run add http://www.yahoo.co.jp/ ヤッホー
Bookmarked [8] Yahoo! JAPAN <http://www.yahoo.co.jp/>
@2011-08-16 ヤッホー
- run list
- ブックマークの一覧を出力
> run list
*** motemen's bookmarks ***
[8] Yahoo! JAPAN <http://www.yahoo.co.jp/>
@2011-08-16 ヤッホー
[7] The CPAN Search Site - search.cpan.org <http://search.cpan.org/>
@2011-08-16 くぱん
[6] はてな <http://www.hatena.ne.jp/>
@2011-08-16 はてー
[4] Google <http://www.google.com/>
@2011-08-16 ごー
[1] motemen <http://motemen.appspot.com/>
@2011-08-15 モテメンドットコム
- run delete <url>
- ブックマークを削除
> run delete http://www.google.com/
Deleted
コードを手元にもってきて試してみましょう
$ git clone https://github.com/hatena/scala-Intern-Bookmark
$ cd scala-Intern-Bookmark
$ sbt
> run
データベーススキーマを考える前にどのようなデータが登場するか整理してみよう。
User
ブックマークをするユーザEntry
ブックマークされた記事(URL)Bookmark
ユーザが行ったブックマーク
- 1つのEntryには複数のBookmarkが属する (一対多)
- 1つのUserには複数のBookmarkが属する (一対多)
はじめに図を書くと整理できる & モデリングをレビューしてもらえる。
クラス図で分析したデータ構造をSQLのテーブルに対応付ける。
- モデル同士の関係
- 何によってデータを一意に特定できるか
id | name |
---|---|
1 | jkondo |
2 | chris4403 |
3 | motemen |
- UNIQUE KEY (name)
ユーザに共通の、URL に関する情報
id | url | title |
---|---|---|
1 | http://www.example.com/ | IANA — Example domains |
2 | http://www.hatena.ne.jp/ | はてな |
3 | http://motemen.appspot.com/ | motemen |
- UNIQUE KEY (url)
ユーザが URL をブックマークした情報 (ユーザ×エントリ)
id | user_id | entry_id | comment |
---|---|---|---|
1 | 1 (= jkondo) | 1 (= example.com) | 例示用ドメインですね |
2 | 1 | 2 (= はてな) | はてな〜。 |
3 | 2 (= chris4403) | 3 (= motemen.com) | モテメンさんのホームページですね |
4 | 3 (= motemen) | 3 | 僕のホームページです |
5 | 3 | 1 | example ですね |
- UNIQUE KEY (user_id, entry_id)
- データの定義はできた
- どこにどのようなプログラムを書けばよいか??
- DBにアクセス
- 得られたデータを集めてくる
- データを表示する部分
- 綺麗に分割することで品質の高いソフトウェアになる
- プログラムを責務ごとのレイヤに分けて設計する。
- より上位の層が下位の層を利用するという形でプログラムを実装することで、見通しがよくなる
名前 | 説明 |
---|---|
インターフェース層 | ユーザや外部プログラムとインタラクションする層 |
アプリケーション層 | ドメイン層の機能を同士を組み合わせる層 |
ドメイン層 | インフラ層の機能を使いプログラムの役立つ機能を実装する層 |
インフラ層 | DBやネットワークなどプログラムの外部機能とやりとりする層 |
- 今回は規模が小さいのでアプリケーション層は使わない.
はてなでよく使われている、ドメイン層を整理するための設計方法の一つ。
- Model: モデルを抽象化した単純なオブジェクト
- Service: モデルに含めることが出来ないインフラ層とのやり取りを実装するモジュール
Modelを単純なオブジェクトにすることで見通しがよくなる。
internbookmark.cli.BookmarkCLI
は最小限の処理に- ドメインロジックはドメイン層であるmodelとserviceに集約
add_bookmark
やlist_bookmarks
などのコマンドはmodelと serviceを組み合わせるだけ- 引数からコマンドを受け付ける部分 = インターフェース層
- モデルを抽象化した単純なケースクラス
- テーブルの1レコードがmodelの1オブジェクトになることが多い
- 副作用はなく、状態を持たない
- ここからデータベースへアクセスしない ように注意
- 思っても見ないところからDBアクセスが行われないように
package internbookmark.model
import org.joda.time.DateTime
case class User(id: Long, name: String, createdAt: DateTime, updatedAt: DateTime)
- その他 Entry, Bookmark も同じように
- サービスはリポジトリを使ってアプリケーションのコアロジックを実装する.
データベースなどのやり取りを実装するインフラ層のモジュール。
- SQLを実行するのはrepositoryからのみ
- repositoryのメソッドは、必要に応じてModelのオブジェクトを返す
どんなSQLが使えるか考えてみよう。
// ブックマーク一覧
// SELECT * FROM bookmark WHERE user_id = ... のようなSQLを使って実装
val bookmarks = internbookmark.service.Bookmark.searchBookmarksByUser(user)
// ブックマーク追加
// INSERT INTO bookmark ... のようなSQLを使って実装
internbookmark.service.Bookmark.addBookmark(user, url, comment)
// ブックマーク削除
// DELETE FROM bookmark WHERE id = ... のようなSQLを使って実装
internbookmark.service.Bookmark.deleteBookmarkByUrl(user, url)
- いきなり実装を書くのは難しい?
- 案1: とりあえずテストを書いてみる
- 案2: とりあえず一番外側のスクリプトを書いてみる
- REPL で試しながら少しずつ実装する (sbt console)
- レイヤ化アーキテクチャを意識
- repositoryにはDBへのアクセスを書く
- modelからDBにアクセスしない
- modelはテーブルのレコードを表現する
- service でアプリケーションのコアロジックを実装する
internbookmark.cli.BookmarkCLI
ではserviceのメソッドを呼び出し、modelを表示する- scala-Intern-Bookmarkをよく読もう
- 書いたプログラムが正しく動くことをどう確かめるか?
- 小規模なら実際に動かしてみるのでもやっていける
- = 大規模だとムリ
- コードの変更の影響を完全に把握するのは無理
- 意図せず別の機能に不具合を引き起こしていないか (リグレッション)
- 他人のコードの意図は把握できない
- 昔の自分も他人です (だいたい一晩から)
- 小規模なら実際に動かしてみるのでもやっていける
- 今回は単体テストを書きましょう
- 正しい条件で正しく動くこと (正常系)
- おかしな条件で正しく動くこと (異常系)
- エラーを吐くなど
- 境界条件で正しく動くこと
-
ScalaTest というテストフレームワークを使っています
- すべてのテスト用スクリプトから import する
- 事前条件をセットするユーティリティ(ユーザーを作る、エントリを用意するなど) Factory.scala
- HTTP アクセスしないフラグを立てる、等々EntriesDBForTest.scala
- 本番の DB にアクセスしないようにする
- テスト専用の DB を用意して、テストでは必ずそちらを使うようにする
- test.conf
- 外部との通信を発生させない
- テストの高速化/自動化にもつながります
scala-Intern-Bookmark/
├── .gitignore
├── README.md
├── build.sbt
├── db/
│ └── schema.sql
├── project/
│ ├── build.properties
│ └── plugins.sbt
├── script/
│ └── setup.sh
└── src/
├── main/
│ ├── resources/
│ │ ├── application.conf
│ │ └── logback.xml
│ ├── scala/
│ │ ├── HatenaOAuth.scala # WAF の講義で使います
│ │ ├── ScalatraBootstrap.scala # WAF の講義で使います
│ │ ├── internbookmark/
│ │ │ ├── cli/
│ │ │ │ └── BookmarkCLI.scala
│ │ │ ├── model/
│ │ │ │ ├── Bookmark.scala
│ │ │ │ ├── Entry.scala
│ │ │ │ └── User.scala
│ │ │ ├── repository/
│ │ │ │ ├── Bookmarks.scala
│ │ │ │ ├── Context.scala
│ │ │ │ ├── Entreis.scala
│ │ │ │ ├── Identifier.scala
│ │ │ │ ├── TitleExtractor.scala
│ │ │ │ ├── TitleExtractorDispatch.scala
│ │ │ │ ├── Users.scala
│ │ │ │ ├── db/
│ │ │ │ │ └── JdbcBackend.scala
│ │ │ │ └── package.scala
│ │ │ ├── service/
│ │ │ │ ├── BookmarkApp.scala
│ │ │ │ ├── Error.scala
│ │ │ │ ├── Json.scala
│ │ │ │ └── package.scala
│ │ │ └── web/ # WAF の講義で使います
│ │ └── slick/
│ │ └── jdbc/
│ │ └── TransactionalJdbcBackend.scala
│ ├── twirl/ # WAF の講義で使います
│ └── webapp/ # WAF の講義で使います
└── test/
├── resources/
│ └── test.conf
└── scala/
└── internbookmark/
├── helper/
│ ├── EntriesDBForTest.scala
│ ├── Factory.scala
│ ├── SetupDB.scala
│ ├── UnitSpec.scala
│ └── WebUnitSpec.scala # WAF の講義で使います
├── service/
│ └── BookmarkAppSpec.scala
└── web/ # WAF の講義で使います
データベースに日記を記録するCLI版 Intern-Diaryを作りましょう。
- 必須課題(1点)
- モデルクラスを定義してみてください
- 考えたクラスを元にデータベースのテーブルスキーマをSQLで記述してください
- SQLはdb/schema.sql というファイルに書いてください
- できたら先に進む前にメンターに見てもらってください
- データベースに日記を記録するCLI版 Intern-Diaryを作って下さい
- オプション課題(1点)
- テストを書いてください(できるだけがんばろう)
- オプション課題(1点)
- アプリケーションに独自の機能を追加してみてください
- (例)記事のカテゴリ分け機能
- ヒント: 多対多リレーションの活用
- (例)検索
- ヒント:
LIKE
演算子
- ヒント:
- (例)マルチユーザ化
- (例)記事のカテゴリ分け機能
- アプリケーションに独自の機能を追加してみてください
https://github.com/hatena/scala-Intern-Diary にひな形があるので fork して開発しましょう。 課題提出時は元ブランチへ Pull-Request を送ってください。
評価のため mysqldump もお願いします。
保存先は mysqldump ディレクトリに
$ mkdir mysqldump
$ mysqldump -uroot -Q intern_diary_$USER > mysqldump/intern_diary_$USER.sql
これも commit, push してください。