PostgreSQL の配列型(text[])を Spring Boot + JPA でマッピングする方法
はじめに
PostgreSQLには text[] や integer[] のような配列型があります。
「映画のタグを配列で保存したい」という要件で使おうとしたところ、Spring Boot + JPA との組み合わせでかなり詰まりました。
この記事は、その詰まりポイントと解決方法をまとめたものです。
やりたかったこと
LLMで映画の雰囲気タグを自動生成して、film テーブルの taste_tags カラムに保存したい。
ALTER TABLE film ADD COLUMN taste_tags text[];
格納されるデータのイメージ:
UPDATE film SET taste_tags = ARRAY['アクション', 'ハラハラ', '家族向け'] WHERE film_id = 1;
これをJavaのエンティティで受け取りたい。
最初のアプローチ(失敗)
まず素直に String[] でマッピングしてみました。
@Entity
@Table(name = "film")
public class Film {
// ... 省略 ...
@Column(name = "taste_tags")
private String[] tasteTags; // これでいけると思った
}
結果: 起動時にエラー
org.hibernate.MappingException: Could not determine type for: [Ljava.lang.String;
HibernateはJavaの String[] をPostgreSQLの text[] に自動マッピングしません。
解決策1:columnDefinition を明示する
@Column(name = "taste_tags", columnDefinition = "text[]")
private String[] tasteTags;
これで起動エラーは消えますが、実行時に別のエラーが出ることがあります。
ERROR: column "taste_tags" is of type text[] but expression is of type character varying
HibernateがSQLを生成するとき、text[] として正しく扱ってくれないことがあります。
解決策2:カスタム UserType を実装する(Hibernate 6系)
Hibernate 6以降では UserType インターフェースを実装してカスタム型を作れます。
public class StringArrayType implements UserType<String[]> {
@Override
public int getSqlType() {
return Types.ARRAY;
}
@Override
public Class<String[]> returnedClass() {
return String[].class;
}
@Override
public String[] nullSafeGet(ResultSet rs, int position,
SharedSessionContractImplementor session, Object owner)
throws SQLException {
Array array = rs.getArray(position);
if (array == null) return null;
return (String[]) array.getArray();
}
@Override
public void nullSafeSet(PreparedStatement st, String[] value, int index,
SharedSessionContractImplementor session)
throws SQLException {
if (value == null) {
st.setNull(index, Types.ARRAY);
} else {
Array array = st.getConnection().createArrayOf("text", value);
st.setArray(index, array);
}
}
// equals, hashCode, deepCopy, isMutable, disassemble, assemble も実装が必要
@Override
public boolean equals(String[] x, String[] y) {
return Arrays.equals(x, y);
}
@Override
public int hashCode(String[] x) {
return Arrays.hashCode(x);
}
@Override
public String[] deepCopy(String[] value) {
return value == null ? null : Arrays.copyOf(value, value.length);
}
@Override
public boolean isMutable() { return true; }
@Override
public Serializable disassemble(String[] value) {
return deepCopy(value);
}
@Override
public String[] assemble(Serializable cached, Object owner) {
return deepCopy((String[]) cached);
}
}
エンティティで使用:
@Column(name = "taste_tags", columnDefinition = "text[]")
@Type(StringArrayType.class)
private String[] tasteTags;
解決策3:Hibernate Types ライブラリを使う(最もシンプル)
vladmihalcea/hibernate-types というライブラリを使うと、カスタム型の実装をスキップできます。
<!-- pom.xml -->
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-63</artifactId>
<version>3.7.0</version>
</dependency>
import io.hypersistence.utils.hibernate.type.array.StringArrayType;
@Entity
@TypeDef(name = "string-array", typeClass = StringArrayType.class)
public class Film {
@Type(type = "string-array")
@Column(name = "taste_tags", columnDefinition = "text[]")
private String[] tasteTags;
}
Hibernate 6系(Spring Boot 3.x)では @TypeDef が非推奨になっているため、下記の書き方が現在の推奨です:
@Column(name = "taste_tags", columnDefinition = "text[]")
@Type(io.hypersistence.utils.hibernate.type.array.StringArrayType.class)
private String[] tasteTags;
MyBatisでの代替手法
今回のプロジェクトではバッチ処理側(管理画面)でJPAを使いますが、
顧客向けAPIはMyBatisで実装しているため、こちらでは String[] の扱いが簡単です。
// MyBatisのResultMapでの受け取り
public class FilmDetail {
private String[] tasteTags;
// JDBCのArrayをStringの配列として受け取れる
}
<!-- Mapper XML -->
<select id="findById" resultType="FilmDetail">
SELECT taste_tags FROM film WHERE film_id = #{filmId}
</select>
MyBatisは Array → String[] の変換を自動でやってくれます。この点はJPAより楽でした。
まとめ
| アプローチ | 難易度 | 備考 |
|---|---|---|
String[] そのまま | ✗ | マッピングエラー |
columnDefinition = "text[]" のみ | △ | 実行時エラーの可能性 |
カスタム UserType 実装 | ⚠️ 難 | 自前で型変換を全部実装 |
| hypersistence-utils | ✅ 簡単 | ライブラリ追加で解決 |
| MyBatis | ✅ 簡単 | 自動変換あり |
PostgreSQLの配列型を使いたい場合は hypersistence-utils の利用が現時点での最善策です。
MyBatisを使っているなら追加ライブラリ不要でシンプルに動きます。
このシリーズの記事マップ
→ dvdrental 管理アプリと対になるエンドユーザー向けDVDレンタルアプリを作っている話 — Vue 3 + Spring Boot の全体構成と記事マップ