Posts for: #Java

dto に対するリフレクションの是非

1時に寝て7時に起きた。

ドラクエタクトのリアルタイム対戦

もう2年間もずっとゲームし続けている。最近 リアルタイム対戦 モードがリリースされた。全然やる気なかったんやけど、リアルタイム対戦で得たコインでもらえるアイテムが魅力的なのでやることにした。そして、実際にアルタイム対戦をやってみるとはまる。運営の手の平でゲームさせられている。

  • 人間が相手で狡猾な戦略で負けると悔しい
  • 人間が相手の戦略の方が創意工夫があって学びになる
  • 実際にやり始めるとおもしろくなってきてずっとやってしまう

できる時間を制限しているというのもうまいやり方だなと思っている。朝・昼・晩の7-9時、12-14時、19-22時に制限している。そこまでしてリアルタイムに人間同士をマッチングしてゲームさせる必要があるのか?という素朴な疑問に私は辿りつくが、おそらくゲーム開発者からみたらそうじゃない大事なユーザー体験があるのだろうと推測する。あと不思議なことが1つ。マッチングしていると、たまにわざと負けてくれる人がマッチングされる。チームのメンバーが1人で向こうが先行なら戦いを辞退 (こちらの勝ちになる) するし、こちらが先行でもすぐにやっつけられる。ちゃんと統計をとってないけど、20回に1回ぐらいの頻度でわざと負けてくれる人とマッチングする。あの人たちは一体どういう理由でわざと負けているんだろう?

リフレクションのユーティリティを作った

いまお手伝いで開発している api サーバーは外界と内部のデータの境界を明確にわけていて、外向けのオブジェクト定義と内部向けのオブジェクト定義が異なる。ほとんど同じデータであっても dto を介して値を受け渡ししないといけない。そうすると、次のような dto と他のオブジェクトとの値渡しのための処理が型ごとにあちこちに実装されている。

private MyRecord toMyRecord(MyDataInput in) {
    var record = new MyRecord();
    record.id = in.id
    record.name = in.name;
    record.someId1 = in.someId1;
    record.someId2 = in.someId2;
    record.someId3 = in.someId3;
    record.sortOrder = in.sortOrder;
    record.createUser = in.createUser;
    record.updateUser = in.updateUser;
    ...
}

メンバー数が20-30ぐらいあると、たまに値のセット忘れがあったり、あとから追加したメンバーの保守ができてないとか、たまにトラブルが起きる。これ自体は間違っているわけじゃなくて境界を明確にわけるメリットもあるのでプログラミングの煩雑さとトレードオフと言える。

最近、私が管理系の web api のエンドポイントを作る機会が多いせいか、dto と外部向けのオブジェクトを明確にわける必要のない要件もあったりする。試しにリフレクションを使って同名のフィールド間の値の受け渡しは自動でやってみたらどんな感じかな?と思って作ってみた。

public class ReflectionUtil {

    private ReflectionUtil() {
        throw new AssertionError("ReflectionUtil is a utility class");
    }

    private static <T> Field getField(Class<T> klass, String fieldName) {
        try {
            return klass.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            return null;
        }
    }

    public static <T1, T2> T2 mapFieldValues(T1 fromInstance, T2 toInstance) {
        var fromClass = fromInstance.getClass();
        for (var toField : toInstance.getClass().getDeclaredFields()) {
            toField.setAccessible(true);
            var fromField = getField(fromClass, toField.getName());
            if (fromField != null) {
                fromField.setAccessible(true);
                try {
                    toField.set(toInstance, fromField.get(fromInstance));
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return toInstance;
    }
}

これを使うと、先のコードがこれだけで済む。煩わしい値の受け渡しだけのコードを削減できる。

var record = ReflectionUtil.mapFieldValues(in, new MyRecord());

こんなことやると、セキュリティ的によくないとか反論されるかな?と思いながら pr を出してみたら思いの外、好評だったのでちょっと使ってみようと思う。

wiremock を使った kubernetes モックサーバーのテスト拡張

2時に寝て7時に起きて9時までだらだらしてた。

Kubernetes Clients のライブラリ実装

昨日の続き 。ある程度、クライアントの振る舞いを確認できたので自前のライブラリを作ることにした。ライブラリなのでテストをちゃんと書きたいと思って単体テストのやり方を調べてたら Kubernetes Clients のリポジトリにも単体テストはほとんどなくて、どうも e2e テストの方を重視しているようにもみえた。github issues を検索してみたら次の issue をみつけた。

なにかしら単体テストの仕組みを作った方がいいんじゃないかという提案と一緒に issue の作者?かどうかはわからんけど、wiremock を使ったテストのサンプルコードをあげていた。名前だけは聞いたことがあったけど、過去に使ったこともなく、どういうものか全くわかってない。ドキュメントを軽く読んでみたら http モックサーバーらしい。issue の内容を参考にしながら wiremock のドキュメントをみて junit5 のテスト拡張を書いてみた。これが適切な実装かはあまり自信がないけど、こんな感じでモックサーバーとモッククライアントの junit5 のテスト拡張を実装した。これは static なモックサーバーの設定になるので wiremock 自体の起動コストは速く感じた。ライブラリのテストとしては申し分ない。いまのところは自画自賛。

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface KubernetesApiClient {
}
public class SetupKubernetesWireMock implements BeforeAllCallback, BeforeEachCallback, ExtensionContext.Store.CloseableResource {
    private static final Logger logger = LogManager.getLogger(SetupKubernetesWireMock.class.getName());
    private static int PORT = 8384;
    private static WireMockServer wireMockServer = new WireMockServer(options().port(PORT));
    private static boolean started = false;

    private ApiClient apiClient;

    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        if (!started) {
            wireMockServer.start();
            started = true;
            this.configureMockClient();
            var basePath = String.format("http://localhost:%d", wireMockServer.port());
            this.apiClient = ClientBuilder.standard().setBasePath(basePath).build();
            logger.info("started kubernetes wiremock: {}", basePath);
            context.getRoot().getStore(GLOBAL).put("SetupKubernetesWireMock", this);
        }
    }

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        for (var instance : context.getRequiredTestInstances().getAllInstances()) {
            for (var field : instance.getClass().getDeclaredFields()) {
                if (field.isAnnotationPresent(KubernetesApiClient.class)) {
                    field.setAccessible(true);
                    field.set(instance, this.apiClient);
                }
            }
        }
    }

    @Override
    public void close() throws Throwable {
        wireMockServer.stop();
    }

    private void configureMockClient() throws IOException {
        // add static stubs for mock client
        configureFor("localhost", PORT);
        this.stubForBatchGetCronjobs();
        this.stubForBatchGetSingleCronJob("my-job1");
        this.stubForBatchGetSingleCronJob("my-job2");
        this.stubForBatchGetSingleCronJob("my-job3");
        this.stubForBatchPostJob();
        this.stubForBatchGetJob();
    }

    private byte[] getContents(String name) throws IOException {
        var json = new File(this.getClass().getResource(name).getPath());
        return Files.readAllBytes(Paths.get(json.getPath()));
    }

    private void stubForBatchGetCronjobs() throws IOException {
        var contents = this.getContents("/fixtures/kubernetes/batch/cronjobs.json");
        var path = urlPathEqualTo("/apis/batch/v1/namespaces/default/cronjobs");
        stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchGetSingleCronJob(String jobName) throws IOException {
        var contents = this.getContents(String.format("/fixtures/kubernetes/batch/%s.json", jobName));
        var url = String.format("/apis/batch/v1/namespaces/default/cronjobs/%s", jobName);
        var path = urlPathEqualTo(url);
        stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchPostJob() throws IOException {
        var name = "/fixtures/kubernetes/batch/post-my-job.json";
        var contents = this.getContents(name);
        var url = "/apis/batch/v1/namespaces/default/jobs";
        var path = urlPathEqualTo(url);
        stubFor(post(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchGetJob() throws IOException {
        var name = "/fixtures/kubernetes/batch/get-my-job.json";
        var contents = this.getContents(name);
        var url = "/apis/batch/v1/namespaces/default/jobs/my-job";
        var path = urlPathEqualTo(url);
        stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withStatus(200).withBody(contents)));
    }
}

リアクティブプログラミングと WebClient

0時に寝て6時に起きた。金曜日は非稼働日だけど、今週は月曜日が祝日だったから普通に働いてた。

spring-webflux とプロキシ

たまたま api client 周りを触っている。それらは spring の WebClient で実装されている。WebClient は spring-webflux プロジェクトが提供している http リクエストを扱うためのクライアントでリアクティブプログラミングを用いた設計になっている。リアクティブという言葉がピンとこなければ非同期フレームワークを用いた http クライアントと言い換えても大枠ではあっているのではないかと思う。spring-webflux プロジェクトそのものはノンブロッキング、バックプレッシャーといった機能をサポートする web アプリケーションフレームワークを提供するもの。

Reactor というコアライブラリを使って spring-webflux のフレームワークは実装されている。このデータ構造の1つに MonoFlux が出てくる。初見の開発者はこの名前のデータ構造がよくわからんというところから始まる。私がそうだった。ドキュメントの説明によると、Mono は0から1、Flux は0からNまでのデータ列の概念を扱うという。おそらく json のようなレスポンスを返す場合は Mono を使い、ストリームを返すレスポンスは Flux を使えばいいんじゃないかと思う。

Reactor is the reactive library of choice for Spring WebFlux. It provides the Mono and Flux API types to work on data sequences of 0..1 (Mono) and 0..N (Flux) through a rich set of operators aligned with the ReactiveX vocabulary of operators. Reactor is a Reactive Streams library and, therefore, all of its operators support non-blocking back pressure. Reactor has a strong focus on server-side Java. It is developed in close collaboration with Spring.

WebFlux requires Reactor as a core dependency but it is interoperable with other reactive libraries via Reactive Streams. As a general rule, a WebFlux API accepts a plain Publisher as input, adapts it to a Reactor type internally, uses that, and returns either a Flux or a Mono as output. So, you can pass any Publisher as input and you can apply operations on the output, but you need to adapt the output for use with another reactive library. Whenever feasible (for example, annotated controllers), WebFlux adapts transparently to the use of RxJava or another reactive library. See Reactive Libraries for more details.

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-reactive-api

いまローカルの開発環境では vpn 接続をしてプロキシ経由でアクセスするサーバーがいる。WebClient のプロキシ経由で通信できないという問題があることを同僚から教えてもらった。プロキシ経由でアクセスしようとすると認可エラーになってしまう。なにかしらプロキシ経由の接続に問題がある。

Caused by: io.netty.handler.proxy.HttpProxyHandler$HttpProxyConnectException: http, none, /10.100.101.10:8080 => /192.168.201.35:18980, status: 403 Forbidden

同僚は WebClient のデフォルトでは http の CONNECT メソッドを使って通信しようとするが、それを squid がサポートしていないか、設定を変更しないとダメなんじゃないかと話していた。その内容が正しいかどうか、私は未検証だけどデフォルト設定では通信できないことがわかった。ここで WebClient の設定の1つに ClientHttpConnector があり、任意の http client ライブラリに置き換えられる。ソースをみると次の4つの ClientHttpConnector が使えるらしい。デフォルトが ReactorClientHttpConnector になる。

  private ClientHttpConnector initConnector() {
    if (reactorClientPresent) {
      return new ReactorClientHttpConnector();
    }
    else if (jettyClientPresent) {
      return new JettyClientHttpConnector();
    }
    else if (httpComponentsClientPresent) {
      return new HttpComponentsClientHttpConnector();
    }
    else {
      return new JdkClientHttpConnector();
    }
  }

試しに jetty-reactive-httpclient を使って JettyClientHttpConnector に置き換えてみたところ、プロキシサーバー経由のアクセスができるようになった。

public WebClient create(String proxyIp, int proxyPort) {
    var httpClient = new HttpClient();
    httpClient.setFollowRedirects(true);
    httpClient.getProxyConfiguration().getProxies().add(new HttpProxy(proxyIp, proxyPort));
    var connector = new JettyClientHttpConnector(httpClient);
    return WebClient.builder().clientConnector(connector).build();
}

localstack s3 想像以上に難しかった

1時に寝て5時半に起きて7時に起きた。夏バテなのか朝起きれなくなってきた。

localstack 入門

s3 とやり取りするアプリケーションの保守を手伝うことになった。いま開発環境向けに minio を使っていて、そのためだけにトリッキーな DI を実装している。そのコードがトリッキーなだけに知らない人がコードをコピペしてトラブルを起こす火種になってた。minio 使う必要性はまったくなく localstack を使えば解決できるのを、誰もその保守してなくて、仕方ないからこの機に私がやることにした。すぐにできるやろと思ったら意外にはまって2-3時間デバッグに時間を取られたので書いておく。

簡潔に言うと、(おそらく歴史的経緯で) minio は基本的に path style で s3 api を扱う。virtual hosted style でリクエストするとアクセスできなくてどうやって解決するのかが分からなかった。ググって出てきたどこかの開発者の言っている通りで path style は deprecated していて、aws も削除するとまで宣言していて、いつなくなるかもわからないのに未だにそれがデフォルトというのはどうなの?っていうお気持ちを表明している。

私も同意見で issue をよくよく調べてみると次のエンドポイントに対しては virtual hosted style でアクセスできた。localhost に対しては path style で動いていて、virutual hosted style は localhost.localstack.cloud という、よくわからんドメインを使わないといけない。ドキュメントには書いてなくてググって辿り着く issue のコメントをみてたら気付いた。

mybuket.localhost.localstack.cloud 

aws-sdk-java v1 のコード例だと次のようになる。バケット名は実際にリクエストするときのパラメーターから s3 client がセットしてくれるのでこんな感じ。

var endpoint = new AwsClientBuilder.EndpointConfiguration("localhost.localstack.cloud:4566", "ap-northeast-1");
var client = AmazonS3ClientBuilder.standard()
        .withEndpointConfiguration(endpoint)
        .build();
return client;

こんな初期設定でつまづくと、このライブラリを使うのをやめようかという気持ちになる。ちゃんとドキュメントに書いておいてほしい。

窓付きオフィスの空きをみつけた

エリンサーブさんの内覧 に行ってきてから縁があればという感じでオフィス引っ越しは待ち状態になっていた。いま契約しているところの別オフィスで 神戸旧居留地 のサイトを、たまたま今日みたら窓付きの部屋が空き予定だと書いてあった。

7F-07 7月末空き予定 ¥69,300 ¥6,600 2名 6.22㎡ 完全個室/窓付き/棚付き

家賃はいまより 31,900 円増える。年間にすると 382,800 円のコスト増になる。高くはないけど、いまの環境でもう少し続けたら?と言われたらそれでもいっかと思えるぐらいのコスト感。優先度は高くないが、縁があるなら見逃す手もないといったスタンスで臨む。ひとまず明日、電話してまだ空いているなら内覧できるかを聞いてみるところから始める。内覧してよさそうな場所だったら引っ越しを検討する。7月末って急な話だけど、小さいオフィスなので本気出せば レントラ便 で2時間もあれば引っ越しできるはず。荷造りの準備に1時間、搬送に1時間といったところかな。

設計のリファクタリング

23時に寝て4時に起きた。その後もだらだらしていて変な寝方した。

型定義のリファクタリング

開発が佳境でサービスイン前なのと機能開発は完了しているのでリスクのある作業やリファクタリングなどを主にやっている。別の開発者が作った機能の型定義が曖昧なところをまとめてリファクタリングしていた。若い開発者だから仕方ないことだけど、ジェネリクスとポリモーフィズムを正しく理解できていない。effective java で言うところの抽象骨格実装という設計手法になる。あと ide のリファクタリング機能を使ってコードを書いているのか?public な api の設計がおかしかった。人間が考えてメソッド分割したようには思えない oop らしからぬ手続き的な api になっていた。後から oop としてジェネリックな型定義に落とし込むのがなかなか難しくて丸1日ぐらいやってた。私がドメイン知識をもっていないのもある。以前にもそのコードの pr をレビューしていてよくないコードだとはわかっていたけど、私はドメイン知識がないためにやりたいことがわからないから最低限の品質でマージした後に私がリファクタリングすればいいかと考えていた。若い開発者にプログラミングの設計やコーディングを教えるのは時間を要するのでお手伝いという立場だとなかなか難しい。だから私が直してしまえばいいやというノリで勝手に直した。

openapi generator の設定

3時に寝て6時半に起きた。昨日は夕方に昼寝したので夜は眠れなかった。

openapi generator の x-implements 機能

外部ベンダーの api client の wrapper を実装していて、api client が扱うリクエストやレスポンスを型 (インターフェース) で抽象化できるとよさそうと思って openapi generator の設定を調べていた。maven-plugin の設定と openapi-generator の設定の2つがあるので両方のドキュメントを確認しないといけない。

そんなに都合よくインターフェースを指定できるような仕組みがなければ、最悪は mustache テンプレートをカスタマイズするしかないかなぁとか考えていた。テンプレートを操作すると、今後の保守コストが上がってしまうのでそのメリット・デメリットを比較して考えないといけない。諦めかけていたときに so でこの issue をみつけた。

ちょうどこの5月末にリリースされたばかりの 6.0.0 に x-implements と指定すれば、任意のインターフェースを implements できる機能が追加された。これはスキーマに対する設定なのでテンプレートをカスタマイズするよりずっと保守コストは小さくて済む。

例えば、openapi schema の json で設定すると、コード生成したときにそんな風にインターフェースが付く。

       "SomethingApiResponse": {
+        "x-implements": "com.example.app.MyResponse",
         "title": "SomethingApiResponse",
         "type": "object",
         "properties": {
-public class SomethingApiResponse {
+public class SomethingApiResponse implements com.example.app.MyResponse {

あまりにも意図していた機能をみつけて嬉しくてツィートしてしまった。

assertj を使ってみた

0時に寝て6時に起きた。

assertDeepEquals を作った

AssertJ というアサーションライブラリを使って assertDeepEquals を実装した。

junit4 では hamcrest という matcher が使われていて、それが assertDeepEquals 相当の機能を提供していたが、それが junit5 では提供されなくなったので自分で実装するか、アサーションライブラリを別途使う必要がある。

現時点でJUnit5ではHamcrestのMatcherは提供せず、使用者が自由に選択する方針で進んでいます。そうなった場合、標準でサポートされるassertTrueやassertEquelsなどだけでは、ちょっと頼りなく車輪の再発明になりそうなので、候補になりそうなHamcrestとAssertJのよく使いそうなメソッド比較表を作りました。

JUnitのアサーションライブラリHamcrest,AssertJ比較

2.4.2. Third-party Assertion Libraries によると、junit は基本的なアサーション機能を提供し、より強力なアサーションはサードパーティ製の好きなライブラリを使ってくれみたいなことが書いてある。軽く github でソースコード検索しても、みんな自前で作っているんやなということも分かる。

hamcrest はもう保守されていないようにみえるので assertj を使うことにした。assertj の機能を使うと assertDeepEquals を次のように実装できる。直接 assertj を使ってもよいのだけど、assertXxx という名前で使えた方が junit ベースのテストのアサートの統合性があるし、いまお手伝い先では myapp-test のような、テスト向けの共通ライブラリを提供していて、すべてのプロジェクトで既に使っているので assertj の依存関係を追加しなくてもすぐに使えるというぐらいの利便性を提供するだけのユーティリティになる。

public class Assertions {

    public static final void assertDeepEquals(Object expected, Object actual) {
        assertThat(expected).usingRecursiveComparison().isEqualTo(actual);
    }

    public static final void assertDeepEquals(Object expected, Object actual, String... fields) {
        assertThat(expected).usingRecursiveComparison().comparingOnlyFields(fields).isEqualTo(actual);
    }

    public static final void assertDeepEqualsIgnoringFields(Object expected, Object actual, String... fields) {
        assertThat(expected).usingRecursiveComparison().ignoringFields(fields).isEqualTo(actual);
    }
}

maven で executable jar を作る

4時に寝て7時に起きた。

maven での executable jar の作り方

gradle では作ったことがあったけど、maven では初めてなので要領がわかっていない。

これらの記事を読むと、maven-assembly-plugin を使えばいいのかな?とまずはこのプラグインで検証を始めた。古くからあるプラグインなので実績は十分なのだけど、もうあまり保守されていないのか、他プラグインから jar のマニフェストに書き込んで git のリビジョン番号が連携できてなかったり、通常の jar の生成処理を置き換えられなかったりと、あまり使い勝手のよいものではなかった。あと log4j2 と相性が悪くて意図したように設定ファイルを読み込んで初期化ができない。

main ERROR Error processing element EcsLayout: CLASS_NOT_FOUND
main ERROR Unable to locate plugin type for EcsLayout
main ERROR Unable to locate plugin for EcsLayout
main ERROR Could not create plugin of type class org.apache.logging.log4j.core.appender.ConsoleAppender for element Console:
  java.lang.NullPointerException: Cannot invoke "org.apache.logging.log4j.core.config.plugins.util.PluginType.getElementName()"
  because "childType" is null java.lang.NullPointerException:
    Cannot invoke "org.apache.logging.log4j.core.config.plugins.util.PluginType.getElementName()" because "childType" is null

この厄介な問題をデバッグするよりも、すでにうまくいくことがわかっている spring-boot-maven-plugin を使った方が簡単そうだったのでそうすることにした。不要な spring boot 関連の jar なども executable jar や docker イメージに含まれてしまうことだけがデメリット。そこだけ目を瞑れば log4j2 の初期化エラーも起きず、正常に動作した。やっぱり最近のアプリケーションで使われているプラグインはちゃんとしてるねみたいな話しにしておく。次の設定だけでうまくいった。

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <mainClass>com.example.myapp.Main</mainClass>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>

log4j2 の設定ファイルの動的な読み込み

0時に寝て7時に起きた。

バッチ処理モジュール

cli でバッチ処理モジュールを作った。コマンドラインの引数パーサーと yml のパーサーを使うことにした。

ロガー実装に log4j2 を使っているので設定ファイルはアプリケーションの設定ファイルと log4j2 の設定ファイルの2つになる。それぞれ環境ごとに用意してエントリーポイントから起動したタイミングで明示的に設定ファイルを読み込むようにした。

log4j2 の yml 設定ファイルを動的にどうやって設定するかはドキュメントにもとくに書いてなかった気がする。log4j2 のソースコードやテストコードを読みながら次のようにしたら反映された。

public static Config load(BatchEnvironment env) {
    var path = String.format("config-%s.yml", env.getName());
    var inputStream = ConfigUtil.class.getClassLoader().getResourceAsStream(path);
    var yaml = new Yaml(new Constructor(Config.class));
    return yaml.load(inputStream);
}

アプリケーションの設定は yml 設定に対応する Config クラスを定義しておいて次のようにして読み込む。

public static void initializeLogSettings(BatchEnvironment env) throws IOException {
    var path = String.format("log4j2-%s.yml", env.getName());
    var inputStream = ConfigUtil.class.getClassLoader().getResourceAsStream(path);
    var source = new ConfigurationSource(inputStream);
    var configuration = new YamlConfigurationFactory().getConfiguration(null, source);
    Configurator.initialize(configuration);
}

ちょっとした cli を作るときにちょっとしたライブラリがあると楽でよい。

欠損金の繰り戻し還付の申請の誤り

国税局から電話がかかってきた。初めて提出した欠損金の繰り戻し還付の申請があちこち間違ってますよと。申請書類と一緒に法人税の申告書もみてもらっていて、還付申請した金額も申告の別表1に記入する必要があって、それも一緒に修正してねという話し。法人税の修正申告と還付の訂正依頼の2つが必要とのこと。税務署の人たちは本当に丁寧で親切にあれが間違っている、これが間違っていると教えてくれる。素人が法人決算やっているので初めて行う手続きの間違いはつきものだけど、税務署の人たちが教えてくれるので本当に助かる。感謝。

jjug ccc 2022 spring 参加

1時に寝て7時に起きた。前日、お酒飲んでたくさん雑談したので疲れ果てて寝てた。

jjug ccc 2022 spring

私の発表は朝10:25からだったけれど、その1つ前が同僚の発表なので見とこうと思って9時前にはオフィスに着いてたと思う。twitter のハッシュタグを開いたり、スライド資料のツィートの文面を用意したり、発表者用のTシャツを着たりなど、いろいろ発表前にできそうな準備をしておいた。zoom と youtube live の両方で配信しているせいか、zoom でのやり取りと youtube live のチャット欄でのやり取りが混ざって、発表者も運営も混乱していたように思う。jjug のスタッフさんも当日にあれこれ指示を出していたりもして配信プラットフォームが複数になるとややこしいよなとか思いながら眺めてた。

Track C の発表を午前中いっぱい、私のも含めて3つみてたんだけど、おそらく関係者以外で発表を聴いている人はかなり少なかったのではないかと推測する。まず twitter のハッシュタグも youtube live のチャット欄もほとんど書き込みはなく、質問もないから jjug のスタッフさんが質問するという、予想していた通りの展開になった。同僚と私の発表はぽっと出の発表なので視聴者が少なくてもわかるんだけど、その後のてらだよしおさんの発表もクラスメソッドさんのレポートを書いている人しかコメントしてなかったように思う。

オンラインイベントだからリアルタイムに視聴しないのか、日曜日または朝だから少ないのか、また機会があれば中の人にも聞いてみる。昨日の疲れもあって、会社ブログの記事を書いたら眠くなってきたので、午後から帰って寝てた。

flyway を触ってみた

0時に寝て4時に起きてタイムライン眺めながらだらだらして6時半に起き上がった。

データベースの移行処理

半年前から導入したいという話しは聞いていたものの、先送りになっていたライブラリに flyway がある。データベースの移行処理のためのスクリプト (sql) を管理するツールでどの移行スクリプトを実行したかを記録したり、未適用の処理を自動で適用してくれたりする。spring boot だとすぐ組み込める状態になっていて Community Plugins and Integrations: Spring Boot をみながら設定したらすぐに動いた。flyway 自体の設定も Common Application Properties を参考に spring boot の設定ファイルで行える。

例えば、こんな感じ。

spring:
  flyway:
    enabled: true
    schemas: public
    locations: classpath:db/migration
    baseline-version: 0
    baseline-on-migrate: true

移行処理の履歴情報は flyway_schema_history テーブルに保持される。既存のテーブルが存在して flyway の履歴データがない場合 (初回起動時) に移行処理を実行するかどうかを baseline-on-migrate で決める。実行するなら baseline-version でどのバージョンをベースラインとするかも設定できる。ゼロにすることで V1 からの sql ファイルを適用してくれる。ベースラインの考え方は実際に何度かデータベースの初期状態を変えて実行しないとわかりにくいかもしれない。