システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

CargoでPayara Serverを動かす

はじめに

こちらで使用したCargoというEEサーバーを動かすための薄いラッパーライブラリについて、自分が使ってみて良かったことなどをまとめたものです。 ブログなどでコードを公開する際、特にJakarta EEのようなEEサーバーを使う前提のものは、Spring Boot や QuarkusのようなUberJarを用いるものと比べると 公開されているコードを実際に動かすまでの手間がかかって避けがちになるように思います。
Cargoを使えば、対象となるEEサーバーのダウンロードも含めて実行してくれるので プロジェクトを git clone した後、ほとんど手間なくEEサーバーで動きを含めて確認ができます。
今回は それに加えて Mavenも同時にダウンロードする Maven Wrapper(mvnw) や、H2DBも 準備しています。

EEサーバーがあって、H2DBという標準SQLが書ける*1、基本環境が さっと手元で動かせる雛形プロジェクトの事例として、以下の記事で実際にやったことを踏まえつつ説明をしていきたいと思います。

vermeer.hatenablog.jp

  • 注意点

本プロジェクトでは Payara Serverでしか動作は確認できていません*2
その点は ご了承ください。
参考に挙げている、@backpaper0さんのリポジトリでは、コードを一切書き換えることなく(!) WildFlyやTomEEを Cargoによって起動することが出来ますので、是非 そちらも参考にされると良いと思います。

リポジトリ

https://github.com/vermeer-1977-blog/jakarta-ee9-sample

の中のサブプロジェクト
https://github.com/vermeer-1977-blog/jakarta-ee9-sample/tree/main/jpa-part-0

を使って説明をしたいと思います。

実行環境

Maven Wrapper(mvnw)

なにが嬉しいの?

Cargoの前に、mvnwについて。
mvnwを使うとローカルにMavenがインストールされていなくても、コマンドを実行するだけで 必要なMavenライブラリをダウンロードをしてくれてプロジェクト管理が可能な状態にしてくれます。

Mavenのバージョンが違うという事で弊害にであったことは、私自身はありませんが ビルド環境のベースとなるライブラリが異なるという理由で ビルドが失敗するという経験は出来れば避けたいものです。 mvnwはダウンロードする対象バージョンもプロジェクト毎に固定化されるので、そういった心配もありません。

実際に動かしてみる

jpa-part-0 フォルダへ移動して

./mvnw package

をすると、Mavenダウンロードと加えて、コードをビルドして warファイルの作成までしてくれます。 作成された warファイルは targetフォルダ配下に格納されます。

自分で作りたい

利用側はMavenのインストールが不要ですが、プロジェクトを作る側はMavenのインストールが必要です。 Mavenをインストールした後で

mvn -N io.takari:maven:0.7.7:wrapper

を実行すると

.mvnというフォルダが作成されます。これが mvnwの本体です。
また、カレントフォルダには mvnwmvnw.cmd というファイルが作られます。こちらは 実行するコマンドです。

これで mvnwを使えるようになりました。

詳細は 公式を確認ください。
https://github.com/takari/maven-wrapper

注意点

mvnwを使うだけであれば、上記の通りで良いのですが、このままだと Gitの管理対象外のままなので.gitignoreへ以下を追記します *3

#exclude
!maven-wrapper.jar

を追記して、mvnwの本体をリポジトリの管理対象に入れてください。

Cargo

ようやく本題のCargoです。
コンテナのデータ取得から設定まで pom.xmlで完結します。 pom.xmlの どこで 何をしているのか Cargo周りを中心に説明します。 DB周りや個々の設定意図については、後述します。

なにが嬉しいの?

DockerのようにOSイメージまでダウンロードをせず、「JVMが動くこと」を前提とした それ以降のレイヤーの資産をダウンロードおよび実行までしてくれるコンテナが準備可能という事です(つまり軽い)。

たとえば ちょっとしたデモ環境を作りたいときに 実行環境となるPCの環境設定を一切変えることなく EEアプリを動かせます*4

pom.xml(抜粋)

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.cargo</groupId>
            <artifactId>cargo-maven3-plugin</artifactId>
            <version>1.9.4</version>
            <configuration>
                <configuration>
                    <datasources>
                        <datasource>
                            <jndiName>jdbc/DemoDS</jndiName>
                            <driverClass>org.h2.Driver</driverClass>
                            <url>jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1</url>
                            <username>demouser</username>
                            <password>demosecret</password>
                        </datasource>
                    </datasources>
                </configuration>
                <container>
                    <dependencies>
                        <dependency>
                            <groupId>com.h2database</groupId>
                            <artifactId>h2</artifactId>
                        </dependency>
                    </dependencies>
                    <systemProperties>
                        <javax.persistence.schema-generation.database.action>create</javax.persistence.schema-generation.database.action>
                        <javax.persistence.schema-generation.scripts.action>create</javax.persistence.schema-generation.scripts.action>
                        <javax.persistence.schema-generation.scripts.create-target>${project.basedir}/target/create.sql</javax.persistence.schema-generation.scripts.create-target>
                        <javax.persistence.sql-load-script-source>META-INF/tmp/initializer.sql</javax.persistence.sql-load-script-source>
                        <eclipselink.logging.level>FINE</eclipselink.logging.level>
                    </systemProperties>
                </container>
            </configuration>
        </plugin>
    </plugins>
</build>

<profiles>
    <profile>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <id>payara</id>
        <properties>
            <cargo.maven.containerId>payara</cargo.maven.containerId>
            <cargo.maven.containerUrl>https://repo.maven.apache.org/maven2/fish/payara/distributions/payara/5.2021.4/payara-5.2021.4.zip</cargo.maven.containerUrl>
        </properties>
    </profile>
</profiles>

maven(build)

Cargoのプラグイン(cargo-maven3-plugin)、build定義に追記します。

datasource

利用するDBの設定をします。 この設定は「Cargoで起動したEEサーバーの設定」です。
Javaアプリ(persistence.xml)とEEサーバーとの関連付けは、 <jndiName>jdbc/DemoDS</jndiName> です。

なので、usernamepasswordを demouser や demosecret から変更しても、実際のところ 問題なくCargoで起動したWebアプリは動きます*5

じゃぁ、そのDB本体(ミドルウェア)は どこから入手しているの?というのは後述します(環境設定周り)。

systemProperties

システムプロパティも指定が可能です。
このシステムプロパティも先ほどと同様に「Cargoで起動したEEサーバーの設定」です。

javax.persistence.の記載の意図などについては後述します(環境設定周り)。

maven(profile)

利用する EEサーバーの本体となるファイルのダウンロードおよび、コマンド実行時の挙動を設定します。

id

コマンドでパラメータ指定する際に利用します。

複数のEEサーバーを定義した際には <activeByDefault>true</activeByDefault>を付与している要素がデフォルトのEEサーバーとして起動できます。

任意のサーバーをパラメータで指定する場合は

./mvnw cargo:run -Ppayara

のように idを指定すれば実行できます。

properties

Payara Serverのzipファイルの取得先を指定します。

cargo:runした時に、zipファイルがダウンロードされていなければジョブの開始時点でダウンロードをします。ダウンロードして展開したEEサーバーの実体は実行パスのカレントに作成される targetフォルダの中にあります。

ダウンロード先や設定の詳細は公式を確認してみてください。

https://codehaus-cargo.github.io/cargo/Home.html

ちなみに fileプロトコルを使えば ローカルに保存しているzipファイルを取得先として指定できます*6

<cargo.maven.containerUrl>file:/home/hoge/Downloads/payara-5.2021.4.zip</(...)>

Windowsであればfile:c:\(以下略)という感じで指定すれば出来ると思います(未検証)。

注意点

Cargoが利用するEEサーバーの実体は targetフォルダにあるため ./mvnw cleanなどでtargetフォルダを削除した場合、再ダウンロードをします。
検証中で なんども cleanを実行するけれど ダウンロードの頻度は避けたい時は、上記の fileプロトコルを使った取得に書き換えると作業が捗ります。

環境設定周り

コンテナに関する環境設定の詳細と意図について説明したいと思います。

なにが嬉しいの?

  • コンテナで利用するDB(H2)を立ち上げる(個別にインストールしなくて良い)
  • @Entityクラスと同期のとれたテーブルが生成される
  • データの初期登録もできる
  • 上記の設定に関する記述をコードに記述しなくて良い
  • warファイルに設定が含まれないので、独立して準備したEEサーバーや開発環境のために書き直しなどをしなくて良い

H2を利用する

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.200</version>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>     

実行時依存として追加しておくことで、コンテナ起動時にH2をDBとして使えるようになります。 接続に関する説明は既出のため割愛します。

@EntityクラスからDBを生成

<javax.persistence.schema-generation.database.action>create</(...)> で@Entityクラスの実装から、コンテナ起動時にH2へテーブルを生成します。

もしDDLがある場合は、そちらを使ってテーブル生成しても良いと思います(その際のプロパティについては割愛します)。

DBからDDLを生成

<javax.persistence.schema-generation.scripts.action>create</(...)> でDBからテーブル生成DDLを作成します。

<javax.persistence.schema-generation.scripts.create-target>${project.basedir}/target/create.sql</(...)> は生成したDDLの出力先です。

このDDLを使えば、コンテナ起動時に自動生成されるDBを手元のDB環境で作ることも出来ます。(この辺りの意図は後述(開発IDEとの連携)で)

DBへ初期データを登録

<javax.persistence.sql-load-script-source>META-INF/tmp/initializer.sql</(...)> でコンテナ起動時にデータを初期登録できます。
指定パスは クラスパス配下です*7

SQLの実行ログ

<eclipselink.logging.level>FINE</(...)>

この辺りは実装依存です。
Payara Serverは JPAの実装が EclipseLinkなので このように指定します。 Hibernate が実装となっているEEサーバーの場合は指定方法が違います。

開発IDEとの連携

Cargoとは直接関係ないのですが、これまでの設定は この「開発IDEとの連携」を見越して行ったものもあるので説明をしたいと思います。

EEサーバー

Cargoは実行環境ではありますが、開発時に接続するサーバーと比べて あれこれとしようと思うと少し手間がかかります。
たとえばIDEによるデバッグモード起動などはローカルにインストールしたEEサーバーを使用する方が、既知の開発方法を流用できるので便利です。

Cargoの設定は Cargoに閉じています*8
IDEにてローカルにインストールしたEEサーバーを選択して これまで通り開発ができます。

テーブル生成

CargoではH2を使いましたが、本番環境では異なるDBかもしれません。
またCargoでは テストのために作り込んだDBの状態が起動の都度クリアされてしまいます。

Jakarta Persistence(旧JPA)のJPQLや 標準SQLの範囲で、適切に抽象化されていれば DB製品が異なっても利用可能です*9
ということで、Cargo管理下とは別にDBをつくって開発を進めましょうということになるわけですが、空のDBは各DBの手順に従うとして、その先のテーブル生成はどうしましょう?

<javax.persistence.schema-generation.database.action>create</(...)>を使って、@Entityから生成をするという方法もあるでしょう。
では DDLを使ってテーブルを生成したい場合は、どうしましょうか?

ということで<javax.persistence.schema-generation.scripts.(...)で作成したDDLを使いましょう。

卵とヒヨコみたいな感じですが、とにかく入手したDDLを元に開発環境や本番環境のテーブル生成ができます。

なお、Cargoで設定した時には、その値に特に重きを置いていなかった、usernamepasswordですが、 EEサーバーで JDBCのコネクションプールの設定や、直接DBへの接続をするためには必要な情報となります。

DBの接続設定

DBの接続情報を、利用するEEサーバーにも設定をしましょう。
JNDIは Cargoと同じ jdbc/DemoDS としてください。こうすることでDBの実体は違ってもプロジェクトの定義を変えることなく起動できます。
IDとパスワードは前項の「テーブル生成」で設定した値を使用してください。

なお、IDおよびパスワードですが、Cargoで設定したものとは必ず別の値にしましょう。
リポジトリ管理される資産に有効なIDとパスワードは記録すべきではありません。

参考

github.com

GitHub - takari/maven-wrapper: The easiest way to integrate Maven into your project!

Codehaus Cargo - Home

事のキッカケの自tweet

さいごに

Jakarta PersistenceのNativeQueryを使う記事で、初めてCargoを使いました。 他にも色々と便利な機能などありそうな気がしますが、DBを使うアプリの実行および開発環境準備まで含めた やっておきたいことは試せたように思います。
可能な限り、今後の Jakarta EEに関するブログを書くときには、Cargoを使って動くサンプル実装が作られたらと思います。

*1:PosgreSQL互換モードもある

*2:というか TomEEなどでは動かないという確認もしています

*3:Java向けの設定では一般的に jarファイルを対象外としています

*4:JDKのインストールは必要だけど

*5:実際に変えてみて動くことまでは確認しました

*6:当然ですが、一度は自分でローカルにzipファイルを保存しておく必要はあります

*7:Javaコードから読み込める場所にないといけないですからね

*8:portの衝突があるので、コンテナは終了させておきましょう

*9:厳密に言えば「EEの仕様としては そのように推定される」という言い方になるかもしれませんが

Jakarta Persistence(旧 JPA)でNativeQueryを外部ファイルで実装する

Jakarta Persistence(旧 JPA:便宜的に以下「JPA」と記載しています)による、@Entityを使わないクエリ―の実装サンプルを考えてみました。

例えば、ReadOnlyなDTOを任意に作りたい時などに使えます。

はじめに

少し背景や動機的なところを。
JPAはクエリ―の戻り型とし@Entityを付与したクラスを指定しないといけないため、@Idを付与したプロパティが必須です。 それはそれでつけてしまえば良いのですが、参照系クエリ―は更新系と異なりDBから取得した状態を そのままクライアントへ返却しても十分なケースが多々あります。 最終的に JSONで返却しますが、特に必要でない場合でも「ID(もしくはそれに相当するカラム)」が必要となります。

私には どうもそれが煩わしいと思いました。
参照系クエリ―はクライアントの都合に合わせて自由な型で返したい、と。そのためには可能な限り制約は減らしておきたい、と。

また、JPQLは悪くはないと思うのですが 個人的には標準仕様のSQLの範囲であれば、各種DBにおいても概ね動くのではないか? また、標準仕様に加えて さらに抽象化しても、結局のところ 実行されたSQLを確認することは避けられないと思っています。
ということで、NativeQueryを使うやり方を きちんと整理しておこうと思い立ち色々と久しぶりにJPAと戯れた足跡を未来の自分のために残しておくことにしました(ついでにSQLは外部ファイル化しておきたい*1)。

なお、やってみて思ったことなのですが
「確かにDTOを好きに定義できるのは便利だと思う。ただ当初のクライアントの都合に合わせて自由な型で返したいというのはアクセスする仕様に合わせて変えれば良いので、データの取得時点で あれこれ気にしすぎず、用途に合わせて詰め替えれば良いんじゃないのかな」という、結局のところ原点回帰みたいな感想を持ったわけですが、それは別の話ということで。

part-0:プロジェクトの基本構成 ですが、パッケージやクラスの役割の説明です。
本題とは無関係です。
自分が何故こういう構成にしたのか?というメモなので やり方だけを見たい方は飛ばしていただければと思います。

リポジトリについて

ルートプロジェクトはこちらです。
https://github.com/vermeer-1977-blog/jakarta-ee9-sample

サブプロジェクト毎に やったことを分けています。
全てのプロジェクトは単独で起動できるように Cargo*2を使っています。
ルートプロジェクトを git cloneした後で、各プロジェクトフォルダにて

./mvnw package

でパッケージを作成して

./mvnw cargo:run

で起動します。 各プロジェクトのアクセスポイントはREADMEに記載しています。

Cargo自体は各EEサーバーで動かす薄いラッパーなのですが、私の手元ではPayara Serverでしか動かせませんでした。その点はご了承ください。
.mvn/wrapper フォルダですが 本来はparentに1つだけ作っておき、サブプロジェクトから相対位置指定でコマンド実行をすれば より無駄は無いと思いますが、今回はブログ用サンプルと言う特性も鑑みて 各プロジェクトに配置しています。

実行環境

part-0:プロジェクトの基本構成

パッケージ構成(レイヤー構成)は「三層+ドメイン」が好きなので、それに則っています。

vermeer.hatenablog.jp

今回は単純な参照アプリということで、Service(UseCase)は不要と考え、Application層を そもそも中継していません。
Presentation層から、Domain層のQueryインターフェース(Repository)を経由してDBから情報を取得しています。なので正しくは「2層+ドメイン」です。
もし参照する際に 事前条件や事後条件の検証をしたい場合は、Application層での操作を挟んでください。

Domain層はEEも含めて依存の無いPOJOです。
Domain層は業務ロジックの中枢みたいな扱いをすべきかもしれませんが、私は「依存の無いPOJOを、適宜 分類して格納する層」くらいに雑な整理をしています。なので、例えば Infrastructure層で何かしらロジカルなことをしたい場合は、関数として抜き出してパッケージで分類をしてDomain層へ配置するという感じです。全部が全部をDomain層へ移動すべきだ!とは思っていないですし 過度にパッケージ移動させるのも違うかもしれないのですが、例えば Domain層に「domain.presentation.converter」というパッケージが例えあったとしても良いんじゃないかなぁと。POJOユニットテストがしやすいので、その辺りも含めて「Domain層のクラスは依存を持たずにテストはしやすく」であり「依存を持たないテスト対象のクラスはDomain層へ集約しておく」というのが 現時点でのパッケージ整理における方向性です。
この辺りの「Domain Objectは どの層からでも利用する」というところが「三層+ドメイン」を好きな理由でもあります。

余談ではあるのですが 私はTDDが苦手です。もちろんユニットテストは作ると後々で楽になることが多いので作ります。VOやEntityに業務の関心事を集約すべし*3という延長でTDDもあると思っているのですが、「頭が業務フロー的思考」な私には部品は主たる振舞いの流れの中の部品でしかなく、それの先出しを強制されると全体俯瞰への思考から離れるため苦手です。金額計算ロジックを集約するような部品としての業務知識も大事ですが、そもそもこのシステムは何するものぞというフローやユースケースを考える方が 私の思考には親和性があります。私にとって「設計」とは幹となる骨格を扱うことで、枝葉は「設定」と捉えているのかもしれません。

part-0 ですが、データアクセスをJPAの@Entityを使っています。

@Entity
@NamedQueries({
    @NamedQuery(name = "UserData.findAll", query = "SELECT u FROM UserData u"),})
public class UserData implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private Long id;
    private String userName;
    private String nickName;

(...)
@RequestScoped
public class UserDataQueryImpl implements UserDataQuery {

    @PersistenceContext
    EntityManager em;

    @Override
    public List<User> getUserData() {
        var results = em.createNamedQuery("UserData.findAll", UserData.class).getResultList();
        var users = results.stream().map(userData -> {
            var _names = userData.getUserName().split(" ");
            var _userName = new UserName(_names[0], 2 <= _names.length ? _names[1] : "");
            return new User(_userName, userData.getNickName());
        }).collect(Collectors.toList());

        return users;
    }
}

NamedQueryでデータを取得して、DomainObject(User.class)へデータを詰め替えて呼び出し元へ返却しています。
Domain層に jakarta.persistence.* パッケージが流入しないように、ここで詰め替えをします。

以下では、この NamedQueryを NativeQueryに変えるところを中心に説明をします

part-1:クラスで実装

NativeQueryによるデータ取得についてクラスを使った実装例です。

受け取るクラスを@Entityではなく、POJOであるDTOにします。このDTOは使用しない「Id」をプロパティから除いています。 このように自分のSQLの結果と同じレイアウトのDTOを準備しておけば良いので、DBスキーマの構造は考えなくてよくなります。 難点は、@Entityで使用できた @OneToMany などが使えないので 階層的な構造を持たせたい場合は 詰め替えが必要です。

@RequestScoped
public class UserDataQueryImpl implements UserDataQuery {

    @PersistenceContext
    EntityManager em;

    @Override
    public List<User> getUserData() {
        List<UserDataQueryDto> results = em.createNativeQuery("select userName, nickName from UserData", "UserDataQuery").getResultList();
        var users = results.stream().map((result) -> {
            var user = result.toUser();
            return user;
        }).collect(Collectors.toList());

        return users;
    }

}
public class UserDataQueryDto {

    private final String userName;
    private final String nickName;

    public UserDataQueryDto(String userName, String nickName) {
        this.userName = userName;
        this.nickName = nickName;
    }

    public User toUser() {
        var names = this.userName.split(" ");
        var _userName = new UserName(names[0], 2 <= names.length ? names[1] : "");
        var user = new User(_userName, this.nickName);
        return user;
    }

    /**
     * Dummy Class for ResultSet Mapping
     *
     * <code>@Entity</code> class must set <code>@Id</code> property. <br>
     * (Id field is not needed for DTOs.)
     */
    @SqlResultSetMapping(name = "UserDataQuery",
            classes = {
                @ConstructorResult(
                        targetClass = UserDataQueryDto.class,
                        columns = {
                            @ColumnResult(name = "userName", type = String.class),
                            @ColumnResult(name = "nickName", type = String.class),}
                )

            })
    @Entity
    static class MappingConfig implements Serializable {

        @Id
        String id;
    }

}

em.createNativeQuery の第二引数 "UserDataQuery" と UserDataQueryDto の @SqlResultSetMapping(name = "UserDataQuery", と関連付けされて クエリ―の取得結果がマッピングされます。
項目マッピングは UserDataQueryDto の targetClass = UserDataQueryDto.class で関連付けされます。
List<UserDataQueryDto> results = em.createNativeQueryのように戻り型を明示的に指定している(varを使っていない)のは、コンパイル時点では型が確定していないためです(実行時に解釈される)*4

toUser()では、レスポンスJSONの型(User.class)にあわせて DTO(UserDataQueryDto)から詰め替えをしています。 こうすることで Queryのメソッドに変換する処理が集約できます。(なお part-0 でも UserData.class内で 同等のメソッドを準備しておけば同じことが実現できます)

MappingConfig は DTOとNativeQueryの結果をマッピングするためのクラスです。
@SqlResultSetMapping アノテーションは@Entityクラスに付与することが必須となっています。
実際に該当するテーブルが存在する必要はありません(@Tableは必須ではない)。
ただし @Idは必須です。
そういったJPAの都合を踏まえて作成したクラスです。
アクセス修飾子をdefault(無印)にして、他パッケージからのアクセスを抑制しています。privateにしても動きますが、Lintによって警告メッセージ(未使用のクラス)が出たりするので、defaultにしています。

MappingConfigクラスですが、UserDataQueryImpl に記述しても良いですし 独立したクラスで実装しても問題ありません。私はDTOのプロパティと近い場所にあった方が確認しやすいということで上述の箇所に記述することにしました。

  • 寸評

概ね これで問題は無いのですが、SQL記述部分は 外部ファイルにしたいです。 複数行にまたがった場合、記述したSQLをコピーして実行検証がやりにくいためです。 EEサーバーで対応するJDKのバージョンが上がり、Text Blocks(JEP 378)が使えるようになったら この辺りの作法も変わっていくかもしれません。

part-2:orm.xmlで実装

NativeQueryを外部ファイルで扱う実装例です。
動機は前述の通り「複数行にまたがった場合、記述したSQLをコピーして実行検証がやりにくいため」です。
orm.xml を使って実現します。

@RequestScoped
public class UserDataQueryImpl implements UserDataQuery {

    @PersistenceContext
    EntityManager em;

    @Override
    public List<User> getUserData() {
        var results = em.createNamedQuery("UserDataQuery", UserDataQueryDto.class).getResultList();
        var users = results.stream().map((result) -> {
            var user = result.toUser();
            return user;
        }).collect(Collectors.toList());

        return users;
    }

}
public class UserDataQueryDto {

    private final String userName;
    private final String nickName;

    public UserDataQueryDto(String userName, String nickName) {
        this.userName = userName;
        this.nickName = nickName;
    }

    public User toUser() {
        var names = this.userName.split(" ");
        var _userName = new UserName(names[0], 2 <= names.length ? names[1] : "");
        var user = new User(_userName, this.nickName);
        return user;
    }

//    /**
//     * Dummy Class for ResultSet Mapping
//     *
//     * <code>@Entity</code> class must set <code>@Id</code> to property. <br>
//     * (Id field is not needed for DTOs.)
//     */
//    @SqlResultSetMapping(name = "UserDataQuery",
//            classes = {
//                @ConstructorResult(
//                        targetClass = UserDataQueryDto.class,
//                        columns = {
//                            @ColumnResult(name = "userName", type = String.class),
//                            @ColumnResult(name = "nickName", type = String.class),}
//                )
//
//            })
//    @Entity
//    static class MappingConfig implements Serializable {
//
//        @Id
//        String id;
//    }
}
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
    xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm_2_2.xsd"
    version="2.2">

    <named-native-query name="UserDataQuery" result-set-mapping="UserDataQueryDto">
        <query>
<![CDATA[
/* UserDataQuery */
select userName, nickName from UserData;
]]>
        </query>
    </named-native-query>

    <sql-result-set-mapping name="UserDataQueryDto">
        <constructor-result target-class="vermeer.sample.ee9.infra.queries.userdata.UserDataQueryDto">
            <column name="userName" class="java.lang.String"/>
            <column name="nickName" class="java.lang.String"/>
        </constructor-result>
    </sql-result-set-mapping>    
    
</entity-mappings>

UserDataQueryDto のマッピングに関する内容は orm.xmlで記述するので除外です。 UserDataQueryImpl は

 var results = em.createNamedQuery("UserDataQuery", UserDataQueryDto.class).getResultList();

のように varが使えるようになっています。Queryの第二引数で型(UserDataQueryDto.class)を渡していることで解釈可能になっているためです。
注意してほしいところは「createNamedQuery」となっているところです。先の「createNativeQuery」ではありません。 orm.xml で定義した名前と一致する情報を使用する(Namedなクエリ―を使用する)という違いがあります。

createNamedQuery の第一引数 "UserDataQuery" は以下の orm.xml の named-native-query の name属性 と関連付いています。

orm.xml
named-native-query の result-set-mapping="UserDataQueryDto" は sql-result-set-mapping name="UserDataQueryDto" で SQLと項目マッピング定義を関連付けしています。そして、 constructor-result target-class でクラスの完全修飾を記述してDTOクラスとの関連付けをします。

  • 寸評

SQL文にコメントで実行クエリー名をメモとして記載しておくことで、ログにもクエリ―名が出力され part-0,part-1と比べて 保守や障害時の調査にも役立ちそうです。

SQLファイルを外部ファイルに できたのですが、orm.xmlsql-result-set-mapping が せっかく Javaを使っているのに型の恩恵を受けられません。ちょっとしたtypoで動かなくなりますし、DTOクラスの場所を移動させても動かなくなります。非常にリファクタリングに弱い仕組みだと思います。

part-1(クラス)、part-2(XML) それぞれ単独では 帯に短し襷に長しです。
ということで 次の part-3 で両方を組み合わせていきます。

part-3:クラスとorm.xmlを組み合わせ

前述の通り、クラスとxmlの良いとこ取りをした実装例です。
私調べの範囲では ミックスした例はあまり紹介はされていませんでしたが個人的には有効な実装だと思っています。

@RequestScoped
public class UserDataQueryImpl implements UserDataQuery {

    @PersistenceContext
    EntityManager em;

    @Override
    public List<User> getUserData() {
        var results = em.createNamedQuery("UserDataQuery", UserDataQueryDto.class).getResultList();
        var users = results.stream().map((result) -> {
            var user = result.toUser();
            return user;
        }).collect(Collectors.toList());

        return users;
    }

}
public class UserDataQueryDto {

    private final String userName;
    private final String nickName;

    public UserDataQueryDto(String userName, String nickName) {
        this.userName = userName;
        this.nickName = nickName;
    }

    public User toUser() {
        var names = this.userName.split(" ");
        var _userName = new UserName(names[0], 2 <= names.length ? names[1] : "");
        var user = new User(_userName, this.nickName);
        return user;
    }

    /**
     * Dummy Class for ResultSet Mapping.
     *
     * <code>@Entity</code> class must set <code>@Id</code> to property. <br>
     * (Id field is not needed for DTOs.)
     */
    @SqlResultSetMapping(name = "UserDataQueryDto",
            classes = {
                @ConstructorResult(
                        targetClass = UserDataQueryDto.class,
                        columns = {
                            @ColumnResult(name = "userName", type = String.class),
                            @ColumnResult(name = "nickName", type = String.class),}
                )

            })
    @Entity
    static class MappingConfig implements Serializable {

        @Id
        String id;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
    xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm_2_2.xsd"
    version="2.2">

    <named-native-query name="UserDataQuery" result-set-mapping="UserDataQueryDto">
        <query>
<![CDATA[
/* UserDataQuery */
select userName, nickName from UserData;
]]>
        </query>
    </named-native-query>    
</entity-mappings>

各資産との関連付けは

  • createNamedQuery の第一引数 "UserDataQuery" は orm.xml の named-native-query のname属性
  • @SqlResultSetMapping(name = "UserDataQueryDto" ... は orm.xmll named-native-query の result-set-mapping属性

となります。

em.createNamedQueryの第二引数のクラス指定はメソッドの戻り値の型、@ConstructorResultの targetClass はDTOマッピング対象のクラス指定なので、同じクラス(UserDataQueryDto.class)を指定していますが 別の意図による指定です*5

  • 寸評

マッピングはクラスで実装しているので入力補完も出来ており、リファクタリングにも強い実装になっています。 外部SQLファイル(orm.xml)も、SQL文とマッピングキーだけというシンプルな構成です。

ここまでは orm.xmlを1ファイルだけで管理していました。クエリ―を追加するときには <named-native-query>を追加していくことになります。ただ、それだとチーム開発をする際に1つのファイルを奪い合うため競合が発生してしまいます。

ということで、次は外部SQLファイル(orm.xml)を複数に分けたいと思います。

part-4:外部SQLを複数orm.xmlで管理

クエリ―毎に orm.xml を作成する(複数orm.xmlで管理する)実装例です。
実際の開発では条件を固定してクエリ―を分けるという事はしませんが、ここでは例示を簡素にしたかったので 元のクエリーに絞り込み条件を追加しただけのものを準備しました。

@RequestScoped
public class UserDataQueryImpl implements UserDataQuery {

    @PersistenceContext
    EntityManager em;

    @Override
    public List<User> getUserData(String condition) {
        var results = condition.equals("data1")
                ? em.createNamedQuery("UserDataQuery.data1", UserDataQueryDto.class).getResultList()
                : em.createNamedQuery("UserDataQuery.data2", UserDataQueryDto.class).getResultList();
        var users = results.stream().map((result) -> {
            var user = result.toUser();
            return user;
        }).collect(Collectors.toList());

        return users;
    }

}
  • persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/jakartaee" 
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
    <persistence-unit name="PU" transaction-type="JTA" >
        <jta-data-source>jdbc/DemoDS</jta-data-source>
        <properties />
        <mapping-file>META-INF/orm-data1.xml</mapping-file>
        <mapping-file>META-INF/orm-data2.xml</mapping-file>

    </persistence-unit>
</persistence>
  • orm-data1.xml
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
    xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm_2_2.xsd"
    version="2.2">

    <named-native-query name="UserDataQuery.data1" result-set-mapping="UserDataQueryDto">
        <query>
<![CDATA[
/* UserDataQuery.data1 */
select userName, nickName from UserData where id = 1;
]]>
        </query>
    </named-native-query>    
</entity-mappings>
  • orm-data2.xml
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
    xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm_2_2.xsd"
    version="2.2">

    <named-native-query name="UserDataQuery.data2" result-set-mapping="UserDataQueryDto">
        <query>
<![CDATA[
/* UserDataQuery.data2 */
select userName, nickName from UserData where id = 2;
]]>
        </query>
    </named-native-query>    
</entity-mappings>
  • ファイル構成
 resources
  └ META-INF
      ├─ orm-data1.xml
      ├─ orm-data2.xml
      └─ persistence.xml

UserDataQueryImpl を見ていただければ part-3以前のものより、DTOマッピングのイメージが付きやすいかもしれません。 UserDataQueryDtoは受け取る型なので、使い回しが出来ます。

  • createNamedQuery の第一引数 "UserDataQuery" は orm.xml の named-native-query のname属性

ということで、クエリ―に追加する属性情報を付与したマッピング名にしてマッピングキーとしてユニークにしています。

orm-data1.xml、orm-data2.xml の格納場所は persistence.xml の mapping-file属性として クラスパスを列挙します。

  • 寸評

ということで、このパターンが最終稿になります。

なお、これまでクエリ―名と同じ文字列をマッピングキーにしていましたが、システム全体で重複しないようにしなければいけません。その意味ではクラスのFQDNにしておけば確実なのですが、先述の通り やりすぎるとリファクタリングのコストにもなります。
orm.xmlは クラスパス配下であればサブフォルダを作っても良いので、コンテキスト毎にサブフォルダを設けて衝突を避けるというやり方は可能です。

例えば、こんな感じです。

persistence.xml

└── META-INF
    ├── orm
    │   ├── context1
    │   │   └── orm-UserDataQuery.xml
    │   └── context2
    │       └── orm-UserDataQuery.xml
    ├── persistence.xml
    <named-native-query name="context1.UserDataQuery" result-set-mapping="UserDataQueryDto">
        <query>
<![CDATA[
(...)
]]>
        </query>
    </named-native-query>    

外部ファイルとマッピングキーの一意性については、色々と考え方はあると思いますので、よしなに決めていただければと思います。

part-5:DTOを中継しない

基本的には part-4で完結なのですが、当初構想であった クエリ―の結果を そのままJSON形式でクライアントに返却する実装例を最後におまけで追記しておきます。
フラットな配列構造を返却するのであれば、この方法で必要十分なこともあると思います。
あくまで私のレイヤー構成を前提としていますので、これでなければいけないということはありません。

package vermeer.sample.ee9.domain;

public class UserDataQueryDto {

    private final String userName;
    private final String nickName;

    public UserDataQueryDto(String userName, String nickName) {
        this.userName = userName;
        this.nickName = nickName;
    }

    public String getFirstName() {
        return this.userName.split(" ")[0];
    }

    public String getLastName() {
        var _names = this.userName.split(" ");
        return 2 <= _names.length ? _names[1] : "";
    }

    public String getNickName() {
        return nickName;
    }

}
@RequestScoped
public class UserDataQueryImpl implements UserDataQuery {

    @PersistenceContext
    EntityManager em;

    @Override
    public List<UserDataQueryDto> getUserData() {
        var results = em.createNamedQuery("UserDataQuery", UserDataQueryDto.class).getResultList();
        return results;
    }

}

/**
 * Dummy Class for ResultSet Mapping.
 *
 * <code>@Entity</code> class must set <code>@Id</code> to property. <br>
 * (Id field is not needed for DTOs.)
 */
@SqlResultSetMapping(name = "UserDataQueryDto",
        classes = {
            @ConstructorResult(
                    targetClass = UserDataQueryDto.class,
                    columns = {
                        @ColumnResult(name = "userName", type = String.class),
                        @ColumnResult(name = "nickName", type = String.class),}
            )

        })
@Entity
public class UserDataQueryMappingConfig implements Serializable {

    @Id
    String id;
}

part-0 で説明した通り、Domain層にはEEライブラリを含めて依存の無い状態にしたいため、UserDataQueryDto には @SqlResultSetMapping を記述しないようにしています。 代わりに UserDataQueryMappingConfigとして独立したクラスに変更しました。

せっかく(?)DTOを直接使用するということで、DTOのgetterに UserNameを加工するロジックを追加して Domain Objectととしてロジックを持たせた実装例にしています。 Jakarta RESTful Web Services(旧 JAX-RS)は JavaBeansの仕様に従った形式でJSON形式に編集をしてくれます。

  • 寸評

Domain ObjectのパターンとQueryDTOのパターンで分けるのが煩わしいければ*6、本例のようにマッピングクラスを作成するというルールにしても良いかもしれません。 個人的には、フラットなDTOをそのまま返却するよりも、ある程度の階層構造にしてクライアントへ返却するケースが多いと思うので、どちらかといえば本例のパターンがマイナーになるのではないか?と、今回いろいろと試す中で当初構想の逆の思いに至りました。

参考リンク

Cargoの使い方

JPA周りをあれこれやっているときに、ちょうど @backpaper0さんが Cargoを使った EEのリポジトリTweetされて そこから色々と参考にさせていただきました。 https://github.com/backpaper0/java-you

JPA関連

JPA では Read-Only の Entity は定義できない - A Memorandum

(spring-data-jpa)JPA2.1を利用してPOJO(エンティティクラス以外)を取得する方法 - Qiita

SQL文を外部ファイルに | 老いぼれSEの艱難辛苦

さいごに

5年位前にJPAを使ってあれこれとしたときには、EEサーバーを使わず*7無駄努力をしていました。自分が思いつく方法を手探りでやったので、orm.xmlのやり方など調べた記憶は多少ありますが、自前でsqlファイルを読み込むUtilを作ったりと、とにかく独自路線を突き進んでいました*8
今は 可能な限り独自実装は避け、EEを上手に使ったり、割り切る前提で考えるように変わったことで 色々と見える世界が変わったように思います。 月日の流れにより情報が増えたという事もありますが(例えば、参考としてSpringの実装例も役に立ちました)、多少は調べて試せるくらいにはなったのかもしれません。

cargoというものを初めて知りました。
ブログなどでコードを公開する際には、動く環境というのは凄くありがたいです。 mvnwを組み合わせることで、手元で環境構築することなく UberJarのように試せるのがすごく良いです。最低限の使い方であれば @backpaper0さんのものや 今回の私のプロジェクトを流用すれば簡単に始められると思います。

*1:Native Query かつ 外部ファイルが好きと言うのは、いつまで経っても昔のやり方に固執している感が否めないのですが、こればっかりは好みということで…

*2:https://codehaus-cargo.github.io/cargo/Home.html

*3:たとえば これがDDD的なのかもしれないけど

*4:という理解を私はしています

*5:というのが私の理解。ドキュメントまで追って確認はしていません

*6:開発規約として縛っておきたい

*7:EEサーバーを使いもせずに重いものだという思い込みから、tomcatにEclipceLink載せていた

*8:おかげでpom.xmlには依存記述が大量

Payara Server と Jakarta EE9 で Hello(他人のふんどし)

EEサーバー(Payara)を使った、簡単なREST Webアプリ。
利用するコードなど、人様のものなので 完全他人のふんどしの自分向けメモです。
ほとんど参考リンクで独学できるので そちらを見るのもいいでしょう。
今回は全くの新規ではなく、既存の雛形から肉付けしながら実装をしていく予定なので それを見越したメモです。
UberJarとかMicroProfileではなく「EEサーバー(フル)」を使います(Batchを使う可能性もあるので)。

目標

  • 開発環境で動かす
  • EEサーバーで動かす*1

環境

  • OS:Lubuntu 20.04(VirtualBox 6.1.22)
  • AdoptOpenJDK 11.0.11+9
  • NetBeans 12.4
  • Payara Server 5.2021.4

JDKを準備

今回は以前ダウンロードをして JAVA_HOMEに設定しているものを そのまま使用
(なので詳細は割愛)

EEサーバーを準備

Community Edition (Payara Server 5.2021.4 (Full)) をダウンロード

www.payara.fish

解凍もしておく。
Linuxなので権限回りもよしなに。
今回は開発用サーバーということ&権限周りの設定が面倒だったのでユーザーフォルダ(~直下)に「paraya5」として解凍しました。

開発環境でデプロイ

ダウンロード

https://netbeans.apache.org/download/nb124/nb124.html から

Apache-NetBeans-12.4-bin-linux-x64.sh

をダウンロード。

sh Apache-NetBeans-12.4-bin-linux-x64.sh

インストーラーを起動。
JDKは上記のものを選択。

www.torutk.com

を参考に見栄えとか日本語化とかをやって最低限の設定は終わり

コードを入手

今回は人様のコードを使用させていただきました。
JJUG CCC(Jakarta REST Examples for JJUG CCC 2021 Spring)で使用されていたコードです。

github.com

を git clone

手癖で、NetBeansからgit cloneではなく、ターミナルからコマンドで実行しました。

プロジェクトを選択

モノレポ構成でいくつかサンプルプロジェクトが格納されているので、どれかを選択。
私は「restapp」を選びました。

サーバーを設定(Payara Server)

Payara Toolsなどのプラグインを入れなくても、サーバー追加で選択肢として「Payara Server」が選べるので、それを選びます。

先のダウンロード&解凍したフォルダを選択。
ユーザー「admin」、パスワードは空白して起動。

実行&起動確認

ビルドして実行
ブラウザで

http://localhost:8080/restapp/api/hello

で起動確認

Payara Serverへデプロイ

 はじめに

  • NetBeansは閉じて、Serverもシャットダウンしていることを前提

実際は、こちらの サーバーへのデプロイをして コードに変更なくても動くのか? Payara Serverでのデプロイって こんな感じかな ?というのを お試しで先にやっています。
その際は warを作りたいプロジェクト配下のフォルダで(対象のpo.xmlがあるフォルダ)で

mvn package

して warファイルを作成しました。

この手順の流れであれば、NetBeansで実行確認した段階でwarファイルは出来ているので、それを使えばよいと思います。
targetフォルダ配下に出来ています。

Payara Serverを起動

{インストールフォルダ}/bin

に移動して

./asadmin start-domain domain1

Linuxなので、「./」をつけて実行してください*2

でサーバーを起動して

http://localhost:4848

以降は、こちらの動画を参考にしていただきます(手抜き)

youtu.be

すでにアプリとして「restapp」が存在しているかもしれません。
これはNetBeansで起動したものです。

動画のデプロイするwarファイルを、先に作成した target配下の restapp-1.0-SNAPSHOT.warを選択します。

  • CDI Development Mode

のチェックをONにします(しないとアプリが動きません)。
コンテキストを変えたい時は「Context Root:」を書き換えます。

admin画面から アプリを起動して

http://localhost:8080/restapp-1.0-SNAPSHOT/api/hello

へアクセスできたらOKです。

参考リンク

www.youtube.com

  • Payara Server と NetBeans による RESTful Web サービスの作成

www.youtube.com

余談

その1

IDEVS Codeにしてみようと試みましたが、試行錯誤の末 諦めました。

その2

Payara Serverにアプリを同じコンテキストのアプリをデプロイしていたので、NetBeansの開発用に起動したアプリと競合した。

./asadmin start-domain domain1

でserver(port:4848)に入って、不要なアプリを削除して競合を解消しました。
コンテキスト名を変えれば解消できたかも、と後から思ったりもしましたが serverでデプロイして動くことを確認することだけ 目標としていたことと、serverにデプロイして確認する(いわゆるIT的な試験環境)は 別環境を作るか 同居させるか その時になったら改めて考えます。

その3

そもそも何をしたかったのか?
雛形となる「初版となるpom.xml」が欲しかった。
Javaに限らず、システムの開発初期環境はハードルが高いものです。
たとえば、それが ここでいうところの「初版となるpom.xml」に相当します。

さいごに

かつて「ベンダーロックインを避けるぞ!」「EOLなどがあった時のために自分で置き換えられるようになるぞ!」「EEサーバーは重いらしいから避けよう」「余計なものが無いっていない方が軽いから良い」とか思って、Tomcat+mavenでコツコツ依存を設定したのですが、正直なところ勉強にはなったけど無駄なことをしていたと思います(苦笑)。
多少馴染があった(使ったことがあった)ので&warをフォルダに突っ込んで起動したらデプロイされるという簡便さを取って Tomcatにしたけれど、素直に金魚本(EE6)と寺田さんのブログに則ってGlassFishを使えばよかったです。
今回のシステムリプレイスでは その辺りの反省も踏まえて Payara Serverを使うことにしました。
技術力もないのにEOL心配とか寝言は寝て言えって話です(泣笑)。

*1:Tomcatからの卒業

*2:これをしなかったので、あれーコマンドが実行できないと、Linux素人の私は少し躓いてしまいました

【書評】川島さんのScrapbox

はじめに

いつも勉強になる 川島さんの著作(ブログ・スライド)の書評というか、何を読んだかメモ。
読む順番は順不同&気分次第です。

Scrapbox URL

kawasima

アーキテクチャ設計における垂直思考と水平思考

アーキテクチャ設計における垂直思考と水平思考 - kawasima

表題の考察については以下の書籍で色々と考えました。

結論としては、かなり違う切り口でしたが、なるほど という感じです。
自分としては、ロジカル(垂直)とラテラル(水平)についての区別なく「Why」「What」「How」くらいのザックリでした。

あわせて読みたい

www.slideshare.net

ソフトウェア設計練習帳

ソフトウェア設計練習帳 - kawasima

新人研修のシステム開発シナリオの素案にもなるかも

未読管理

未読管理 - kawasima

未読(もしくは読んだことにする範囲)の捉え方で管理方法は変わる

予約

予約 - kawasima

マイクロサービスで、取り消し(ロールバック)を実現しようとした場合の話だと理解しました。
境界を越えた整合性については、同期的であれ非同期的であれ、DBで実現できていたトランザクションに相当する機能を自分たちで実装しないといけないから、大変だという認識。
Akkaとか使えば良い感じになるのかな?(わかんないけど)

ページネーション

ページネーション - kawasima

オフセットベース
* 「最後のページへ」のようなページネーションは避けた方が良い。
理由:大きな結果セットに対して大きなオフセットの指定になるから。
SQLで降順に並べて順番に取得するってことは そういうことになるってことかな?
んーでも、それだと昇順でも結果セットの規模は同じか…
分かった気になったけど、多分 分かっていないということが分かった。

カーソルベース * 日付は一意にならないのでカーソルとしては使ってはいけない
「ID生成大全」にもありますが、厳密なソート順を求めると性能面でボトルネックが出来うるという事だけは押さえておいた方が良いような気もします。

  • 無限スクロールとも相性が良い
  • 特定のページへのダイレクトリンクはできない

諸々 雰囲気では分かったけど、多分 ちゃんと分かっていない…

あわせて読みたい

ID生成大全 - Qiita

Snowflake形式のIDを採用した場合の苦労ポイント - yoskhdia’s diary

アンケート

アンケート - kawasima

ERDの練習教材に使えそう

ログイン

ログイン - kawasima

世のログイン機能はメールアドレスや電話番号が本人のものであるという確証がとれていることを前提としていることが多いんだなーということを学べた。

検索条件保存

検索条件保存 - kawasima

保存形式は、、、僕だったら「QueryStringのまま」か「JSON」かなぁ。
検索条件による分析とか必要な場合は「検索条件パラメータごとにカラムを作る」を選択しがちだけど、そういった2次利用は それ専用として別途準備した方が良いと思っているので。

検索条件の再利用だけであれば、「QueryStringのまま」で、検索APIの仕様変更時の追随など 条件変更耐性的なところ鑑みた中間を狙ったら「JSON」かなー。

お気に入り

お気に入り - kawasima

お気に入り情報は「お気に入りに入れたときの情報」を保持する、というのは納得。
つまりは伝票と同じ。 某会計クラウドサービスで請求先名を変えたら 過去の請求書も書き換わったというのは あってはならないのとニュアンスは同じ*1

UIに関する記載(Ajax通信でAPI呼ぶだけ)というのも納得。

*1:今は解消されているかもしれないけど

Reactでの開発メモ

はじめに

vermeer.hatenablog.jp

を参考にしながら開発しながら思ったメモ
(※当面随時更新)

メモ

実装のベースとその理由

第13章のSuspenseにページルーティングはv6を使う。
まだ本リリースはしていない機能だけれど実装の印象だけで判断。
外部公開するものでもないので ライブラリが安定版であるかどうかじゃなくて直感的にわかりやすい書き方ができる方を優先しようと割り切り。
技術課題があったら、あったでその時考える。少なくとも書籍の範疇であれば「動くらしい」ということは保証されている割り切り。

本を読みながらとったメモ

  • useStateで状態と状態変更をする
  • useEffectで状態遷移(第2引数が監視対象の変数)を管理してステートパターン的に処理をする
  • useMemo を使って計算結果をコンポーネントシステムの外に保存しておく。依存配列が変わった時だけ再計算する。
  • useCallbackは関数のメモ化。関数を共有したいとき&依存配列が変わった時だけ再処理したいときに使う。
  • useRefは初期ロード時にリアルDOMへの参照を取得して処理する(初期フォーカスとか)&再レンダリングを伴わずに何らかのデータを関数コンポーネントで保存しておくときにも使える
  • useRef は useState と違って値の変更がコンポーネントの再レンダリングを発生させない
  • Custom Hook
    関数の名前の頭に『use』をつける
  • SPA におけるルーティングとは『DOM の動的な書き換えによってページ遷移を擬似的に実現するとともに、ブラウザのセッション履歴をそれに同期させること』
  • ほとんどの場面では Route は Switch といっしょに使う
  • <a> タグを使って書くと、そのリンクを踏んだ時点で React Router の管轄外となり管理していた履歴がすべて消える
    (Web サーバにリクエストが飛んで、SPA のコード全体がリロードされるため)
  • リンク先を設定する属性の to にはパスの文字列またはlocation オブジェクトを渡すことができる。
    location オブジェクトならパスの他にクエリパラメータやハッシュも設定できるし、ユーザーに見せたくない情報を埋め込んでリンク先に受け渡すこと ができる
  • boolean 型の属性である replace を指定すれば、クリックした時点でページの履歴が消える
  • useRouteMatch なら match オブジェクトをまるごと取得できる。マッチした URL パラメータにしか用がないときは useParams を使ったほうが便利
  • 『Atomic Design』という UI デザイン手法を適用
    Atoms、Molecules、Organisms、Templates、PagesのUI パーツを分ける
  • React Helmet
    ライブラリで、どこからでも HTML ドキュメントヘッダを動的に上書き
    SPA では意図して書き換えないと常に public/index.htmlに書かれた <title> の中身がどこのページでもそのページタイトルになるので注意
  • 任意のコンポーネントでいつでもルーティングできる(できてしまう)
  • useEffect を使ってコンポーネントの初回レンダリング時に強制的にトップにスクロールさせる(という実装例あり)
  • Modelじゃなくてコンポーネントを主軸にしてフロントエンドは実装するみたいなので設計や実装の考え方を変える必要あり
  • useReducerで状態変更の手段を複数持たせられる
    -- (状態管理の手段が単一の場合はuseState、複数の場合はuseReducerをつかう?)
  • useContextで状態を扱う
    --(Recoilもあるけどそちらは使わないことにする。Providerで親から渡してuseCallbankで振る舞いを実装)
  • 状態変更からリダイレクトでページ遷移するときは「Navigate」でパス指定する

ページ遷移

App.tsx にまとめて書く
Routerを作って、Appから呼び出すという実装にはしない RoutingはContainerの仕事(だからAppの仕事でもある)

LinkとNavigateはComponentで明示するとよい?(ページ遷移の副作用はViewで分かったほうが分かりやすい?)

パッケージ構成

  • ComponentとContainerの違いを意識する
  • https://qiita.com/ShiratoriTenta/items/7b4d2ea1a4d1d2336032
  • Reactに関係するものだけをComponentとContainerに格納
    --(それ以外はdomainという感じでフォルダを分ける)
  • contextはファクトリーと型をペアにして作成する(Containerフォルダに入れる)
  • (複数ページ、複数機能でのパッケージ構成は1つ目を作ってから改めて考える)

ファイル構成

  • Container側の関数名に「Enhanced」などを付与して名前衝突を回避する
    (Componentだけで画面構築ができるケースもある=Container側がオプショナルになると考えから)

実装の順番(ToDoメモ的なもの)

初回の作成順序

一回作って全体の工程を把握しつつ実際に動くものを作る流れの整理

  • ベースのページ枠を作る(Component)
  • ページ遷移を試す(単純なリンク)
  • ページ内の動作や動的書き換え部分を作る(状態遷移で宣言的に)
    -- (ここまではデータ分離はしていない)
  • データ(固定)の埋め込み動的なモック状態へ
    -- (この時点ではコンポーネントを分割しないで割とベタに1つのファイルでゴリゴリ)
    --(ここまではコンポーネントの切り替えのContext以外は使用していない)
  • ページ内で使用するモデル(type)を作る
  • アプリの主要な機能を実装
    --(機能実現のための状態管理を含めて実装)
  • 共通機能であるタイマーコンポーネントを作成して埋め込む
    -- (ここまででデザインはともかくとして主要な機能は出来ている)
  • ファイル分割をする
  • CSS in JSを使ってデザイン(Component)を組み入れる
  • StoryBookを使ってコンポーネントのデザインを確認できるようにする
  • 状態によってCSSを変えたい部分の追加実装(状態遷移)
  • テストを実装
  • (E2Eは迷い中。まずはそれ以外かな。)

2つ目のアプリに向けて

  • monorepo構成に変更
  • アプリ共通部分と個別の部分を分ける
  • buildをして意図した資産出力がされるか確認する
  • script周りを整理(test)
  • パッケージの役割と実装パターンを整理
    -- (動くだけモノから、次の開発プロセス的やアーキテクト的な整理をちゃんとする)

2つ目アプリ

(ざっくり。1つ目を作った後で見直す)

  • ComponentとStoryBookで表記コンポーネントだけを実装
  • モデル(type)を作成
  • 検証用のデータを作成
  • Containerを作成
  • Reducer(とContext)で作成(状態管理を実装)
  • ContainerとContextを連携
  • StoryBookを修正(Container分割でPageのStoryBookが壊れているため)
  • テストを実装(最低限はStoryShot)

理解のイメージ

AppはルートとなるContainer。次にPageのComponent。
部品となるPanelのContainerへ問いかけて ViewとなるComponentを受け取って表示。
これを繰り返したミルフィーユでReactは画面を構築する。
僕にとってなじみの画面構築はテンプレートということが ようやくわかった気がする。テンプレートはViewのパーツを組み合わせて、そこからContainerに相当するアクションを呼び出す。
(思考が1つ前に進んだ気がする。)

実装の流れ

containerとComponentの往復をしながら作り込んでいく。
はじめは往復が面倒だったけど、フォルダであれファイルであれ デザインと実装の分離はStoryBookの利用も考えると分けておきたい。
いずれにしても何かしらの分離は必須だから多かれ少なかれ往復は発生するので心理的には許容範囲。
(当初は「理解のイメージ」がピタッときていなかったけれど、それがイメージできたら急に読みやすくなった。)

イベント処理

useKeyPress React Hook - useHooks

Reactでkeydownイベントを取得する方法

お作法

命名

  • React では、イベントを表す props には on[Event] という名前、イベントを処理するメソッドには handle[Event] という名前を付けるのが慣習となっています。
  • コンポーネントが自身の key について確認する方法はありません。
  • 配列のインデックスを使う場合と同様な問題が生じるためほとんどの場合は推奨されません。
    key はグローバルに一意である必要はありません。コンポーネントとその兄弟の間で一意であれば十分です。

チュートリアル:React の導入 – React

Reducer(もしくはAction/dispatch)

ステートの扱い
Reduxを使わなくとも、useReducerを使うにあたって実装パターンとして踏襲するべきように思う。
* 書籍(3部 P62近辺) * (prevState,action) => nextState * actionが同じなら「差分も同じ」ことが保証される

Reducer ファイルの基本構造

関数毎にファイルを分けてもいいけど、1つにまとめて良いかな

const xxxReducer = (state: StateType, action: ActionType): StateType

のところでJSDocでActionでやるべきことを書きながらステートとアクションとその詳細で何をするのか先に書いておいてから具体的な実装をする感じ。
実装をしながら必要なステートがあったら適宜追加していく

// type ValueOf<T> = T[keyof T];

export const actionKeys = [
  'initialized',
] as const;

export type PayloadType = {
  value: string;
};

export type ActionType = {
  type: typeof actionKeys[number];
  payload: PayloadType;
};

export type StateType = {
  stateValue: typeof actionKeys[number];
  value: string;
};

export const initializedState = (): StateType => {
  return {
    stateValue: 'initialized',
    value: ``,
  };
};

const xxxReducer = (state: StateType, action: ActionType): StateType => {
  switch (action.type) {
    /**
     * 状態初期化
     */
    case "initialized": {
      return {
        ...state,
      };
    }
    /**
     * 上記以外のアクションは状態変化なし
     */
    default: {
      return state;
    }
  }
};

export default xxxReducer;

関連資産の雛形もついでに

import { createContext } from 'react';
import { ActionType, StateType } from './XXXReducer';

export type XXXContextType =StateType & {
  dispatch: React.Dispatch<ActionType>;
};

export const XXXContext = createContext<XXXContextType>(
  {} as XXXContextType,
);
import React, { FC, useReducer } from 'react';
import {XXXContext} from "./XXXContext";
import xxxReducer, { initializedState } from './XXXReducer';

const XXXProvider: FC = ({ children }) => {
  const [{ value }, dispatch] = useReducer(XXXReducer, initializedState(),);

  return (
    <XXXContext.Provider
      value={{
        value,
        dispatch,
      }}
    >
      {children}
    </XXXContext.Provider>
  );
};

export default XXXProvider;

StoryBook ファイルの基本構造

ページ全体を確認するStoryBook
PageStoryを使うことで、Providerで対象ページコンポーネントを挟み込む
Contextから情報を取得しないで良いものには不要

import React, { FC, ComponentProps } from 'react';
import { Story } from '@storybook/react/types-6-0';
import XXXComponent from './XXXComponent';

type PageProps = {
  value: string;
};

const PageStory: FC<PageProps> = (args: PageProps) => {
  return (
    <>
      <XXXComponent
        value={args.value}
      />
    </>
  );
};

export default {
  title: 'page-name/Page',
  component: PageStory,
};

const Template: Story<ComponentProps<typeof PageStory>> = (args: PageProps) => (
  <PageStory
    value={args.value}
  />
);

export const FirstStory = Template.bind({});
FirstStory.args = {
  value: '',
};


useEffect完全ガイドを読みつつのメモ(抜粋のみ含む)

useEffect完全ガイド — Overreacted

  • Effectは props や state の変化を DOM にシンクロさせるという思考で
  • props や state を必要としない関数はコンポーネント外にして、エフェクトでしか使われない関数はエフェクト内に入れる
  • エフェクトがレンダースコープ内の関数を使うことがあるのであれば(props からの関数も含む)、 useCallback で関数が定義されている場所をラップしてそのプロセスをリピート

さいごに

開発日誌的なTweetをぶら下げます

2021年のスタートで、間違えて、JSFツイートにぶら下げてしまったので そちらのリンク

【書評】りあクト! TypeScriptで始めるつらくないReact開発 第3版

oukayuka.booth.pm

oukayuka.booth.pm

oukayuka.booth.pm

総評

とても良かった
他の人にも勧められる書籍です
技術メンターになる人が周りにいない場合*1や、他のメンターの意見も聞いてみたい人にお勧めです
出版時点(2020/9)の最新動向まで書かれているので、一昔前のスタンダードを追いかけるのではなく、過去の経緯を踏まえて 今 こういう流れになっているというバックボーンの理解も深まります

はじめに

私のバックボーンなど

私はJavaのサーバーサイドのプログラマとして*2、家業の学習ソフトのリプレイス(MSXからブラウザベースへ)を8年前くらいに一人でやったという経歴です。
jQuery(&jQueryMobile)を使って、オレオレアーキテクトを考えて ゴリゴリとオブジェクト指向*3で作り込みつつ それなりに動くものを作りました。
サーバーサイドの見直し*4を含めて考え直している中で、クライアントアプリも技術素振りも兼ねて見直そうと思い立ちます。

あと、JavaScriptについては Node.jsでちょっとしたアプリを作ったことはあるという感じです。

技術選定(ReactかVue.jsか)

JavaScript名前空間でグローバルに扱うプログラムも慣れれば そういうものとして悪くは無いですし、動的型付けな面白さも悪くはないと思っています。
が、いかんせん年々記憶力も落ちており Javaのような型があると やっぱりうれしいのでTypeScriptは導入してみたいというモチベーションがありました*5

このあたりも見つつReactにしようと決定

完全に独断と偏見だけどReact vs Vue してみた - Qiita

書評(or 感想)

ようやく本題

JavaScriptの実装とTypeScriptの1冊目

jQueryを使いつつクラス志向の開発はしたことはあるので、ブラウザのちょっとした操作を実装したという経歴以上ではあると思いますが、独学ベースでツギハギでした。
内容としてはReactに直結はしなかったところが多かったかもしれませんが、私としては逆にラッキーでした。
あと他の本で「Create React Appを使わず、自分でwebpack 使っていきましょう(意訳)」とあったのですが、本書にて「webpack職人になれますか?(意訳)」とあり、「確かに 目的はアプリを作ることだから 本当に必要になるまではwebpackによる構築面はスルーしたほうが良いだろう」と踏みとどまれました*6

Reactの基本を学ぶ2冊目

Reactによる実装の基本とJSXとは、ということが学べます。
JSXの好き嫌いは 分からなくは無いですが、結局 動的に形成したものを見て確認するのだから、テンプレート型(Vue.js)であれ 似たようなことだと割り切っているので、私としては そもそも気になりませんでした*7
そもそもテンプレートをベースとした静的なページはJavaで実装して(している)、動的アプリ部分をJavaScriptで構築する(している)の前提ですし。

あと、具体的な開発に必要なツールを含めた開発環境構築に大きな割合を割いているのも、私にはよかったです。
Linterについては、過去の系譜といった ある種 豆知識的な技術選定の根拠を個人的には知りたいので*8、読み物として面白かったです。

Reactによる状態管理を学ぶ3冊目

よほど簡易なものでない限り、アプリケーションにおいて状態管理は必要です。
最新状況に至るまで、これまでのReactにおける状態管理の実装について丁寧に示されています。
読み手の状況(新規開発なのか、既存アプリのメンテナンスなのか)に応じて広く使える内容だと思います。
たとえば 現場によっては、Reduxを使ったり、Redux以前のやり方を使ったり、色々あると思います。
過去の系譜をなぞってコードベースで説明されているので、プロジェクトの実態に合っている部分が どこかしらにあると思います。個々の状況に応じて適用していけばいいでしょう。
私の場合は完全リプレイスを目指しているので 本書の示すオチをベースに、その他の要素を引き算して(遡って)、どこを落としどころを見極めれば良いという使い方ができます。 例えば著者の言うマイクロフロントなアプリを目的としていないし、現時点でテストツールが充実していそうな Redux&REST APIにしたほうが良いかな?という感じで適用範囲を考えられます。

読み方

正直なところ、はじめはコードも理解しつつ読むようにしていたのですが「おそらく この後で より良い変遷を示されるだろう」と思ってからは、コード部分については飛ばし気味で読みました。
上述もしましたが、それはその部分が無意味ということではなくて、個々の現場で それぞれのフェーズに遭遇した時に照らし合わせて読み込めば良いだろうという判断の下で そういう読み方に変えました。
その意味でも本書の守備範疇は広いように思います。

役に立つ

総じて、Reactを知る本ではなくて、Reactを使ってJavaScript(&TypeScript)によるアプリ開発をする本だったという印象です。
書籍のサンプルはGitHubでも公開されています。
書籍によっては、ツギハギな情報だけを示して、実際の全体像が分からないということもありますが、そういうこともありません。 実際に自分のローカルに落として動かしてみることができます*9

さいごに

一回読んだだけなので、まだまだ理解は浅いです。
また私は本を読むときには読むことに専念して手を動かすことを後回しにしています。 ですので再度読み返しをしつつ 手も動かそうと思います。
また、初見の感想を記録として残した上で、改めて本書の活用をしつつ 以下の書籍も購入してしたのでテスト周りのやり方もあわせて学ぼうかと思っています。

oukayuka.booth.pm

追記(2020/11/13)

「りあクト! TypeScriptで極める現場のReact開発」読了

おすすめは 6章「プロフェッショナル React の流儀」
開発プロセスへの言及がとても良かったです。

先の書籍を購入された方は、この書籍を「締め」に読むと良いと思います。 開発プロセスについては、個人的には スッとなじめました。
こちらのサンプルの基本構成をベースに開発すれば、個人開発でも ツギハギではない 一気通貫した思想の下で開発ができる手ごたえのようなものを感じました。

繰り返しになりますが、せっかく先の3冊を読んだのであれば この本も必ず読んでほしいと思える本です。

追々記

書籍のサンプルにあわせて、Sagaで状態遷移を考えていたけれど以下のコメントをいただきました。

ということで サンプルを自分なりにアレンジして 理解を深めつつ あれこれやってみる余地もありそうです。
あれこれすることも含めて「技術素振り」の範疇だと思っていますし、この書籍があったお陰で「技術調査および自分にあっているスタイルの検討」の手探りの時間を 大幅にショートカットできたと思っています。

追々々記(2021/1/4)

「りあクト! TypeScriptで極める現場のReact開発」ではStorybookのバージョンが5系であるため追記時点の6系とは推奨される書き方やaddonが違います。
目的と役割については流用できますが、実装については独自に調べる必要があります。


*1:私は主にこれです

*2:厳密には現役SIer時代はPLでプログラマでもない。退職してからプログラミングした人。

*3:クラス志向な部品を作ったり、継承も使ったり云々

*4:Javaのバージョンアップとか TomcatからPayaraへの乗り換えとか

*5:そのためにオレオレアーキテクトで部品作ったりもしましたが、、まぁ当初の思惑は作っているときには それなりに覚えていますが、数カ月もたつと、、という良くある話

*6:悪い意味で、私は 本当にやりたいことがなかなか進められない性格なので、立ち止まれたことは多分 良いことだと思っています。この反省の下 Payaraを使おうと考え直したわけですし

*7:ふむふむ、こうやって画面構築するんですね、くらいの温度感

*8:気になるという言い方でも良いですが

*9:まだ動かしていないんですけどね(汗)