msawady’s learning memo

ただのJavaエンジニアが、学んだことをログっていくところ

【Python】key と value が連なった List を Dict に変換する

Python の tips を備忘代わりに

  • プロジェクトが変わりまして、Pythonを触ることが増えました
  • 掲題の件で悩んだので、パフォーマンスの検証も含めて post しておきます

やりたいこと

こういう List を

hoge_list = ['key1',  'value1',  'key2',  'value2',  'key3',  'value3',,,,]

こういう Dict にしたい

hoge_dict = {'key1':  'value1',  'key2':  'value2',  'key3':  'value3',,,,}

これだけ。

調べて出てきたやり方

困ったときは、stackoverflow ですね。 stackoverflow.com

method1

hoge_dict = dict(zip(hoge_list[0::2], hoge_list[1::2]))

綺麗なワンライナーですね。奇数番目の要素を取り出した List と、偶数番目の要素を取り出した List を zip するというやり方です。

method2

hoge_dict = { hoge_list[i]: hoge_list[i+1] for i in range(0, len(hoge_list), 2) }

dict comprehension を使って、値を順々に key: value に入れていくというやり方です。

method3 (Python3 のみ)

i = iter(hoge_list)
hoge_dict = dict(zip(i, i))

Python3 からは zip が遅延評価になっているので、このやり方で iterator から順々に要素を取り出して zip することができます。

パフォーマンス検証

検証用コード

1万個の key と value のセットを並べたリストを、それぞれのやり方で1万回ずつ回して、実行時間を取ります。

from datetime import datetime


def method1(hoge_list):
    return dict(zip(hoge_list[0::2], hoge_list[1::2]))


def method2(hoge_list):
    return {hoge_list[i]: hoge_list[i + 1] for i in range(0, len(hoge_list), 2)}


def method3(hoge_list):
    i = iter(hoge_list)
    return dict(zip(i, i))


if __name__ == '__main__':
    hoge_list = []
    for i in range(0, 10000):
        hoge_list.append('key{}'.format(i))
        hoge_list.append('value{}'.format(i))

    start = datetime.now()
    for i in range(0, 10000):
        method1(hoge_list)
    time1 = datetime.now() - start

    start = datetime.now()
    for i in range(0, 10000):
        method2(hoge_list)
    time2 = datetime.now() - start

    start = datetime.now()
    for i in range(0, 10000):
        method3(hoge_list)
    time3 = datetime.now() - start

    print('time1: {}'.format(time1))
    print('time2: {}'.format(time2))
    print('time3: {}'.format(time3))

結果

time1: 0:00:13.092214
time2: 0:00:24.749644
time3: 0:00:09.524495

method3 < method1 <<< method2 という結果になりました。

  • method3 が method1 より速いのは納得です。「1回 key のリストと value のリストを作ってから zip する」のと、「順々に値を取り出してzipする」のでは、後者のほうがメモリコピーの回数が少ないので速くなりそうですね。
  • method2 がこんなに遅いのがちょっとビックリでした。調べたところ、Dict Comprehension は内部的に update() を呼んでいるようなので、「1回ずつupdate()を行う」のと、「zipでまとめてからdictに変換する」のでは確かに後者のほうが速そうですね。

結論

method3を使いましょう。

i = iter(hoge_list)
hoge_dict = dict(zip(i, i))

参考ページ

  • 各種 comprehension の内部的な挙動(英語)

Stupid Python Ideas: How do comprehensions work?

  • zip の内部的な挙動 (英語)

How does Python's zip function work internally? - Quora

【Scala】【mongoDB】mongoDB のレコードを case クラスにマッピングする

株のポジション管理を出来るようにしたいと思っています

  • 大分放置してしまいましたが、ちょっとずつ再開します。
  • まずは銘柄一覧をブラウザから見えるようにします。
  • http://kabusapo.com/ranking/ からダウンロードしたcsvファイルを case クラスにパースしてmongoDBにインサート→ mongoDBからフェッチして画面表示しました
  • クライアントなどは前回のTODO管理で作ったものをほとんど再利用しています。

msawady.hatenablog.com

コード

Stock.scala

package services.domain

import org.mongodb.scala.bson.ObjectId
import org.mongodb.scala.bson.codecs.Macros._
import org.mongodb.scala.bson.codecs.DEFAULT_CODEC_REGISTRY
import org.bson.codecs.configuration.CodecRegistries.{fromRegistries, fromProviders}


case class Stock(_id: ObjectId, code: String, name: String, market: String, industryType: String, unit: Int, isNK225: Boolean)

object Stock {
  def apply(code: String, name: String, market: String, industryType: String, unit: Int, isNK225: Boolean): Stock 
  = new Stock(new ObjectId, code, name, market, industryType, unit, isNK225)
  
  implicit val codecRegistry = fromRegistries(fromProviders(classOf[Stock]), DEFAULT_CODEC_REGISTRY)
}

Entityクラスはこんな感じで書きました。 * mongoDBのユニークキーであるObjectIdを生成してからインスタンス化するようにしています。 * codecRegistry は クラスとJsonのマッパーのようなものだと解釈しています。

StockRepositry.scala

package services.repository

import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Singleton

import org.mongodb.scala.{MongoClient, MongoCollection}
import play.Environment
import services.domain.Stock
import services.repository.Helpers._

import scala.io.Source

@Singleton
class StockRepository {

  val mongoClient = MongoClient()
  val database = mongoClient.getDatabase("database").withCodecRegistry(Stock.codecRegistry)
  val initialized = new AtomicBoolean()


  def toStock(data: String): Stock = {
    val el = data.split(",")
    Stock(el(0), el(1), el(2), el(3), safeStringToInt(el(4)).getOrElse(0), if (el.length > 5) "1".equals(el(5)) else false)
  }

  def initializeIfNeeded(): Unit = {
    if (initialized.get()) {
      return
    }

    val collection: MongoCollection[Stock] = database.getCollection("stock")
    collection.drop().results()

    val s = Source.fromFile(Environment.simple().getFile("data/stocklist.csv"), "UTF-8")
    val lines = s.getLines().toList
    val stocks: List[Stock] = lines.tail.map(l => toStock(l)).toList

    collection.insertMany(stocks).results()
    this.initialized.set(true)
  }

  def getStockList(): List[Stock] = {
    initializeIfNeeded()

    val collection: MongoCollection[Stock] = database.getCollection("stock")
    return collection.find().results().toList
  }

  def safeStringToInt(str: String): Option[Int] = {
    import scala.util.control.Exception._
    catching(classOf[NumberFormatException]) opt str.toInt
  }

}
  • AtomicBoolean を初期化済みフラグとして、初期化されていなかったらファイルの読み込み→インサートとしています。

    • ホントはファイルをダウンロードしたいんだけど上手く行かなかった...
  • tailを使うことで1列目を読み飛ばす(= ヘッダーを読み飛ばす)ことが出来ます。

    val stocks: List[Stock] = lines.tail.map(l => toStock(l)).toList
  • 文字列からオブジェクトにパースする際に、単元株数の列に"単元制度なし"というレコードが有ったので(#^ω^)ピキピキ
  def safeStringToInt(str: String): Option[Int] = {
    import scala.util.control.Exception._
    catching(classOf[NumberFormatException]) opt str.toInt
  }

といったメソッドで Option を返すようにしています。

結果

こんな感じになりました。 f:id:msawady:20171029143445p:plain

次にやること

  • まずは kendo-ui で上手くフィルタリング出来るようにしたい
  • ポジションを登録できるようにする
  • 時価をうまい具合に取れるようにする
  • お気に入り銘柄(ウォッチリスト)を作れるようにする

くらいですかね。ちょっとずつ頑張ります。

【mongoDB】【Scala】mongoDB のインストール → Scala で CRUD する

次のテーマは mongoDB

  • twitter で呟いたりしたんですが、次のプロジェクトではmongoDB使います
  • ただ、自分のタスクはインフラ運用に近い部分なので、実際にどうクエリするかみたいなところは暫く触らなそう
  • なので、自習でやってみることにしました
  • とりあえずインストール→CRUDまでやってみる。

インストール

  • 以下のサイトからダウンロード、インストール。

    MongoDB Download Center | MongoDB

  • データ出力先、ログ出力先のフォルダを作成する

mkdir C\mongo\data
mkdir C\mongo\log
  • 実行ファイルにパスを通す。デフォルトだとC:\Program Files\MongoDB\Server\3.4\bin

  • 以下のコマンドを実行する。コマンドプロンプトは開きっぱなしにする。(.batファイルにしておくと便利)

mongod --dbpath c:\mongo\data --logpath c:\mongo\log\mongodb.log
mongo
  • テスト用のDBを作成 → 接続
use helloMongo

めっちゃ簡単ですね。

scalaCRUDする

依存関係にMongo Scala Driverを追加

build.sbtに以下を追加。

libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "2.1.0

mongodb.github.io

Observable を処理するための Helper クラス作成

コピペ from 公式チュートリアル

mongo-scala-driver/Helpers.scala at master · mongodb/mongo-scala-driver · GitHub

import java.util.concurrent.TimeUnit

import scala.concurrent.Await
import scala.concurrent.duration.Duration

import org.mongodb.scala._

object Helpers {

  implicit class DocumentObservable[C](val observable: Observable[Document]) extends ImplicitObservable[Document] {
    override val converter: (Document) => String = (doc) => doc.toJson
  }

  implicit class GenericObservable[C](val observable: Observable[C]) extends ImplicitObservable[C] {
    override val converter: (C) => String = (doc) => doc.toString
  }

  trait ImplicitObservable[C] {
    val observable: Observable[C]
    val converter: (C) => String

    def results(): Seq[C] = Await.result(observable.toFuture(), Duration(10, TimeUnit.SECONDS))

    def headResult() = Await.result(observable.head(), Duration(10, TimeUnit.SECONDS))

    def printResults(initial: String = ""): Unit = {
      if (initial.length > 0) print(initial)
      results().foreach(res => println(converter(res)))
    }

    def printHeadResult(initial: String = ""): Unit = println(s"${initial}${converter(headResult())}")
  }

}

CRUD する

import org.mongodb.scala._
import org.mongodb.scala.model.Updates._
import org.mongodb.scala.model.Filters._
import Helpers._

object HelloMongo {

  def main(args: Array[String]): Unit = {

    // connect to //localhost:27017
    val mongoClient: MongoClient = MongoClient()
    val database: MongoDatabase = mongoClient.getDatabase("helloMongo")

    // create and get collection
    database.createCollection("stocks").results()
    val stocks: MongoCollection[Document] = database.getCollection("stocks")

    // insert
    stocks.insertOne(Document("code" -> "8410", "name" -> "セブン銀行")).results()
    println("=========after INSERT=========")
    stocks.count().printHeadResult("count: ")
    stocks.find().results().foreach(println(_))

    // update
    stocks.updateOne(equal("code", "8410"), set("tradingUnit", "100")).results()
    println("=========after UPDATE=========")
    stocks.count().printHeadResult("count: ")
    stocks.find().results().foreach(println(_))

    // delete
    stocks.deleteOne(equal("code", "8410")).results()
    println("=========after DELETE=========")
    stocks.count().printHeadResult("count: ")
    stocks.find().results().foreach(println(_))

    // drop connection
    stocks.drop().results()

  }
}

実行結果

=========after INSERT=========
count: 1
Document((_id,BsonObjectId{value=59d0dea2223c756f2445f7eb}), (code,BsonString{value='8410'}), (name,BsonString{value='セブン銀行'}))
=========after UPDATE=========
count: 1
Document((_id,BsonObjectId{value=59d0dea2223c756f2445f7eb}), (code,BsonString{value='8410'}), (name,BsonString{value='セブン銀行'}), (tradingUnit,BsonString{value='100'}))
=========after DELETE=========
count: 0

コード解説

  • コレクション(RDBでいうテーブル)を作成・取得
    database.createCollection("stocks").results()
    val stocks: MongoCollection[Document] = database.getCollection("stocks")
  • ドキュメント(RDBでいうレコード)をINSERT
stocks.insertOne(Document("code" -> "8410", "name" -> "セブン銀行")).results()
  • UPDATE

    updateOne だとwhere条件にマッチした最初のドキュメントしか更新されないので注意。 where にマッチした全てのドキュメントを更新したい場合はupdateManyを利用する。
    stocks.updateOne(equal("code", "8410"), set("tradingUnit", "100")).results()
  • DELETE

    update と同様に、deleteOneだとマッチした最初のドキュメントしか削除されない。全て消す場合はdeleteMany
    stocks.deleteOne(equal("code", "8410")).results()
  • コレクションの削除

    DROP TABLE がこんなあっさりアプリケーションから出来るってのは中々面白いと思いました。 一時テーブルをアプリ側から作成→削除まで行えるのは何かと便利そうです。
    stocks.drop().results()

感想

  • mongoDB のインストールの早さと簡単さに衝撃を受けました
  • Mongo Scala Driver は公式ドキュメントやサンプルコードがちゃんと揃っているので使いやすいですね
    • ライブラリの"品質" というと api の使い勝手やパフォーマンスが重視されるけど、こういうところが地味に大事だと思う。
  • 操作自体は単純なものの、これを使ったエンティティ設計どうするんだろう...というのはやってみないと分からなそう
    • Play Framework と組み合わせてポジション管理とかやってみようと思います

以上です。思ったよりもあっさり出来てびっくりしてます。来週には動くページを作れるように頑張ります。

【Javascript】【DataTables】DataTables を利用した Bootstrap テーブルのフィルタ機能

テーブルのフィルタリングを実装

  • DataTables を使ってフィルタリング機能を実装しました

DataTables | Table plug-in for jQuery

  • フッターにセレクトボックスを利用したエクセル的なフィルタリングを出来るようになっています。

ソース

build.sbtに依存関係を追加

webJarsを利用してインポートを行います。

libraryDependencies += "org.webjars.bower" % "datatables" % "1.10.15"

html の headの中でインポート

    @Html(webJarsUtil.css("jquery.dataTables.min.css"))
    @Html(webJarsUtil.css("dataTables.bootstrap4.min.css"))
    @Html(webJarsUtil.script("jquery.dataTables.min.js"))
    @Html(webJarsUtil.script("dataTables.bootstrap4.min.js"))

DataTables を読み込み

    var todoTable = $('#todoTable').DataTable({
         initComplete: function () {
             this.api().columns().every( function () {
                 var column = this;
                 var select = $('<select><option value=""></option></select>')
                     .appendTo( $(column.footer()).empty() )
                     .on( 'change', function () {
                         var val = $.fn.dataTable.util.escapeRegex(
                             $(this).val()
                         );

                         column
                             .search( val ? '^'+val+'$' : '', true, false )
                             .draw();
                     } );

                 column.data().unique().sort().each( function ( d, j ) {
                     select.append( '<option value="'+d+'">'+d+'</option>' )
                 } );
             } );
         }
     } );

DataTables example - Individual column searching (select inputs) からコピペしています。

var todoTable = $('#todoTable').DataTable(

とすることで、テーブルをDataTableにすることができます。

                 var select = $('<select><option value=""></option></select>')
                     .appendTo( $(column.footer()).empty() )
                     .on( 'change', function () {
                         var val = $.fn.dataTable.util.escapeRegex(
                             $(this).val()
                         );

                         column
                             .search( val ? '^'+val+'$' : '', true, false )
                             .draw();
                     } );

              

html に 空のtfootタグを追加し上記スクリプトを読み込むと、各カラムのフッターにセレクトボックスを追加し、on('change')で選ばれた値と同一の値を持つレコードでフィルタ(検索→再描画)をすることが出来ます。

 column.data().unique().sort().each( function ( d, j ) {
                     select.append( '<option value="'+d+'">'+d+'</option>' )
                 } );

セレクトボックスの中身を作っているところです。各カラムのデータをuniqして、セレクトボックスを作っています。

結果

f:id:msawady:20170919220930p:plain "DOOING" を選ぶと、DOINGのTODOをフィルタすることが出来ます。 f:id:msawady:20170919221048p:plain

感想

  • なんともいえない、「これじゃない」感がありますね....
  • header 側に上手くつけたかったんですが、力及ばず...
  • kendo grid のほうが自分のイメージに合ってるので、次回はこっちへの差し替えをやってみようかなと思います。

demos.telerik.com

  • ライブラリを使いやすいようにハックする力がないと、思ったようにフロントエンドの実装を進められないなぁという小並感。
  • いやまぁ力不足以外のなにものでもないんですが....

以上です。なんとも歯がゆい一週間でした。

障害対応はエンジニアの地力である「問題解決能力」が試される

最近、障害対応で思ったことをポエムしておく

  • 自分は開発か要件定義・設計を担当することが多く、保守の仕事をあまりしてきませんでした
  • 最近、障害対応が増えたので、感想というかポエムをつぶやいておきます

障害対応は、エンジニアとしての地力が試される

障害対応のとき、エンジニアには以下のことが求められます。

  • 冷静さを保つ
  • 問題を正確に切り分ける
  • 過不足なくオペレーションを行う

冷静さを保つ

当然ですが、障害が発生したら焦ります。

場合によってはタフな時間に対応を行うことが求められます。

そのようなときでも、決してヤケにならず、悲観的になりすぎず、

  • 問題を正確に切り分ける
  • 過不足なくオペレーションを行う

の2点を遂行し続ける精神力が必要になります。若干フィジカルかつメンタルな話では有りますが、これはエンジニアのポータブルな能力の一つだと思います。

常に落ち着いている人が1人いるだけで、チームとしての落ち着きが変わります。

全体のパフォーマンス向上という面から見ても、「常に冷静」というのはとても大事なエンジニアの能力なのではないかと思います。

問題を正確に切り分ける

障害というのは当然イレギュラーな状況です。その状況においてファクトを正確に収集することが、障害対応では必要不可欠です。

  • 何がイレギュラーな状態なのか
  • 何をきっかけにイレギュラーな状態になっているか
  • 逆にどこまでがマトモな状態なのか

そしてファクトを分析し、事実関係・根本原因を推察する能力も必要です。

  • 何が根本原因なのか、何がボトルネックなのか
  • 再発するのか、するとしたら次はいつになるか
  • 何がトリガーになったのか、なぜ今まで発生しなかったのか

ファクトの収集・分析はシステムの知識を持っているエンジニアにしか出来ないことです。また、正確に仮説検証を繰り返しながら根本原因を探るという能力も試されます。

過不足なくオペレーションを行う

調査段階においても、実際の対応においても、「過不足なく」オペレーションを行うことが重要です。

オペレーションが不足してはいけないのはもちろん、過剰なオペレーションも避けなくてはいけません。

システムがイレギュラーな状態になっているので、思いもよらぬ副作用をもたらし複雑骨折する可能性が有るからです。

だからといって、何もしないわけには行きません。慎重かつ迅速に調査・復旧を行う必要があります。

  • 目的に適うオペレーションを行うこと
  • オペレーションの前提条件を確認すること
  • オペレーションのリスク、副作用を把握すること
    • リスクを軽減するための方策は無いか確認すること

上記のようなオペレーションを行うためには、問題の切り分けが出来ることが不可欠です。加えて、漏れなく影響を把握する視野の広さ、システム全般に対する広く深い理解が求められます。

まとめ

障害を対応するためには、「冷静に状況と向き合い、正確に問題を切り分け、必要十分なオペレーションをする」ことが求められます。

「プログラミング能力とは別に物凄い勢いでキーボードを叩いてプログラムを完成させてしまう能力のことではない」というツイートが話題になりました。私も同感でエンジニアの能力はあくまで「問題解決能力」だと考えています。

togetter.com

障害対応は、まさしく問題解決能力を試されるものです。勿論、障害なんて無い方が良いに決まってるんです。ただ起こってしまったとしても「これは俺のエンジニアとしての力が試されている...!」と思うと、ちょっと気分が上がって、辛さが薄れるんではないかなぁ、と思ったのでこの記事を書きました。

以上です。皆さんが今夜も?ゆっくりお休み出来ることを心よりお祈りしています。おやすみなさいませ。

【Scala】【Play Framework】Bootstrap のモーダルダイアログに書いた内容をDBにinsertする

TODOの新規追加を出来るようにする

Bootstrap を利用した Modal Dialog の追加

依存関係(tether.js)の追加

bootstrap.jsを動かすにあたり、jQueryだけでなくtether.jsも必要なので、webJarsを利用してインポート

libraryDependencies += "org.webjars.bower" % "tether" % "1.4.0"
    @Html(webJarsUtil.script("jquery.min.js"))
    @Html(webJarsUtil.script("tether.min.js"))
    @Html(webJarsUtil.script("bootstrap.min.js"))

bootstrap.js の前に jQuery, tether.js のimport を行う必要があるので注意。

Modal Dialog の追加

公式ドキュメントのサンプルコードをコピペしています。

v4-alpha.getbootstrap.com

<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addTodoModal">
   Add TODO
</button>

<div class="modal fade" id="addTodoModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLabel">Input TODO</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <label for="new-todo" class="form-control-label">Title:</label>
                    <input type="text" class="form-control" id="new-todo">
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" id="add-todo">Add TODO!!</button>
            </div>
        </div>
    </div>
</div>

コピペにあたっての注意点としては、button のdata-target属性とmodal-dialog の idは揃える必要があるよ..くらいですね^^;

サーバーへの永続化リクエストは以下のようにJavascriptを書きます.

    $('#add-todo').click(function() {
        var todo_title = $('#new-todo').val()
        if(todo_title){
            $.get('todo/add',
            {
                title: todo_title
            },
            function(){
                $('#myModal').modal('hide');
                location.reload()
            })
        }
    });

コールバック処理で、ダイアログを閉じてリロードするようにしています。Javascriptからモーダルダイアログを閉じるには以下のように書きます。

$('#myModal').modal('hide');

サーバーでの永続化処理の実装

めちゃめちゃ簡単です。Play Framework, slickともに素晴らしい…!

ルーティングの追加

conf/routes に以下を追加します。

GET     /todo/add                 controllers.TodoController.add(title)

Controller, Service, Repositoryの実装

処理をRepositoryまで持っていくだけなのであっさりと。

Controller
  def add(title: String) = Action {
    todoManager.add(title)
    Ok(todoView(todoManager.list))
  }
Service
def add(title: String): Unit = todoRepository.addTodo(new Todo(0, TodoStatus.UNDONE, title))

AUTO_INCREMENT なフィールドを持つレコード(今回はidがAUTO_INCREMENT)をinsertするときはデフォルト値として0をセットしておくと、INSERTのタイミングでよしなに採番されます。

Repository
 def addTodo(todo: Todo) = {
    database.run(Todos += todo)
  }

これで永続化できます。slick素晴らしい。

結果

Add TODOボタンを押すと f:id:msawady:20170910154958p:plain こんな感じのダイアログが出るので、Todoを入力 f:id:msawady:20170910155105p:plain ちゃんと採番されて追加されます f:id:msawady:20170910155151p:plain

感想と次にやること

  • めちゃめちゃあっさり行けたなぁという印象です。特にサーバー側はほとんど工夫してないですね。
    • Play Framework, slick, bootstrap が使いやすくて助かりました。
  • あとはDelete(論理削除), フィルタリング, メモ機能の追加あたりをやってみたい。

【Scala】【Play Framework】Slick を使ってMySQLのDBに接続する

やっとデータベースへの接続を

  • ファイルの読み書きでやっていたデータストアをデータベースにする
  • MySQLデータベースへの接続
  • ORMとしてSlickを使う

MySQLデータベースへの接続

事前にテーブル作成、初期データ挿入

create table todo(
    id bigint(20) NOT NULL AUTO_INCREMENT,
    status varchar(31) NOT NULL,
    title varchar(255) NOT NULL,
    PRIMARY KEY (ID)) AUTO_INCREMENT=10000;

insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の要件定義');
insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の設計');
insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の実装');

10000始まりのauto increment にしたい場合はcreate table の末尾にAUTO_INCREMENT=10000 をつけます。 また、insertの際はidをnullで入れることでよしなにインクリメントしてくれます。

依存ライブラリの定義

build.sbtにjdbc, slick, mysql-connectorの依存関係を追加します。

libraryDependencies += jdbc
libraryDependencies += "com.typesafe.slick" %% "slick" % "3.2.1"
libraryDependencies += "mysql" % "mysql-connector-java" % "6.0.6"

データベースの設定書き込み

conf/application.conf に接続先データベースを定義

todo-db = {
  url = "jdbc:mysql://localhost/todo_play?characterEncoding=UTF8&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"
  driver = com.mysql.jdbc.Driver
  user = "user"
  password = "password"
  connectionPool = disabled
}

url のパラメータには呪文が書かれていますが、、、

  • characterEncoding=UTF8&useUnicode=true : これは全角文字の文字化け防止
  • `useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC これをいれないとTimezoneのパースでエラーになります。
SQLException: The server time zone value '���� (�W����)' is unrecognized or represents more than one time zone.
You must configure either the server or JDBC driver (via the serverTimezone configuration property) 
to use a more specifc time zone value if you want to utilize time zone support.]

まったく理解してないですが、サーバーのtimezoneをちゃんと設定してあげる必要があるんだけど、呪文によってJDBCの設定で動くようにしているんじゃないかなーくらいに予想してます。 stackoverflow.com

RepositoryクラスでDatabaseの読み込み

import slick.driver.MySQLDriver.api._
.
.
private val database = Database.forConfig("todo-db")

MySQLDriverをインポートして、Database.forConfig("DB設定")をすることでセッションをはれます。

ORMとしてSlick利用

Entity クラスを編集

Slick によるマッピングができるよう、Domain Entityを編集します。

import slick.driver.MySQLDriver.api._

/**
  * entity class of todo
  */
case class Todo(id: Int, var status: TodoStatus, title: String)

class Todos(tag: Tag) extends Table[Todo](tag, "todo") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)

  def status = column[TodoStatus]("status")

  def title = column[String]("title")

  def * = (id, status, title) <> (Todo.tupled, Todo.unapply)

}
object Todos extends TableQuery(new Todos(_))

case classに実装されるtupled, unapply を利用してデータの展開を行いますが、自然体だと

 def status = column[TodoStatus]("status")

自作したsealed traitのTodoStatusによってunapplyができません。

sealed traitにマッパーを実装

import slick.driver.MySQLDriver.api._

sealed trait TodoStatus

/**
  * todo status
  */
object TodoStatus {

  case object UNDONE extends TodoStatus

  case object DOING extends TodoStatus

  case object DONE extends TodoStatus

  implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String](
    _.toString, TodoStatus.withName(_)
  )

  def withName(s: String) = s.toLowerCase match {
    case "undone" => UNDONE
    case "doing" => DOING
    case "done" => DONE
  }

}

こんな感じでimplicit val としてマッパーを実装してあげると、unapplyが出来るようになります。

  implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String](
    _.toString, TodoStatus.withName(_)
  )

Repository クラスの実装

Slick の力を借りることで、とてもスッキリとした形でORマッピングが可能になっています。 可読性も高く、Repository クラスがビジネスロジックの実装に集中できる形ですね。

package services.repository

import com.google.inject.Singleton
import services.domain.{Todo, TodoStatus, Todos}
import slick.driver.MySQLDriver.api._

import scala.concurrent.Await
import scala.concurrent.duration.Duration

/**
  * repository class for todo management
  *
  */
@Singleton
class TodoRepository {

  private val database = Database.forConfig("todo-db")

  def getTodoList(): List[Todo] = {
    val f = database.run(Todos.sortBy(_.id).result)
    Await.result(f, Duration.Inf).toList
  }

  def updateTodoList(ids: List[Int], status: TodoStatus) = {
    val toBeUpdated = Todos.filter(row => row.id inSetBind ids)
    database.run(toBeUpdated.map(_.status).update(status))
  }

}

Slickを利用すると、クエリの結果は全てFutureに包まれて帰ってきます。 結果を待ってからレスポンスを返すか、非同期にレスポンスを返すかは実装者の側で選択する必要があります。 ここでは、同期的に結果を待ってから返すようにしています。

    val f = database.run(Todos.sortBy(_.id).result)
    Await.result(f, Duration.Inf).toList

感想

  • あっちこっちの設定で色々とハマりましたが、なんとかデータベースとの接続が出来ました。
  • Slick は設定さえできれば、Repositoryクラスをかなり綺麗に書けて便利だなぁという印象です。
    • DDL の生成とかも出来るようなので、先にテーブルを作ってしまったのはちょっと後悔。
  • Scalaapply, unapply, implicit など、Scala特有の?暗黙的なクラスやオブジェクトの仕組みも少しずつ分かってきた気がします。
  • 次は画面からのaddを実装したいですね。

以上です。なかなか苦しみましたが、非常に勉強になりました。