JavaのSpring BootでIPアドレスをPostgreSQLにINSERTする際の落とし穴と対策

こんにちは、エキサイト株式会社の岩藤です。

JavaのSpring Bootを使用して、PostgreSQLデータベースのinet型cidr型IPアドレスを"124.24.32.2"の形式でINSERTすると、データ型の不一致によるエラーが発生することがあります。

本記事では、このエラーを回避するための解決策について解説します。

動作確認環境

Java

openjdk 21.0.2 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)

Spring Boot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.1)

Gradle

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.2 (Amazon.com Inc. 21.0.2+13-LTS)
OS:           Mac OS X 14.7 aarch64

PostgreSQL

PostgreSQL 15.4 (Debian 15.4-2.pgdg120+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit

PostgreSQLIPアドレスを扱う型

PostgreSQLIPアドレスを扱う型は下記の3種類があります。

データ型 説明
cidr型 ネットワークアドレスを表現するデータ型。IPアドレスサブネットマスクを含む。 192.168.100.0/24
inet型 ホストやネットワークのIPアドレスを格納するデータ型。IPv4IPv6に対応している。 192.168.100.15 または 192.168.100.15/24
macaddr型 MACアドレス(ネットワーク機器の物理アドレス)を格納するデータ型。 08:00:2b:01:02:03

今回は、ネットワークのIPアドレスを格納したく、サブネットマスクなしで242.168.100.15のように登録したいので、inet型を採用しました。

作成したテーブル

CREATE文

CREATE TABLE ip_address (
    id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    ip_address INET NOT NULL,
    created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
    PRIMARY KEY (id)
);

COMMENT ON TABLE ip_address IS 'IPアドレス管理';
COMMENT ON COLUMN ip_address.id IS 'ID';
COMMENT ON COLUMN ip_address.ip_address IS 'IPアドレス';
COMMENT ON COLUMN ip_address.created_at IS '作成日時';

CREATE INDEX ip_address_index ON ip_address (ip_address);

手動INSERTした場合

作成したテーブルに手動でINSERTしてみます。

INSERT INTO ip_address (id, ip_address) VALUES (1, '18.177.66.153');
Query 1 OK: INSERT 0 1, 1 row affected

手動でINSERTした場合は、エラーにならずにINSERTされました。

なお、CIDR型の場合は、同様のINSERT文を実行した際に自動で /32 のサブネットマスクが付与されます。

18.177.66.153/32

MyBatis Generatorで自動生成したクラスでINSERTした場合

id = 1L, ip = "18.177.66.153"

@Override
public void insertIpAddress(final Long id, final String ip) {
    IpAddress ipAddress = new IpAddress();
    ipAddress.setIpAddress(ip);
    ipAddressMapper.insertSelective(ipAddress);
}

実行結果

Error updating database.  Cause: org.postgresql.util.PSQLException: ERROR: column "ip_address" is of type inet but expression is of type character varying
  ヒント: You will need to rewrite or cast the expression.

挿入しようとしている値が文字列型(character varying)であるため、型の不一致によるエラーが発生しました。

Freemarkerテンプレート(ftlファイル)を作成してINSERT文を作成した場合

ip_address_insert.ftl id = 1L, ip = "18.177.66.153"

/* IPアドレスを登録 */

INSERT INTO ip_address (id, ip_address)
VALUES (<@p name="id" />, <@p name="ipAddress" />);

実行結果

Error querying database.  Cause: org.postgresql.util.PSQLException: ERROR: column "ip_address" is of type inet but expression is of type character varying
  ヒント: You will need to rewrite or cast the expression.

こちらでも同様のエラーが発生しました。

CIDR型の場合も同様のエラーが確認できました。

INET型CIDR型の両方において、プログラムを通じて実行した際に、文字列型IPアドレスを指定すると型の不一致によりINSERTが失敗する事がわかりました。

データ型不一致のエラー解決策 Freemarkerテンプレート(ftlファイル)を利用した場合

INSERT文を下記の内容に変更します。

変更箇所は、VALUESのipAddress後ろに::inetが指定されている部分です。 これは型キャスト演算子と言います。

/* IPアドレスを登録 */

INSERT INTO ip_address (id, ip_address)
VALUES (<@p name="id" />, <@p name="ipAddress" />::inet);

実行結果 inet型にキャストした事で型が一致してINSERTが成功しました!

CIDR型の場合は::cidrという指定になります。

データ型不一致のエラー解決策 Mybatisの場合(断念)

MyBatisでは以下のように、あらかじめ定義された型エイリアスを使用することが一般的です。

MyBatisのTypeAliasesについて

しかし、PostgreSQLのinet型に直接マッピングする場合、MyBatisはデフォルトでこのデータ型をサポートしていないため、独自に型エイリアスを定義する必要があります。

また、PostgreSQLのinet型に関連するJDBCドライバも、inet型をその他のデータ型として扱っています。

データ型 対応するJavaクラス SQL
inet, cidr, macaddr org.postgresql.util.PGobject java.sql.Types.OTHER

下記参照 JDBCドライバのドキュメント

つまり、MyBatisの型マッピングと、JDBCドライバ固有の処理を組み合わせて対応する必要があり、少し複雑な対応が必要そうで断念しました。

以上となります、同様のエラーでお困りの方は参考にいただけますと幸いです。