しおしお

IntelliJ IDEAのことなんかを書いてます

elastic4sでcase classを使った登録と検索

elastic4sを使って↓を試してみました。

  • case classの内容をElasticsearchに登録する
  • 検索結果をcase classマッピングして取得する

build.sbt

build.sbtには、elastic4sを追加します。

val elastic4sVersion = "6.5.1"
libraryDependencies ++= Seq(
  "com.sksamuel.elastic4s" %% "elastic4s-core" % elastic4sVersion,

  "com.sksamuel.elastic4s" %% "elastic4s-http" % elastic4sVersion,
)

データの登録

  • 登録対象のデータを持つ case class を定義します
  • Indexableを継承してcase classの内容を登録用のjsonに変換します
    この例では、elastic4sで定義されているObject Mapperを使って単純にjsonに変換しています
  • データの登録では、docに登録対象のデータを持つcase classを指定します
case class Data(id: Long, name: String)

implicit object SampleIndexable extends Indexable[Data] {

  override def json(t: Data): String = JacksonSupport.mapper.writeValueAsString(t)
}

private val client = ElasticClient(ElasticProperties("http://localhost:9200"))

client.execute {
  indexInto("sample" / "_doc").id("1").doc(Data(1, "sample1"))
}.await

データの検索

  • HitReaderを継承して検索結果の内容をcase classに変換します
  • 検索結果は、toをつかてcase classに変換して受け取ります
implicit object SampleHitReader extends HitReader[Data] {

  override def read(hit: Hit): Try[Data] = {
    Try(Data(hit.sourceAsMap("id").toString.toLong, hit.sourceAsMap("name").toString))
  }
}

val result = client.execute {
  search("sample") termQuery("name", "sample1")
}.await.result.to[Data]
println(result.headOption)

実行結果

登録したデータが検索できて、Dataクラスにマッピングできてるのが確認できます。

Some(Data(1,sample1))

IntelliJ IDEA 2019.1でのJUnit実行について…

IntelliJ IDEA 2019.1からはデフォルトではGradleでテスト実行されるようになったようです。

例えばテストを実行すると、こんな感じにGradleで実行されます。 f:id:sioiri:20190330074358p:plain

実行方法の設定変更方法

Settings -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner(↓の画面) から設定を変更します。 f:id:sioiri:20190330075733p:plain

設定内容はこんな感じになってます。

  • IntelliJでテスト実行したい場合は、Platform Test Runnerを選択します
  • テスト実行のたびに選択したい場合には、Chose per testを選択します
  • Gradleでテスト実行したい場合は、Gradle Test Runnerを選択します。

JUnit5&Gradle Test Runnerの組み合わせ

JUnit5を使っている場合、build.gradleにJUnit5用の設定がないとだめなので足してあげましょう。 Gradle&Junit5にあるように、build.gradleにいかが必要となります。

test {
  useJUnitPlatform()
}

Dockerコンテナ上で動いているJavaアプリケーションをIntelliJさんからデバッグしてみる

IntelliJ IDEA 2019.1 Release Candidate is Out! | IntelliJ IDEA Blogにあるように、 2019.1 からの新機能ですね。

Dockerfileの準備

デバッグ対象のアプリケーションをDockerで動かすためのDockerfileを作ります。
※docker-compose.ymlでもデバッグいけるみたいです。

とりあえず、デバッグできることだけを確認したいので、シンプルにこんな感じにしています。

FROM java:8

COPY build/libs/sample.jar .

CMD $JAVA_HOME/bin/java -jar sample.jar

実行構成の作成

  • 実行構成の追加からRemoteを選びます

f:id:sioiri:20190328085903p:plain

  • ポート番号やJavaのバージョンなどの設定を行います
    あとで必要になるので、Command line arguments for remote JVMの内容はコピーしておきましょう

f:id:sioiri:20190328090053p:plain

  • Before launchLaunch Docker before debugを選びます

f:id:sioiri:20190328090256p:plain

  • Launch Docker before debug の設定を行います
  • Custom Commandには、コピーしたCommand line arguments for remote JVMの内容を使ってリモートデバッグ可能な状態でアプリケーションを起動するようコマンドをを設定します((Custom Commandの設定、再度設定画面開くと消えちゃってますがおそらく設定自体は生きています))
  • 今回は、 java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar sample.jarと設定しています

f:id:sioiri:20190328090543p:plain

デバッグ実行してみる

こんな感じにコンテナ上で実行されているアプリケーションに対するデバッグが行なえます。 f:id:sioiri:20190328100035g:plain

・・・

Custom Command での上書き設定がちょっとめんどくさい感じですね

Spring Bootなアプリケーションのキャッシュの内容を定期的にクリアしてみる

@Scheduled アノテーションを使ってタスクを定期実行し、キャッシュの値を定期的にクリアしています。

設定値(application.properties)

  • 簡単に確認出来るインメモリキャッシュを設定しています
spring.cache.type=simple

サンプルコード

  • @Scheduled で10秒毎にキャッシュをクリアするタスクを実行しています
  • 同時に@CacheEvictを設定して、削除対象のキャッシュ名とすべてのエントリを削除するようにしています
@SpringBootApplication
@EnableCaching
@EnableScheduling
class CacheDemoApplication

fun main(args: Array<String>) {
    runApplication<CacheDemoApplication>(*args)
}

@RestController
@RequestMapping("/sample")
class SampleController(private val sampleService: SampleService) {
    val logger = LoggerFactory.getLogger(SampleController::class.java)
    @GetMapping
    fun toUpper(@RequestParam("value") value: String): String {
        logger.info("value: $value")
        return sampleService.toUpper(value)
    }

}

@Service
class SampleService {

    val logger = LoggerFactory.getLogger(SampleService::class.java)

    @Cacheable("toupper")
    fun toUpper(value: String): String {
        TimeUnit.SECONDS.sleep(1)
        val upper = value.toUpperCase()
        logger.info("$value --> $upper")
        return upper
    }
}

@Component
class SampleClearCache {

    val logger = LoggerFactory.getLogger(SampleClearCache::class.java)

    @Scheduled(fixedRateString = "PT10S", initialDelayString = "PT10S")
    @CacheEvict(value = ["toupper"], allEntries = true)
    fun clearCache(): Unit {
        logger.info("clear cache")
    }
}

実行結果

  • 初回はServiceが実行され、それ以降はServiceの実行が行われずキャッシュの値が使われていることがわかります
  • スケジュールなタスクでキャッシュのクリアが行われると、Serviceが実行されていることがわかります
  • スケジュールなタスクが指定された時間(10秒)毎に実行されていることがわかります
2019-03-23 07:28:07.930  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:08.931  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleService           : a --> A
2019-03-23 07:28:09.220  INFO 26010 --- [nio-8080-exec-8] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:09.993  INFO 26010 --- [nio-8080-exec-9] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:10.665  INFO 26010 --- [io-8080-exec-10] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:11.680  INFO 26010 --- [nio-8080-exec-2] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:12.399  INFO 26010 --- [nio-8080-exec-1] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:13.220  INFO 26010 --- [nio-8080-exec-3] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:14.013  INFO 26010 --- [nio-8080-exec-4] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:14.820  INFO 26010 --- [nio-8080-exec-5] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:15.703  INFO 26010 --- [   scheduling-1] siosio.cachedemo.SampleClearCache        : clear cache
2019-03-23 07:28:15.715  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:16.716  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleService           : a --> A
2019-03-23 07:28:17.837  INFO 26010 --- [nio-8080-exec-8] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:20.011  INFO 26010 --- [nio-8080-exec-9] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:25.703  INFO 26010 --- [   scheduling-1] siosio.cachedemo.SampleClearCache        : clear cache

JUnit5でTempDirを使って一時ディレクトリを扱う

JUnit4のTemporaryFolder的なやつですね。5.4から追加されて、アノテーションEXPERIMENTAL とあるのでまだ実験的な機能のようです。

サンプル

テストメソッド単位に一時ディレクトリを使う

  • テストメソッドの引数として java.nio.file.Path を受け取り、 org.junit.jupiter.api.io.TempDir アノテーションを設定します
  • java.nio.file.Path は一時ディレクトリを示すパスになっています
@Test
internal fun sample1(@TempDir tempDir: Path) {
    val file = tempDir.resolve("test.txt").toFile()
    file.writeText("test")
    
    assertThat(file.readText())
            .isEqualTo("test")
}

テストクラス内のすべてのテストメソッドで一時ディレクトリを使う

class TempDirSample {
    
    @TempDir
    lateinit var tempDir: Path

    @Test
    internal fun sample1() {
        println("tempDir = ${tempDir}")
    }

    @Test
    internal fun sample2() {
        println("tempDir = ${tempDir}")
    }
}

IntelliJ IDEAのDomaプラグイン(Doma Support)をKotlin対応したよ

こんなツィートを見かけたので、Doma SupportにKotlin対応追加してみました。

KotlinのDaoで出来ること

  • Daoのメソッドに対するSQLファイルの存在チェック
  • SQLファイルが存在しない場合、QuickFixでSQLファイルを作る機能
  • DaoからSQLファイルへの移動

JavaでDaoを作ったときと比べるとできることは少ないです

動作イメージ

f:id:sioiri:20190309072722g:plain

インストール方法

Kotlin対応版の1.0.2をインストールすることで利用できます。

プラグインが承認されるまでは、プラグインサイトからインストールできないのでRelease 1.0.2 · siosio/DomaSupport · GitHubからダウンロードしてインストールしていただければと。

KtorでDoma2を使ってデータベースアクセスしてみた

Ktor - asynchronous Web framework for KotlinでDoma2を使ってデータベースアクセスしてみました。 Ktorの公式サイト上にはデータベースアクセスする方法などが全く無いので、これが正解かどうかはわかりませんが…

build.gradle

  • データベースアクセスに必要となるライブラリを追加します
dependencies {
  kapt 'org.seasar.doma:doma:2.22.0'
  implementation 'org.seasar.doma:doma:2.22.0'
  runtime 'org.postgresql:postgresql:42.2.5'
  implementation 'com.zaxxer:HikariCP:3.3.1'
}
  • Doma2でkaptが必要になるので、kaptプラグインを追加します
apply plugin: 'kotlin-kapt'

データベースの接続先を定義する

  • resources/application.confにデータベースの接続先などを定義します
database {
    driverClass = org.postgresql.Driver
    url = "jdbc:postgresql://localhost:5432/siosio"
    user = siosio
    password = siosio
}

DomaのConfigクラスを追加する

  • トランザクション — Doma 2.0 ドキュメントを参考にシングルトンなコンフィグを作ります
    Kotlinなので、objectで作ってsingleton@JvmStaticにしています
  • initで、HikariCPのDataSourceを作成します
    接続先などの情報は、resources/application.conf からとってきます
@SingletonConfig
object DomaConfig : Config {
    
    private lateinit var ds: LocalTransactionDataSource
    
    fun init(environment: ApplicationEnvironment) {
        val hikariConfig = HikariConfig()
        hikariConfig.driverClassName = environment.config.property("database.driverClass").getString()
        hikariConfig.jdbcUrl = environment.config.property("database.url").getString()
        hikariConfig.username = environment.config.property("database.user").getString()
        hikariConfig.password = environment.config.property("database.password").getString()
        ds = LocalTransactionDataSource(HikariDataSource(hikariConfig))
    }

    override fun getDialect(): Dialect = PostgresDialect()

    override fun getDataSource(): DataSource {
        if (::ds.isInitialized.not()) {
            throw IllegalStateException("database setting is not initialized")
        }
        return ds
    }

    override fun getTransactionManager(): TransactionManager {
        return LocalTransactionManager(ds.getLocalTransaction(jdbcLogger))
    }

    @JvmStatic
    fun singleton(): DomaConfig = DomaConfig
}

Moduleの定義タイミングでDataSourceを作成する

  • DomaConfig#initを呼び出してDataSourceを作成します
fun Application.module(testing: Boolean = false) {
    // 省略

    DomaConfig.init(environment)
    
    routing {
        user()
    }
}

Domaを使いやすくするヘルパーメソッドを追加する

  • Daoの実装クラス(DaoImpl)を取得するメソッドを追加します
  • transactionを実行するためのメソッドを追加します
inline fun <reified T> dao(): T {
    return Thread.currentThread().contextClassLoader
            .loadClass("${T::class.qualifiedName}Impl")
            .newInstance() as T
}

fun <T> PipelineContext<out Any, out Any>.transaction(block: () -> T): T {
    return DomaConfig.transactionManager.required(block)
}

データベースアクセス処理を実装する

  • 先程定義したtransactionメソッドに渡したブロック内でデータベースアクセスを行います
  • 確認用に登録処理と、登録したデータを一括取得する処理を実装しています
fun Route.user(): Unit {
    get("/users") {
        val userList = transaction {
            dao<UserDao>().findAll()
        }
        call.respond(userList)
    }

    post("/users") {
        val user = call.receive(UserEntity::class)
        transaction {
            dao<UserDao>().insert(user)
        }
        call.respond(HttpStatusCode.Created)
    }
}

アプリケーションを実行して確認してみる

データの登録処理

登録処理を呼び出して、2件データを登録してみます

~ ❮❮❮ curl -H 'Content-Type: application/json' -d '{"name": "hoge"}' -D - http://localhost:8080/users
HTTP/1.1 201 Created
Content-Length: 0

~ ❯❯❯ curl -H 'Content-Type: application/json' -d '{"name": "fuga"}' -D - http://localhost:8080/users
HTTP/1.1 201 Created
Content-Length: 0

データの取得処理

一括取得を呼び出すと登録したデータが返されることが確認できます

~ ❯❯❯ curl  -D - http://localhost:8080/users
HTTP/1.1 200 OK
Content-Length: 72
Content-Type: application/json; charset=UTF-8

[ {
  "id" : 2,
  "name" : "hoge"
}, {
  "id" : 3,
  "name" : "fuga"
} ]

データベースの確認

登録した2件のデータが確認できます。ちゃんと動いていますね。

siosio=# select * from users;
 id | name 
----+------
  2 | hoge
  3 | fuga
(2 rows)