Java読書会BOF 主催「Spring徹底入門 第2版」を読む会(第14回)を12月20日(土)に開催しました。
今回は、12章 Thymeleafの残りと14章チュートリアルの途中、エンティティの実装まで読み進めました。14章のチュートリアルは、参加者がそれぞれ持参のPCで実際にPostgreSQLのユーザー・データベース作成、Spring Bootのプロジェクトを作成してソースコードの打ち込みをしながら進めました。
チュートリアルにおいて引っかかったことなど
実際に手を動かしながら実施すると、書籍を読むだけでは気づかないことや引っかかる事項がいろいろと出てきます。これらを調べ議論しながら進めたことで知識が深まり知見が広がりました。
Spring Framework、Spring Bootのバージョン
先月2025年11月に、Spring Framework 7.0 および Spring Boot 4.0がリリースされました。 書籍で言及されているSpring Bootの構築は、Webサービスの Spring Initializr を利用して作成します。Spring Initializrで選択可能なSpring Bootバージョンは、最新バージョンと一つ前のマイナーバージョンで本日時点では 4.0.1または3.5.9 となっています。書籍のチュートリアルは Spring Boot 2.7を前提としていますが、事前に試した限りでは 3.5でも 一部修正をすればビルド・実行は可能でした。
修正:依存関係でthymeleaf-extras-springsecurity4 をthymeleaf-extras-springsecurity6 に修正
PostgreSQL ユーザー作成時のパスワード指定
書籍では、次のように mrs ユーザーを作成するPostgreSQLコマンドで、パスワードにmd5ハッシュコード化したものを指定しています。
CREATE ROLE mrs LOGIN
ENCRYPTED PASSWORD 'md586082399b5082acb54472ee195a57ce8' ...
PostgreSQLは、バージョン10あたりから ENCRYPTEDの指定は意味をなさなくなり(後方互換性のために残されている)、パスワード文字列がハッシュコードの場合はそのまま(再ハッシュせずに)パスワードとして保存されます。md5ハッシュを使用するときは、先頭にmd5を付け、その後ろにmd5ハッシュ文字列を指定します。指定可能なハッシュは、md5およびSCRAM-SHA256です。
ここで発生した問題は、書籍ではユーザーのパスワードを mrs と記載しているのですが、上述のCREATE ROLE文で指定しているパスワードのmd5ハッシュ文字列が、mrsをmd5ハッシュ化したものと一致しないことでした。書籍の通りユーザーを作成すると認証ができません。 世の中には、md5のハッシュから元の文字列を逆生成するWebサイトがあり、書籍のmd5ハッシュを入れたところ、mrsmrs と結果が十秒足らずで得られました。md5はかなり脆弱ですね。
ところで、PostgreSQLサーバーとはSSH接続でコマンドを実行するなら、パスワードを平文で指定してもいいのかなと思いましたが、この関連でインターネットを調べていたときに、コマンド実行履歴(history)で見れてしまうという点を見かけ、なるほどと思いました。とはいえ、平文をハッシュにするときをどうするか、もあるので平文パスワードを扱う場所をどこに限定するかは別途検討すべき事項と思いました。
LC_COLLATE と LC_CTYPE
データベースを作成するときのパラメータで、書籍ではどちらも "C" を指定していました。 LC_COLLATEは、文字列の並び順(照合順)を決める設定で、Cは、Unicodeの文字(コードポイント)のバイト順で判断するが、高速に動作し、ja_JP.UTF-8 を指定すると日本語の文字も考慮された並びとなるが、動作が遅くなるという模様です。
LC_CTYPEは、文字の種類を分類するための設定で、文字、数字、大文字、小文字などの区別をするものになります。Cは、ASCII文字範囲(英字)を分類します。
テーブル名 usr
なぜかテーブル名に usr と省略形を使っています。userではいけないのかな? と疑問に思いました。 これは、userとしてしまうと、SQL の予約語(CREATE USER ... のUSER)と衝突してエラーになってしまいます。 推奨の回避方法は、名前をuser以外にする(推奨は複数形のusersですが)、あまりよくない回避方法は"user"とクォートする、ただしこの場合、JPAが生成するSQLでもuserをクォートさせる設定を別途行う必要があり、筋が悪い回避方法となってしまいます。
複合キーのクラスReservableRoomId でequalsとhashCodeのオーバーライド
複合キーを使用するエンティティ(テーブル)で、複合キーを表すクラスは包含するキーの値が同じ場合はインスタンスが違っていても同値とする(equalsメソッドがtrueを返し、hashCodeメソッドが同じ値を返す)実装が必要です。
書籍では、次のようなequalsメソッドとhashCodeメソッドの実装をしています。
@Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ReservableRoomId other = (ReservableRoomId) obj; if (reservedDate == null) { if (other.reservedDate != null) { return false; } } else if (!reservedDate.equals(other.reservedDate)) { return false; } if (roomId == null) { if (other.roomId != null) { return false; } } else if (!roomId.equals(other.roomId)) { return false; } return true; }
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((reservedDate == null) ? 0 : reservedDate.hashCode()); result = prime * result + ((roomId == null) ? 0 : roomId.hashCode()); return result; }
Javaでは、同値の判定をするオブジェクト(値オブジェクト)を実装する場合は、equalsとhashCodeをオーバーライドして同値判定の実装をするのが定石です。
IDEにIntelliJ IDEAを使っていたので、試しに IntelliJのコード生成機能で equalsとhashCodeメソッドの実装を生成したところ、次のようなコードとなりました。
@Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; ReservableRoomId that = (ReservableRoomId) o; return Objects.equals(roomId, that.roomId) && Objects.equals(reservedDate, that.reservedDate); }
@Override public int hashCode() { return Objects.hash(roomId, reservedDate); }
java.util.Objectsクラス(JDK 7から導入されたユーティリティクラス)の equals(Object, Object)メソッドとhash(Object...)メソッドを使って、とっても簡潔な実装となっています。
読書会参加メンバーでEclipseを使用している人に、同様に equalsとhashCodeのメソッドをコード生成してもらったところ、java.util.Objects を使用せず、書籍と同じような長いコードが生成されていました。
ということで、IntelliJ IDEAが コード生成機能をきちんと洗練していることが分かりました。
schema.sql
Spring Boot で、application.propertiesに、spring.sql.init.mode=always と指定すると、Spring Bootアプリケーションを起動したときに、src/main/resources/ ディレクトリの下にある schema.sql ファイルと data.sql ファイルを実行します。
schema.sqlには、DDL文を、data.sql には初期データをインサートするSQLを記述します。 書籍では、schema.sql の中に、外部参照の制約をCONSTRAINTで定義しますが、CONSTRAINTに、ハッシュのような長い文字列が付いています。
例
ALTER TABLE reservable_room ADD CONSTRAINT FK_f4wnx2qj0d59s9tl1q5800fw7 FOREIGN KEY (room_id) REFERENCES meeting_room;
これはどうやら Hibernate のスキーマ生成ツール hbm2ddl がJPAのアノテーション付きエンティティからSQLのDDL文を生成しているもののようです。
シンプルに外部参照を指定するなら、CREATE TABLEの中に記述することもできます。
CREATE TABLE reservable_room ( room_id INT4 NOT NULL REFERENCES meeting_room (room_id), :
schema.sql と data.sql に記載のSQL文は、1文ずつJDBC経由でデータベースに投入されているようです。
読書会の冒頭、アフターでの話題
パッケージ構成(ディレクトリ構成)
Spring のパッケージ構成は、書籍のサンプル・チュートリアルのように レイヤーの観点でまずパッケージを構成し、それぞれの中で必要に応じて機能ごとのサブパッケージを構成する方法(package by layer)と、アプリケーションの業務(機能)の観点でパッケージを構成、それぞれの中で必要に応じてレイヤーごとのサブパッケージを構成する方法(package by feature)とがよく適用されています。
src/main/java/com/torutk/hello +- controller/ | +- login/ | +- search/ | +- edit/ +- model/ +- service/ +- repository/
レイヤー観点の分割が上位のパッケージにあると、一つのアプリケーションを構成するソースファイルがかなり広範囲に散らばるので、まとまりという点では機能観点での分割が良さそうに思いました。
MavenはXMLで設定を書かなくても良い
pom.xmlは、XMLなので人の目には少し厳しい(タグの記述がコンテンツよりも目立つ・多い)ですが、最近(しばらく前から?)はXMLではなくYAML形式のpom.yml で記述できるようです。
Maven使っている読書会参加者も知らなかったようです。 Gradleを使っているので、どれだけ利便性が向上するかは分かりませんが、反応はよさそうでした。