Hibenateで関連エンティティが存在しない場合の例外を抑制する

JPAの実装としてHibenateを使っている。

既存のテーブルに対して、新規にエンティティクラスを追加し、 @OneToOne で関連を追加したが、関連元のフィールドがnullを設定可能になっている。

その状態で関連元のエンティティを参照すると、該当のカラムがnullの場合、例外が発生した。

テーブル定義を変えるのは難しく、アノテーションや設定でどうにかできないか調べたのでメモ。

環境

Hibernate 5.6.3.Final。Spring BootからSpring Data JPAを経由して利用。

問題

関連元となるJPA Entityに、以下のような関連を設定。

@OneToOne
@JoinColumn(name = "related_id", insertable = false, updatable = false)
private RelatedEntity relatedEntity;

関連付けたテーブルの、 related_id がnullの場合、関連元のエンティティを参照すると、 javax.persistence.EntityNotFoundException が発生する。

@OneToOne アノテーションに、 optional = true を指定しても解消せず。

OneToOne (Jakarta EE 8 Specification APIs)

対応

Hibernateの独自アノテーションになるが、対象のフィールドに @NotFound(action = NotFoundAction.IGNORE) を追加すると、例外が発生せず、フィールドがnullとなるようになった。

import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;

@OneToOne
@JoinColumn(name = "related_id", insertable = false, updatable = false)
@NotFound(action = NotFoundAction.IGNORE)
private RelatedEntity relatedEntity;

その他のJPA実装の場合

未検証だが、@ManyToOne の場合、Hibenateは同様に例外が発生、それ以外の実装では例外は発生しない模様。

hayassh.hatenadiary.org

振り返り

Javadocを読む限り、 @OneToOne(optional = true) でうまくいくかと思ったが駄目だった。詳細については未調査。

JavaのコレクションファクトリーメソッドやGuavaのImmutableCollectionは、nullを要素に使えない

Java9で追加された List#of などのファクトリーメソッドや、Java10で追加された Collectors#toUnmodifiableList などのイミュータブルなコレクション生成をよく使っているが、nullを含んだ値を指定したら NullPointerException が発生した。

標準APIやGuavaのImmutableListなどでも、不変のコレクションを生成するメソッドでは要素にnullを設定できなかったのでメモ。

Java標準API

List

Java9で追加された List#of、Java10で追加された List#copyOfCollectors#toUnmodifiableList で、変更不能リストを生成する場合、要素はnull不可となる。

それぞれ List および CollectorsJavadocに記載あり。

List#of および List#copyOf については、Listのクラスに対するJavadocにも詳細がある。

Set

Listと同様、 Set#ofSet#copyOfCollectors#toUnmodifiableSet で変更不能セットを生成する場合、要素はnull不可となる。

これまた SetJavadocに記載あり、クラスに対するJavadocに詳細があるのも同様。

Map

こちらも Map#ofMap#copyOfCollectors#toUnmodifiableMap で変更不能マップを生成する場合、キーおよび値はnull不可となる。 MapJavadocに記載があるのも同じ。

Guava

ImmutableListImmutableSetImmutableMap など、 com.google.common.collect 内の Immutable で始まるクラスは、すべて要素やキーと値がnull不可となっている。

詳細は、ImmutableCollection に記載あり。 ImmutableMap は継承関係にないが、継承しているか否かにかかわらず、 Immutable で始まるクラスに適応されるとのこと。

The remainder of this documentation applies to every public Immutable- type in this package, whether it is a subtype of ImmutableCollection or not.

nullを含むコレクションを変更不可コレクションにするには

nullを要素に含んだListやSet、値に含んだMapを変更不可にするには、Optionalでラップするのが手っ取り早い。

ListやSetであれば、Streamにしてmapで変換すればいい。

// of の置き換え
Stream.of(...)
        .map(Optional::ofNullable)
        .collect(Collectors.toUnmodifiableList());

// copyOf の置き換え
values.stream()
        .map(Optional::ofNullable)
        .collect(Collectors.toUnmodifiableList());

Mapの場合、すでにコピー元のインスタンスが存在すれば、Guavaの Maps#transformValues を使うと簡単。

Map.copyOf(Maps.transformValues(map, Optional::ofNullable));

Map#of を使っていた場合、変更可能なマップを生成する同様のメソッドがないため、HashMapなど適当なMapインスタンスを生成した後に、 Maps#transformValues で変換か。

キーがnullのMapは、まず使わないので無視してよさそう。

振り返り

nullが入らないことで、nullチェックが不要となり、かなりシンプルに使えるようになるため、この仕様はありがたい。

ただ、ListやSet、Mapといったインタフェースで変数や戻り値を宣言すると、要素などがnull不可という情報が消えてしまう。Guavaが使える環境であれば、ImmutableCollectionのJavadocにあるように、ImmutableListなどを変数や戻り値に使っておくのがよさそう。

For field types and method return types, you should generally use the immutable type (such as ImmutableList) instead of the general collection interface type (such as List). This communicates to your callers all of the semantic guarantees listed above, which is almost always very useful information.

Spring DATA Repositoryの@Queryによる検索の戻り値を、DTOやインターフェースにする

Spring DATAのJPA Repositoryを使っているが、戻り値をJPA Entity全体ではなく、任意のフィールドだけにしたいという話があった。

単純にSQLのように SELECT フィールド, フィールド... すると Object の配列になるが、それは避けて任意のDTOやインターフェースにする方法を聞かれたのでメモ。

DTOの場合

必要なフィールドを引数に取る、コンストラクタをもったクラスを用意する。

package example;

@Getter
@AllArgsConstructor
class ResultDto {
    private String id;
    private String name;
}

@Query 内で、 new DTOの完全修飾クラス名(コンストラクタ引数...) を書くと、メソッドの戻り値をDTOにできる。

@Query("SELECT new example.ResultDto(e.id, e.name) FROM EntityClass e")
List<ResultDto> findResultDtoAll();

インターフェースの場合

Getterだけのインターフェースを用意。

interface ResultInterface {
    String getId();
    String getName();
}

@Query 内で、 SELECT エンティティクラス.フィールド... を書くと、メソッドの戻り値をインターフェースにできる。

@Query("SELECT e.id, e.name FROM EntityClass e")
List<ResultInterface> findResultInterfaceAll();

ちなみに実態としては、java.sql.ResultSet に動的プロキシでインターフェースをかぶせている模様。エンティティクラスのフィールドと型が合わない場合などは、Getterメソッド呼び出し時に例外が発生する。

参考

Stack Overflowに質問があり、DTOの場合とインターフェースの場合の両方が記載されている。

stackoverflow.com

DevToysの1.0.1.0から1.0.3.0への変更点

前回、DevToysについて書いた直後に、v1.0.2.0で日本語化されたりしたので、変更点をメモ。

devtoys.app

環境

DevToys v1.0.3.0。Microsoft Store経由でインストールした。

変更点

多言語対応

v1.0.2.0で日本語を含む多言語に、プラグインなどではなくネイティブで対応。v1.0.3.0で韓国語も追加されていた。

v1.0.2.0の時点では、新たに追加されたツールは翻訳されていなかったため、機能実装と翻訳にはタイムラグがあることもありそう。

v1.0.1.0時点で実装されていたカテゴリおよびツールのうち、英語と日本語で差異があるものは以下。

「すべてのツール」やカテゴリ名をクリックした際のカード表示では、「ツール名 カテゴリ名」で表示される。例えば、 Json <> Yaml であれば「Json <> Yaml 変換ツール」となっている。

日本ではあまりなじみのなさそうな、 Lorem Ipsum にダミーテキストである旨明記されたのはわかりやすい。

色覚異常シミュレーション では、「Protanopia simulation」、「Tritanopia simulation」、「Deuteranopia simulation」がそれぞれ「(P) 1 型 2 色覚」、「(T) 3 型 2 色覚」、「(D) 2 型 2 色覚」となっていた。

また、 PNG / JPEG 最適化色覚異常シミュレーション の順序が入れ替わった模様。

新ツール

GZip を利用した文字列の圧縮と展開

エンコーダー / デコーダー に追加。v1.0.3.0時点では、左フレームだと GZip となっている。

圧縮の場合、文字列をgzipで圧縮してからBase64変換する。展開ではその逆。

SQLフォーマッター、XMLフォーマッター

フォーマッター に追加。そのままSQLXMLのフォーマッター。

SQLフォーマッターでは、デフォルトの Standard SQL 以外に、MySQLPostgreSQLAmazon Redshiftなどを選択できる。

試してみたところ、SELECT句でのカンマ位置が行末だった。個人的には行頭につけたいが、変更はできない模様。

チェックサム

生成ツール に追加。ファイルを指定して、そのファイルのチェックサムを表示、およびテキストとの比較ができる。地味に便利。

ハッシュアルゴリズムは、MD5, SHA1, SHA256, SHA384, SHA512が選択可能。

画像フォーマット変換

グラフィック に追加。PNG, JPEG, BMPのいずれかを読み込み、同じくPNG, JPEG, BMPのいずれかに変換する。

PNG / JPEG最適化 と同様、フォルダ読み込みも可能。

ロスレス変換ということで、JPEGにしてもファイルサイズはそこそこ大きくなる。

Chocolatey対応

パッケージマネージャーとしては、v1.0.1.0の段階でWinGetに対応していたが、v1.0.3.0でChocolateyにも対応。

community.chocolatey.org

Microsoft Store経由でインストールすると、常に最新版になるため、バージョン指定してインストールしたい場合はChocolateyを使うのもよさそう。

振り返り

すごい勢いで機能追加されている。READMEを見ると and more are coming とあるので、まだまだ追加されそう。

とはいえ、個人的に便利だと思う機能は一通りあるので、増えすぎてメニューがごちゃごちゃしないかがちょっと心配。

余談

Mac版クローンのほうも結構な速度で開発されている模様。

2022/2/19時点で、オリジナルがGitHubスター8.1kに対し、クローン版は4.2kになっていた。

github.com

開発者向けの便利ツール集、DevToysを試してみる

窓の杜の記事を見て気になり、DevToysのツール群をひととおり試してみた。

また、Windows以外での代替アプリも調べたのでメモ。

devtoys.app

2022/2/7追記: v1.0.2.0で日本語化されたが、この記事の記述時点では未翻訳のため、ツール名などは英語で記載している。

環境

DevToys v1.0.1.0。Microsoft Store経由でインストールした。

DevToysとは

公式でうたっている「A Swiss Army knife for developers.」の通り、開発者向けの便利ツールのセット。

いくつかのWebサービスで用意されているような変換処理などをまとめてくれている。

また、v1.0.1.0の時点ではオフラインで動作するため、Webサービスの利用がためらわれるような文字列の変換などにも使えそう。

各ツールへの導線

左フレームにいくつかのカテゴリが表示されており、カテゴリ配下からツールを選択すると、右フレームでそのツールが利用できる。

f:id:hepokon365:20220130162628p:plain
All toolsを選択し、カテゴリを展開した画面

カテゴリを展開する以外にも、All toolsでは各ツールへのショートカットが表示される。左上の検索ボックスからも検索可能。

また、「Smart Detection」と呼ばれる機能で、クリップボードの文字列から推奨されるツールがサジェストされる。設定からオフにすることも可能。

f:id:hepokon365:20220130164611p:plain
JSON文字列をコピーした場合のSmart Detection

利用頻度の高いツールの場合、All toolsや各カテゴリ名を選択した際に右フレームに表示されるアイコンから、Windowsのスタートメニューに直接ピン留めできる。

f:id:hepokon365:20220130165856p:plain
アイコンにホバーするとピン留めアイコンが表示される

各ツールの概要

各カテゴリ展開時のスクリーンショットは公式ページのScreenshots参照。

Converters

Json <> Yaml

JSONYAMLの相互変換。インデントはスペース2つと4つから選択可能。

コメントが含まれるYAMLJSONに変換すると、コメントは削除される。JSON5のようなコメント可能な形式にはできない模様。

Number Base

数値の形式変換。出力形式はHexadecimal、Decimal、Octal、Binary。

「Format number」をONにすると、出力が Decimal の場合は3桁ごとにカンマで、それ以外は4桁ごとに半角スペースで区切られる。

「Input type」で入力形式を変更可能。

「Input type」で選択した形式も出力形式に残るので、「Input type」をDecimalにしておくと、「Format number」がONならカンマ区切りに、OFFならカンマを除いて数値のみにできるのが地味に便利。

Encoders / Decoders

HTML

HTMLのエンコード/デコード。

エンコードはHTMLで利用できない文字を文字実体参照に変換、デコードは文字参照を表示文字列に変換。

デコード時は数値文字参照もしっかり変換してくれるので、 &lt;&#60;&#x003c; と入れれば <<< になる。

URL

URLのエンコード/デコード。文字コードUTF-8で固定の模様。

Base 64

Base64エンコード/デコード。文字コードUTF-8とASCIIを選択可能。

エンコードは通常のBase64が出力され、URLセーフではない。また、デコードもBase64URLには対応していない模様。

JWT Decoder

JWTのデコーダー。これはデコードのみ。

Formatters

JSON

JSONのフォーマット。

「Indentation」で、インデントを半角スペース2つ、半角スペース4つ、タブ、またMinifiedによる最小化も可能。

入力にはコメントが含まれても問題ないが、「Json <> Yaml」と同様、コメントは出力されない。

Generators

Hash

文字列をハッシュ化。形式はMD5SHA1、SHA256、SHA512。出力するアルファベットの大文字小文字を指定可能。

「Load a file」でファイル読み込みが行えるが、テキスト形式ファイルを読み込み、そのファイルのテキストを入力としてハッシュ変換する機能だった。

バイナリファイルを読み込ませるとエラーが発生したため、バイナリファイルのハッシュ値計算はできない模様。

UUID

UUIDの生成。ハイフンの有無、出力するアルファベットの大文字小文字を指定可能。

バージョンはデフォルトが4(GUID)だが、バージョン1も出力可能。また、1回あたりで生成するUUIDの件数も指定可能。

Lorem Ipsum

ダミーテキストの生成。

「Type」でWords(単語)、Sentences(文)、Paragraphs(段落)を指定可能。「Length」の数だけ出力される。

生成される単語や文章はいくつかの候補からランダムで決定される模様。以下はLengthが1の場合の例。

Type 生成される文字
Words Vero
Sentences Sit justo diam dolores elitr autem sed sadipscing magna et et elitr justo.
Paragraphs Clita ut volutpat feugiat. Magna duo nostrud tempor nostrud labore at nulla vel. Zzril sit dolor sanctus duis.

たまたまParagraphsが短くなったが、中には1,000文字を超えるものもあった。

Text

Inspector & Case Converter

文字の大小や区切り変換と、監査機能。

「Convert」で、文字の大文字小文字や、キャメルケース/スネークケース/コンスタントケース/ケバブケースなどの変換が行える。大文字でハイフン区切りの、COBOL-CASEなんてのもあった。

また、右側に文字列の監査結果として、「Selection」にはカーソル位置の行数、「Statistics」には文字、単語、行、文、段落、バイトのそれぞれの数、「Word distribution」と「Character distribution」には単語と文字の出現回数が表示される。

Convert機能を試してみたところ、半角スペースやハイフン、アンダースコア区切りの文字列は適切に解釈されるが、キャメルケースやパスカルケースの文字列は、大文字が区切り文字として認識されず1単語と判断されるようで、変換がうまくいかなかった。

Regex Tester

正規表現テスター。正規表現オプションはOptionsから設定可能。

ECMA Script互換にもOptionsから設定できる。C#製のツールのため、デフォルトは.NETの正規表現エンジンが使われるのかな?

Text Diff

2つのテキストの差分表示。差分の表示形式はデフォルトでは2ペイン表示だが、インライン表示にもできる。

Markdown Preview

シンプルなMarkdownプレビュー。表示テーマをLightとDarkから選択可能。

Graphic

Color Blindness Simulator

色覚異常のある方に、画像がどう見えるかのシミュレーター。PNG, JPEG, BMPを読み込むか、クリップボードからペーストして使用する。

左上がオリジナル画像、右上の「Protanopia simulation」がP型色覚(赤の欠損)、左下の「Tritanopia simulation」がT型色覚(青の欠損)、右下の「Deuteranopia simulation」がD型色覚(緑の欠損)のシミュレーション画像となる。

Viewアイコンでそれぞれの画像を画像ビューワーで表示したり、保存アイコンで画像を保存できる。

PNG / JPEG Compressor

PNGおよびJPEG画像の圧縮。

「各ツールへの導線」に張り付けた、All toolsのPNG画像(Xbox Game Barでキャプチャ)を圧縮してみると、元画像166KBから100KB程度、40%ほど圧縮された。

f:id:hepokon365:20220130203603p:plain

その他

Settings

左下のSettingsから各種設定が可能。

Smart Detectionのオフはこちらから。自分はちょっと試してオフにしてしまった。

他、テーマやフォントの変更、行番号の非表示、半角スペースの表示などが行える。

半角スペースの表示を行うようにしたが、HTMLには反映されなかった。

GitHubへのリンクなども記載されている。また、ログファイルの表示もこちらから行える。

ファイル読み込み時の文字コード

いくつかのツールではファイルから文字列を読み込む機能があるが、文字コードUTF-8限定の模様。BOMの有無は関係なし。

日本語を含んだShift-JISのファイルはエラーで開けず、UTF-16のファイルは読み込めたがファイルの内容と異なる文字として読み込まれた。

まあ、今日日の開発者ならテキストファイルはUTF-8で保存するだろうし、注意がいるのはCSVファイルを読み込むときくらいかな。

マルチプラットフォーム化とWindows以外での代替アプリについて

ぱっと見はElectronっぽいので、なんでWindows限定なんだろうと思ったら、公式に「DevToys is designed to embrace Windows ecosystem.」とあり、結構がっつりWindowsネイティブなアプリの模様。

issue#203およびissue#156で、Windowsでしか動かない理由の記載がある。

ユニバーサル Windows プラットフォーム (UWP) アプリのため、Windows以外では動かないとのこと。MacLinuxには同じようなアプリがあるが、Windowsにはないというのが開発の動機なので、マルチプラットフォーム対応は難しそう。詳細はこちらのコメントを参照。

それぞれのissueのコメントで、MacLinux向けの代替ツールとしては、DevUtils, Boop, DevBox, Text Piecesの名前が挙がっている。

DevUtilsとDevBoxは有料、BoopとText Piecesは無料かな?

2022/2/1 追記

Mac版のクローンアプリが作成中の模様。UIを似せたフルスクラッチっぽい。

github.com

振り返り

これまでいくつかのWebサービスを利用していたような機能がまとめられるので非常に便利。まさにSwiss Army knife(十徳ナイフ)。

個人的にはGraphicのColor Blindness SimulatorやPNG / JPEG Compressorが、専用の画像編集ツールなどを使わずに試せるのがありがたかった。

また、よく使うツールはスタートにピン留めできるのもありがたい。このあたりはWindowsネイティブアプリだからできることなのかも。

JPAのCriteriaBuilderでRDBMSの関数を使う

DBでは数値として持った値を、左0埋めして画面に表示していたが、そこを文字列として部分一致検索させることになった。

検索はSpringのSpecificationで、動的なJPQLをCriteriaBuilderで組み立てており、RDBMSの関数を使えないか調べたのでメモ。

やりたいこと

DB(MySQL)では数値で保存した値を左ゼロ埋めし、接頭語をつけて表示している。

数値が1なら、表示は NUM00001 というイメージ。

これを文字列として、部分一致検索できるようにしたい。

対応

MySQLであればLPAD関数で0埋めし、それを接頭語と CONCAT すればいい。

CONCAT に対応する操作はCriteriaBuilder#concatで行える。

RDBMSの関数を、JPQLで実行できるか調べたところ、JPA Criteriaで左埋めできないかというそのまんまな質問が、Stack Overflowにあった。

stackoverflow.com

CriteriaBuilder#functionを使えば、RDBMSの関数の実行ができる。

第1引数が関数名、第2引数が関数の戻り値の型、それ以降は可変長引数で関数に渡す値を指定する。

注意点としては、可変長引数の型はExpressionのため、リテラルCriteriaBuilder#literalでExpressionに変換してやる必要がある。

// 左ゼロ埋めを実施
var leftPad = cb.function(
        "LPAD",
        String.class,
        root.get(ExampleEntity_.seq).as(String.class),
        cb.literal(5),
        cb.literal("0")
);

// 接頭語とCONCAT
var displayValue = cb.concat("NUM", leftPad);

// CONCATした文字列を、部分一致検索
cb.like(displayValue, "%" + inputValue + "%")

振り返り

SQL Serverを除いた主要なRDBMSではLPADが使えるようだが、可搬性は下がるので、JPQLの使い方としてはやや邪道か。

DBのテーブル側にカラムを追加して、文字列として保存しておくのが無難かなとは思う。

とはいえ、必要に応じて関数を実行できるようになっているのはありがたい。

JPAのCriteriaBuilderのconjunctionとdisjunctionについて

SpringのSpecificationなどで、動的なJPQLをCriteriaBuilderで、Predicateを組み立てるときに使う conjunctiondisjunction 、毎回どっちがどっちか忘れるのでメモ。

conjunction

AND条件の起点。SQLに解釈されると 1 = 1 になる。

(root, query, cb) -> {
    var predicate = cb.conjunction();
    predicate = cb.and(
            predicate,
            cb.equal(root.get(EntityClass_.id), entityId)
    );
}

disjunction

OR条件の起点。SQLに解釈されると 0 = 1 になる。

(root, query, cb) -> {
    var predicate = cb.disjunction();
    predicate = cb.or(
            predicate,
            cb.like(root.get(EntityClass_.name), "%" + entityName + "%")
    );
}

振り返り

入力された値に応じて検索条件を組み立てる場合、CriteriaBuilderのメソッドに渡すPredicateが null だと、実行時にNullPointerExceptionが発生するので、未入力なら conjunctiondisjunction を使うようにすると安全。

また、PredicateのListやStreamを用意して、Stream#reduceを使うと、 cb.andcb.or の呼び出しを減らせる。

// AND条件の場合
(root, query, cb) -> {
    var predicates = new ArrayList<Predicate>();
    predicates.add(...);
    return predicates.stream().reduce(cb.conjunction(), cb::and);
}

// OR条件の場合
(root, query, cb) -> {
    return Stream.of(
        ...
    )
    .reduce(cb.disjunction(), cb::or);
}