GWに読む本を検討する
GWに東京に来てから初めて関西に帰る。
んだけど、多分暇だからGW中にこれらを読破するってのを決めようと思う。
- サーバ/インフラを支える技術
- 理論から学ぶデータベース実践入門
- これは積読になってる
- 初めてのAnsible
- SOFT SKILLS
- システムテスト自動化 標準ガイド
- ゼロから作るDeep Learning
気になるのがあったら追加していこ。
Spring BootでRequest Bodyをログに出力する
API serverをSpring Bootで作っていて、リクエストボディ(JSON文字列)をログに吐こうとしてちょっと悩んだのでメモ。
問題
Interceptor
で実装しているロギング処理でリクエストボディを取得しようとすると以下の問題にぶつかった。
HttpServletRequest#getReader
はIllegalStateException
を投げるHttpServletRequest#getInputStream#readLine
とかで読むと、Controller
でリクエストボディがないと言われるServletInputStream
はreset
に対応していない
辛い。
解決策
ググってると、HttpServletRequest
をラップして、リクエストボディを保持しておいて、そこから都度InputStream
を作ればええねん!
ってのを見つけたので、実装。例によってKotlinで書いている。
private class MyServletInputStream(bytes: ByteArray) : ServletInputStream() { private val inputStream: ByteArrayInputStream = ByteArrayInputStream(bytes) override fun read(): Int = inputStream.read() override fun read(b: ByteArray?, off: Int, len: Int): Int { return inputStream.read(b, off, len) } override fun isReady(): Boolean = inputStream.available() != 0 override fun setReadListener(listener: ReadListener?) {} override fun isFinished(): Boolean = inputStream.available() == 0 } private class MyServletRequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) { private val bytes: ByteArray = ByteArrayOutputStream().use { request.inputStream.copyTo(it) it.toByteArray() } override fun getInputStream(): ServletInputStream { return MyServletInputStream(this.bytes) } }
ServletInputStream
の実装クラスのread
以外のメソッドは適当に書いた。
どう使うか
Interceptor
より前に処理が走るFilter
で、HttpServletRequst
をMyServletRequestWrapper
でラップして、FilterChain#doFilter
の引数として渡す。Interceptor
内で、HttpServletRequest#getInputStream#readLine
とかで読んでログに吐く- この
HttpServletRequest
の実態はMyServletRequestWrapper
- この
終わり
JetBrains IDE Supportの警告を消す方法
Chrome ExtentionにJetBrains IDE Supportというものがあります。
リアルタイムでHTML/CSS/JavaScriptがブラウザに反映されてデバッグまでできるなかなか良い拡張機能です。
なのですが、デフォルトだとChromeのバナーの下に黄色い警告が出ます。
なかなか邪魔だ。
ということで
chrome://flags/#silent-debugger-extension-api
をアドレスバーにコピペして、遷移先にあるサイレントデバッグっていうのを有効にすると消えてくれた。
これでまた一つIntelliJ IDEAが快適になった。めでたしめでたし。
HomebrewでNode.jsを入れる方法とエラーの解決方法
新しいMacにNode.js
を入れた際の手順メモ
Node.js
, Nodebrew
を削除
$ brew uninstall node
$ brew uninstall nodebrew
Nodebrewのインストール
$ brew install nodebrew
PATHを設定
$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bashrc
PATHの反映のため再読み込み
$ source ~/.bashrc
安定版のNode.js
のインストール
$ nodebrew install-binary stable
をするとエラーになった。以下の通り。
curl: (23) Failed writing body (0 != 941) download failed: https://nodejs.org/dist/v6.5.0/node-v6.5.0-darwin-x64.tar.gz
ググった結果、ディレクトリがないらしいので作成
$ mkdir -p ~/.nodebrew/src
再度Node.js
のインストール
$ nodebrew install-binary stable
Fetching: https://nodejs.org/dist/v6.5.0/node-v6.5.0-darwin-x64.tar.gz ... Installed successfully
OK!
使うバージョンを指定
$ nodebrew use stable
インストールされていることを確認
$ node -v
バージョンが表示されたら完了
PATHを設定
$ echo 'export PATH=$PATH:/Users/USER_NAME/.nodebrew/current/bin' >> ~/.bashrc
炎上プロジェクトを振り返る
自分の備忘録というか戒めです。
9月から東京のとある会社で働く。
つまり前の会社を辞めた訳んだけど、
最後に関わった改修案件がよく燃える案件だったので何がダメだったのか・どうすべきだったかを振り返っておこうと思う。
概要
- 既存業務システムの機能追加
- 3月スタート、8月納品
だった - 一つ一つは大したことがないが、数が多く全体でみると大きな案件だった
- メンバーは10人くらい(自分とリーダーがメイン)
- Java6, Struts, ibatis, freemarker, JavaScript(pure)など
- ドキュメントはもちろんExcel
何が起きたか
- 大量の設計漏れ・仕様追加/変更による実装工程の遅れ
- 試験書が仕様通りでないのでスムーズに試験ができない
- 当たり前だけど不具合の頻出
- 再三のデグレ
- 追い討ちの追加対応
オワタ... orz
と言ってても仕方がないので、同じような案件に巻き込まれないようにミスを犯さないように一つずつ振り返る。
大量の仕様漏れ・仕様追加/変更による実装工程の遅れ
仕様追加・変更については、お客さんとの折衝はしていないのでちょっとアレな部分があるが、やはり設計段階で詰めるべきだった仕様についてが多かった。
また、実装段階で気づいて決めた仕様が多すぎた。
そして設計不十分の影響は最後の最後までこの案件に大きな影を落とすことに。
これに関しては設計力の不足を思い知った。
業務知識に寄る部分が大きかったが、なんとなくわかったような気がしたくらいで設計をしていたような気がする。まぁこれでいけるやろ的な。
完全に漏れ無く完璧には無理かもしれないけど、ウォーターフォールで進めていた以上、もっと精度を上げていかないとお話にならない感。今後ウォーターフォールでやるかは知らない
チーム的な問題としては、業務知識を蓄えている人(=同じ現場にいる年数が長い人)がリーダー一人しかいなかったこと。
そして、その人に設計書・試験書のレビューが集中してしまったことがある。
一番わかっている人がレビューをするのは理にかなっていると思うけど、今回は数が多すぎたため一人で見切ろうとするとどうしても漏れが出る。
別チームの人でも、なんとかお願いして作業分担をしてもらうべきだった。
本当は自分自身が出来ると良かったのだけど、業務経験は1年くらいで、今回改修する部分については今までほとんど触ったことがなかった部分だった。
とはいえ、レビューなんかできませんなんてはずはなかったので、比較的業務知識に寄らない部分の作業の切り出しを自分から行うべきだったと思う。
試験書が仕様通りでないのでスムーズに試験ができない
設計書作成 → 試験書作成 → 実装 → 単体試験 → 結合試験
というフローで進めていたが、実装段階で仕様をいじりまくったおかげで、実装(=仕様)と試験書と設計書がそれぞれ乖離する状況になってしまった。
また、試験の少し前くらいにメンバーが諸事情で離脱。ほとんど経験のない人にテスターをお願いすることに。
試験書の通りに操作してもその通りの結果にならないし、設計書を見てもまた違うことが書いてあるしと、大混乱。テスターの不満も不具合件数も山積み...。
また、ある程度業務知識がある人が試験をする前提で試験書を作ることが通例となっていたため、ことあるごとにテスターの方に説明をしなければならず実装者もテスターも時間を奪われるという最悪の状況。
さらに上がった不具合から、仕様通りの動作なのか不具合なのかを実装者が都度判断しなければならず、見積もりを立てた時点で積んでいたバッファは単体試験の工程で無くなり利益を食う状況に。
これで悪かったのは、実装段階で追加・変更となった仕様をドキュメントに反映していなかったこと。そのルール・仕組みがなかったこと。
仮に、実装段階で見つけた仕様漏れや、仕様の追加・変更は設計書・試験書に反映してから実装を行わなければならないというルールが決まっていて、メンバー全員が理解していたなら、実装工程はもう少し遅れたかもしれないけども、その後の混乱や無駄な時間の発生は防げた。
でもどうやったら効率的なその仕組みが作れるかは思い至っていないところ。
こういうトラブルや課題が発生した時にどういう対処を取るかっていうのは、チームメンバーで意識を合わせておかないといけない部分だったんだろうなと。
ある意味納得意外だったのは、もっと誰でも試験できるような試験書を作るべきだったのでは?っていう意見を出したらテスターの方以外から賛成が得られなかったこと。
そんなにコストのかかるようなものではない気がするんだけど、今までのを変えるのはなかなか受け入れられないのだなと...。
当たり前だけど不具合の頻出
まぁ、上の2つを鑑みると当たり前。 もう一つの原因としては、Javaをメインで仕事をしている人たちばかりのチームだったのに、JavaScriptを使ってフロントでごちゃごちゃやりすぎたこと。
諸般の事情によりあまりプラグインや拡張機能を使えない状況で作業をしていたので、変数や関数のタイポなんてしょっちゅう。
そんな状況で1000行とか書くもんだから何が何やら、書いた本人でさえ理解できない状況に。しかも中には昔のコードからコピペしたようなものもあって、そのコードの質も良くない...。
そんなわけで、設計段階でこの処理はフロントで、これはサーバーでと設計をするわけだけども、チームのスキルの志向にあった設計をしなければ泣きを見ることがわかったよね。
もちろん、機能面でこれはフロントで、サーバーでやらなければいけないっていうのはあるので、そこは仕方ないけれどできるだけチームのスキルセットにあった設計をしよう。
再三のデグレ
不具合修正によるデグレ、不具合修正したソースに追加対応したソースをマージした際のデグレ、いつ起きたのかわからないデグレ...。
その度に手動による再試験。そこで別の不具合やデグレを見つけてまた再試験。その繰り返し。インフィニティ。
もうテストコード書けよとしか。
そしてなぜ自分は途中でテストコードを書くのをやめてしまったのか。
UIのテストまで書けとは言わないけど、大事なロジックのテストコードぐらい書いておけよ。
それで見つかる不具合・デグレがどれだけあったか。
確かに、テストコードを書くような工数はお金はもらっていないから書かないっていうのはわかるんだけど、それはある程度までの規模の小さな案件の場合に有効で、今回のように全体として大きい場合はお金をもらえなくても書いたほうが時間を回収できるし、利益が上がる。
お客さん的にも不具合件数が下がって、信頼できるようになるはず。
今回、一人でもテストコードを書くスタイルを崩すべきではなかった。
そうすれば、自分の書いた部分だけでも不具合・デグレは少なくなって、他の部分に割ける時間が増えただろうに。
ちなみに一人で書いてたのは誰もJUnitの書き方を知らなかったから。
追い討ちの追加対応
これはもう本当に勘弁してとしか...。
お客さんも自分たちも幸せになれないものに対しては、断る勇気も持とう。
最後に
もう全行程ダメダメで、二度とやりたくない。本当に。
でも設計段階で少し抜いてたのが全ての始まり。
不具合が多発した時も、それぞれにプライオリティ付けて処理をしなかったのが悪い。
影響範囲を対して検討もせずにその場しのぎな方法で直してたが悪い。
ドキュメントを軽視していたのが悪い。
同じことは繰り返さないようにしよう。
以上。
Kotlin+SpringBootでWebアプリを作ってみる その3
やること
Kotlin+SpringBootでWebアプリを作ってみる その2.1の続きです。
結構間が空いてしまったな...。
今回は、
・Flywayの導入
・DBUnitの導入
をやります。というかやりました。
前回の終わりに書いた次やることとは全然違うけど気にしない。
次で書きます。
ソースはGitHubにあげてあります。
Flywayの導入
ちょっとやってみたかったので、DBマイグレーションツールのFlywayを導入します。
DBマイグレーションツールってのはデータベースの状態をバージョン管理するやつですね。
DB環境生成や移行がやりやすくなったりします。どんな順番でSQLを実行するのかとか、パッチ用SQLを適用してるのかとか全部マイグレーションツールがやってくれます。
仕事でも使いたいですね。
とりあえず現状あるテーブルを管理対象としてやってみました。
かなり簡単です。
build.gradle
dependencies { ... compile('org.flywaydb:flyway-core:4.0') }
依存関係にFlywayを追加しました。バージョンは4.0。
たぶん一番新しいreleaseだと思います。
以上。
V1__createSchema.sql
DROP TABLE IF EXISTS author_book; DROP TABLE IF EXISTS category_book; DROP TABLE IF EXISTS publisher_book; DROP TABLE IF EXISTS book; DROP TABLE IF EXISTS author; DROP TABLE IF EXISTS category; DROP TABLE IF EXISTS publisher; CREATE TABLE IF NOT EXISTS `book` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `image_path` varchar(255) NOT NULL, `leading_sentence` varchar(255) NOT NULL, `sub_title` varchar(255) DEFAULT NULL, `title` varchar(255) NOT NULL, `url` varchar(255) NOT NULL, `like_count` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `author` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `like_count` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `like_count` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `publisher` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `like_count` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `author_book` ( `author_id` bigint(20) NOT NULL, `book_id` bigint(20) NOT NULL, PRIMARY KEY (`author_id`,`book_id`), KEY `book_id` (`book_id`), CONSTRAINT `author_book_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`), CONSTRAINT `author_book_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `category_book` ( `category_id` bigint(20) NOT NULL, `book_id` bigint(20) NOT NULL, PRIMARY KEY (`category_id`,`book_id`), KEY `book_id` (`book_id`), CONSTRAINT `category_book_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`), CONSTRAINT `category_book_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `publisher_book` ( `publisher_id` bigint(20) NOT NULL, `book_id` bigint(20) NOT NULL, PRIMARY KEY (`publisher_id`,`book_id`), KEY `book_id` (`book_id`), CONSTRAINT `publisher_book_ibfk_1` FOREIGN KEY (`publisher_id`) REFERENCES `publisher` (`id`), CONSTRAINT `publisher_book_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Flywayでは、src/main/resources/db/migration
以下のSQLファイルをDBへの適用対象とします。
バージョンごとにSQLファイルを作って管理をするので、ファイル名に規則がありバージョン番号は一意でないとだめです。まぁ当たり前か。
ファイル名はV
からはじめて、バージョン番号(数字と.
、_
が使える)、区切り文字の__
(アンスコ2つ)、説明。
SQLファイルの中は普通にSQL書くだけです。
マイグレーションを実行するとDBに未適用のバージョンのSQLが実行され、適用済みのSQLはFlywayが判断してスキップしてくれます。
Flywayはこれで終わりです。簡単。
これだけでDBが変わっても自動で環境を作ってくれます。
今回はテーブルの作成しか行っていないですが、データのインサートや構造の変更ももちろんできます。
そのへんはSpring Securityを導入した際に使ってみます。
DBUnitの導入
そろそろぐだぐだなテストをなんとかしようと思って、導入しました。
DBUnitはデータベースを操作するクラスのテストをするためのフレームワークですね。
仕事でも使いたい。
CSVファイルかxmlファイル(xlsファイルも?)でデータを作成して、テスト実行時に必要なデータを入れたり消したりしてくれます。
毎回手動でデータを用意しなくても大丈夫なので、テストを何度でも気軽に実行できるようになります。神。
それにSpring Bootはsrc/test/resources/
直下にapplication.yml
かapplication.properties
を置くとテスト時はそちらを読んでくれるので、datasource
を簡単に切り替えられます。
加えて、上記でFlywayを導入したのでテーブルの生成などなども自動でやってくれるのでテストやりたい放題ですね。ほんとに仕事で使いたい。
とりあえずbuild.gradle
から書いていこう。
build.gradle
dependencies { ... testCompile('org.dbunit:dbunit:2.5.1') }
依存関係にDBUnitを追加。以上。
application.yml
spring: # DataSource datasource: url: jdbc:mysql://localhost/book_application_test username: user password: user driver-class-name: com.mysql.jdbc.Driver # JPA jpa: show-sql: true hibernate: ddl-auto: validate
テスト用のdatasource
を書いてsrc/test/resources/
直下に。
これでテスト時はbook_application_test
を読みに行ってくれます。
そういえばhibernate.ddl-auto
はDDLをFlywayがやってくれるのでvalidate
かnone
でいいと思います。
TestDataResources.kt
package book.application.test import org.dbunit.database.DatabaseConnection import org.dbunit.dataset.csv.CsvDataSet import org.dbunit.operation.DatabaseOperation import org.junit.rules.ExternalResource import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component import java.io.File import javax.sql.DataSource @Component class TestDataResources : ExternalResource() { @Autowired lateinit private var dataSource: DataSource override fun before() { var con: DatabaseConnection? = null try { // テストデータのインサート con = DatabaseConnection(dataSource.connection) DatabaseOperation.CLEAN_INSERT.execute(con!!, CsvDataSet(File("src/test/resources/testData"))) }finally{ con?.close() } } override fun after() { // 特にやることがないな... } }
今回はJUnitの@Rule
アノテーションを使います。なのでそれ用のクラスを作成しました。
@Component
でBean
登録して、テストクラスにAutowired
します。
やってることは見た通り、テスト実施前にインサートをするだけです。
CLEAN_INSERT
を使うことで、DELETE_ALL
とINSERT
を一括で行ってくれます。
テスト用のDBなので毎回データを消しても問題ないのでこれを使います。
execute
メソッドの第二引数にデータを記述したファイルのあるパスを設定したDataSet
を渡すと、パス直下のファイルの内容をインサートしてくれます。
今回はCSVファイルで作ったのでCsvDataSet
を使っています。
YAMLでも頑張ればいけそうな気がしたんですが、YAMLで同じ構造のデータを何度も書くのってどうなんだろと思ったのでやめました。
XMLファイルは見にくい書きにくいで出来れば書きたくないし、Excelは毎日格闘してるので触りたくないしで、もうCSVしかないじゃんって感じですね。
BookServiceTest.kt
package book.application.service import book.application.BookApplication import book.application.test.TestDataResources import org.junit.Assert.* import org.junit.BeforeClass import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.SpringApplicationConfiguration import org.springframework.test.context.junit4.SpringJUnit4ClassRunner import org.springframework.test.context.web.WebAppConfiguration import javax.transaction.Transactional import org.hamcrest.CoreMatchers.`is` as be @RunWith(SpringJUnit4ClassRunner::class) @SpringApplicationConfiguration(BookApplication::class) @WebAppConfiguration open class BookServiceTest{ @Rule @Autowired lateinit var testDataResources: TestDataResources @Autowired lateinit var bookService: BookService @Test @Transactional fun findAllBookTest() { var sutList = bookService.findAllBook() var sut = sutList[0] assertThat(sutList.size, be(10)) assertThat(sut.id, be(1L)) assertThat(sut.title, be("title")) assertThat(sut.subTitle, be("subTitle")) assertThat(sut.leadingSentence, be("leadingSentence")) assertThat(sut.imagePath, be("imagePath")) assertThat(sut.url, be("url")) assertThat(sut.authors[0].name, be("author")) assertThat(sut.likeCount, be(0)) assertThat(sut.categories[0].name, be("category")) assertThat(sut.publisher.name, be("publisher")) } }
改変したテストクラス。@Rule
に上記のTestDataResources
をAutowired
したくらいであんまり変わってないです。
でもこれで毎回データを用意しなくてもテストが実行可能になりました。
テストが自動で行えるってことはリファクタリングできるってことですからね、最高ですね。
あとはCSVにデータを書いていくだけです。冗長なので、ここまで飛ばしてもらっても。
csvファイルたち
book.csv
id,title,sub_title,leading_sentence,image_path,url,like_count 1,"title","subTitle","leadingSentence","imagePath","url",0 2,"title2","subTitle2","leadingSentence2","imagePath2","url2",0 3,"title3","subTitle3","leadingSentence3","imagePath3","url3",0 4,"title4","subTitle4","leadingSentence4","imagePath4","url4",0 5,"title5","subTitle5","leadingSentence5","imagePath5","url5",0 6,"title6","subTitle6","leadingSentence6","imagePath6","url6",0 7,"title7","subTitle7","leadingSentence7","imagePath7","url7",0 8,"title8","subTitle8","leadingSentence8","imagePath8","url8",0 9,"title9","subTitle9","leadingSentence9","imagePath9","url9",0 10,"title10","subTitle10","leadingSentence10","imagePath10","url10",0
author.csv
id,name,like_count 1,"author",0 2,"author2",0 3,"author3",0 4,"author4",0 5,"author5",0 6,"author6",0 7,"author7",0 8,"author8",0 9,"author9",0 10,"author10",0
category.csv
id,name,like_count 1,"category",0 2,"category2",0 3,"category3",0 4,"category4",0 5,"category5",0 6,"category6",0 7,"category7",0 8,"category8",0 9,"category9",0 10,"category10",0
publisher.csv
id,name,like_count 1,"publisher",0 2,"publisher2",0 3,"publisher3",0 4,"publisher4",0 5,"publisher5",0 6,"publisher6",0 7,"publisher7",0 8,"publisher8",0 9,"publisher9",0 10,"publisher10",0
author_book.csv
author_id,book_id 1,1 2,2 3,3 4,4 5,5 6,6 7,7 8,8 9,9 10,10
category_book.csv
category_id,book_id 1,1 2,2 3,3 4,4 5,5 6,6 7,7 8,8 9,9 10,10
publisher_book.csv
publisher_id,book_id 1,1 2,2 3,3 4,4 5,5 6,6 7,7 8,8 9,9 10,10
それはさておき、DBUnitではファイル名でテーブル名を、一行目でカラム名を指定します。
あとはデータを詰めるだけ。
ちなみにauto_increment
やdefault
を指定しているようなカラムでもデータを空にしておくとエラーを吐きます。
カラム自体を書かなかったらうまくいくのかもしれません。(試してない)
table-order.txt
book author category publisher author_book category_book publisher_book
このファイルでデータをインサートする順番を制御できます。
データファイルと同じディレクトリ(今回だとsrc/test/resources/testData/
直下)に置くとDBUnitが読んで、順に実行してくれます。
このアプリケーションでいうと、author_book
テーブルにはbook
テーブルと対応するauthor
テーブルのid
しか入らないので、先にbook
,author
テーブルにデータをインサートしないといけません。
そういう順序の制御をします。
これでDBUnitの導入も終了です。割と簡単。
所感
テスト自動化は神。
テストコードを書こう。
次やること
次こそページネーション。
あとはSpring Securityを入れます。
もうやってあるんですけどね...。
Kotlin+SpringBootでWebアプリを作ってみる その2.1
発端
Kotlin+SpringBootでWebアプリを作ってみる その2でテストコードでLazyInitializationException
が出てはまったって話を書きました。
すると、槇さんがSpringの永続コンテキストのライフサイクルを教えてくださりました。
ありがたい... ということで書いておこうと思います。
Springにおける永続コンテキストの話
以下槇さんからいただいたツイート
@KissyBnts 通常Webだと境界はサービスになるので、viewからアクセスするといちいちエラーになるのが面倒で、Springだとインターセプタで永続コンテキストの境界をリクエストに広げています(Bootでは自動設定されます)。なので、Webでは動いてテストでこけていました
— Toshiaki Maki (@making) April 3, 2016
なるほど…。Spring Bootの庇護下になかったからなのか…。
つまりBookService.findAllBook()
の呼び出し終了時にEntityManager
をSpringが閉じてしまってるから、遅延初期化でエラーを吐いていたのですね。
で、@Transactional
をメソッドにアノテートすることでEntityManager
が閉じるのをメソッド単位にすると。するとメソッド内で遅延アクセスをしても問題無し。
ちなみに@Transactional
をクラスもしくはメソッドに付与するとfinal
やめろエラーが吐かれるのでopen
をつけましょう。
BookServiceTest.kt
package book.application.service import book.application.BookApplication import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.SpringApplicationConfiguration import org.springframework.test.context.junit4.SpringJUnit4ClassRunner import org.springframework.test.context.web.WebAppConfiguration import javax.transaction.Transactional import org.hamcrest.CoreMatchers.`is` as be @RunWith(SpringJUnit4ClassRunner::class) @SpringApplicationConfiguration(BookApplication::class) @WebAppConfiguration open class BookServiceTest{ @Autowired lateinit var bookService: BookService @Test @Transactional fun findAllBookTest() { var sutList = bookService.findAllBook() var sut = sutList[0] assertThat(sutList.size, be(1)) assertThat(sut.id, be(1L)) assertThat(sut.title, be("title")) assertThat(sut.subTitle, be("subTitle")) assertThat(sut.leadingSentence, be("leading")) assertThat(sut.imagePath, be("imagePath")) assertThat(sut.url, be("url")) assertThat(sut.authors[0].name, be("author")) assertThat(sut.likeCount, be(0)) assertThat(sut.categories[0].name, be("category")) assertThat(sut.publisher.name, be("publisher")) } }
@Transactional
をクラスにアノテートしても、メソッドにアノテートしても動作します(入れ知恵)。
@KissyBnts テストメソッドかクラスに@ Transactinal付けてみてください
— Toshiaki Maki (@making) April 3, 2016
読もう
@KissyBnts 一連のツイートを読んだ後、https://t.co/IVfabSgKQO
— Toshiaki Maki (@making) April 3, 2016
を読めばなんとなく理解できるかと思います
所感
Spring Bootがやってくれてるから問題ないけど、わかってないことたくさんあるんだろうなぁって感じですね。
これからもちょくちょくはまりそうだな…。
にしても、わからないなーと思ってた部分を拾っていただけて教えていただけるなんて、ほんとにありがたいことです。
もっとインプット・アウトプットして少しでも恩を返せるようになれろうと思いながら書いていました。
槇さん、ありがとうございました!
そして僕は明日の仕事終わりにはじめてのSpring Bootを買いに行こうと決めました…。
GitHubにRepository作った
ここです。以上。