はじめに (対象読者・この記事でわかること)
本記事は、Javaでデータベース操作を行うエンジニア(初心者〜中級者)を対象としています。特に、文字列中にシングルクォーテーション (') が含まれるケースで、SQLインジェクションを防ぎつつ正しくクエリを組み立てる方法を知りたい方に最適です。この記事を読むことで、以下ができるようになります。
PreparedStatementを用いた安全なクエリ作成手順- 文字列リテラル中のシングルクォートをエスケープする具体的なコード例
- データベースごとのエスケープルールの違いと注意点
このテーマは、実務で「名前にシングルクォートが入るデータがINSERTできない」などの障壁に直面した経験がきっかけで取り上げました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本的な文法と例外処理
- JDBC の基礎(
Connection、Statement、ResultSetなど) - 基本的な SQL 文(
SELECT、INSERT、UPDATE)
シングルクォーテーションが問題になる背景
SQL では文字列リテラルをシングルクォートで囲みます。したがって、ユーザー入力や変数にシングルクォートが含まれると、クエリの構文が壊れ、エラーになるだけでなく、悪意のある入力によって SQL インジェクションが発生するリスクがあります。たとえば、次のような SQL 文は構文エラーになります。
SqlINSERT INTO users (name) VALUES ('O'Reilly');
この例では、' が文字列の終了と解釈され、Reilly が余分なトークンとして扱われます。Java からこのような文字列を安全に扱うための一般的な手段は 2 つ です。
- エスケープ:シングルクォートを
''(2 つのシングルクォート)に置き換える - プレースホルダー(パラメータバインディング):
PreparedStatementの?を使い、JDBC が自動でエスケープ処理を行う
エスケープは簡易的ですが、コードが散在しやすく保守性が低くなります。一方、PreparedStatement は安全性と可読性の両方を確保できるため、実務では主にこちらが推奨されます。
Java でシングルクォートを安全に扱う実装手順
以下では、実際に MySQL と PostgreSQL の 2 つのデータベースを対象に、シングルクォートを含む文字列を INSERT / UPDATE する具体的な手順を示します。
ステップ 1: 環境構築と JDBC ドライバの設定
Xml<!-- pom.xml の例 (Maven) --> <dependencies> <!-- MySQL Connector/J --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <!-- PostgreSQL JDBC Driver --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.1</version> </dependency> </dependencies>
pom.xml(Maven)またはbuild.gradle(Gradle)に上記依存関係を追加し、mvn packageでビルドします。- データベース接続 URL、ユーザー名、パスワードは環境変数や
application.propertiesに外部化しておくと安全です。
ステップ 2: PreparedStatement を用いた安全な INSERT
Javaimport java.sql.*; public class UserDao { private final DataSource dataSource; // HikariCP などのコネクションプール public UserDao(DataSource ds) { this.dataSource = ds; } /** * 名前にシングルクォートが入っていても安全に INSERT できる。 */ public void insertUser(String name, int age) throws SQLException { String sql = "INSERT INTO users (name, age) VALUES (?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 第1パラメータに文字列を設定。JDBC が内部でエスケープしてくれる。 ps.setString(1, name); ps.setInt(2, age); int affected = ps.executeUpdate(); System.out.println(affected + " row(s) inserted."); } } }
?プレースホルダーに対してsetStringを呼び出すだけで、シングルクォートは自動的にエスケープされます。- 例外は
SQLExceptionで捕捉し、エラーログに詳細を残すことが推奨されます。
ステップ 3: エスケープを手動で行う場合(レガシーコード向け)
古いコードベースで Statement を使い続ける必要がある場合、手動でエスケープするヘルパーメソッドを用意します。
Javapublic class SqlUtil { /** * シングルクォートを二重シングルクォートに置き換えるだけの簡易エスケープ。 * JDBC のプレースホルダーを使用できない場合の最終手段として利用。 */ public static String escapeSingleQuote(String input) { if (input == null) return null; return input.replace("'", "''"); } }
使用例:
JavaString unsafeName = "O'Reilly"; String safeName = SqlUtil.escapeSingleQuote(unsafeName); String sql = "INSERT INTO users (name) VALUES ('" + safeName + "')"; Statement stmt = conn.createStatement(); stmt.executeUpdate(sql);
注意点
- 手動エスケープは DBMS に依存しない保証がないため、必ずテストしてください。
- MySQL の NO_BACKSLASH_ESCAPES モードや PostgreSQL の standard_conforming_strings 設定によっては、バックスラッシュエスケープが無効になることがあります。
ステップ 4: データベース別のエスケープルールと設定確認
| DBMS | デフォルトエスケープ方式 | 重要な設定項目 |
|---|---|---|
| MySQL | ''(二重シングルクォート) |
sql_mode に NO_BACKSLASH_ESCAPES が含まれるか |
| PostgreSQL | ''(二重シングルクォート) |
standard_conforming_strings が on であること |
| Oracle | ''(二重シングルクォート) |
N/A |
| SQL Server | ''(二重シングルクォート) |
N/A |
- MySQL:
NO_BACKSLASH_ESCAPESが有効になると、バックスラッシュ (\) がエスケープ文字として機能しなくなる点に注意。 - PostgreSQL:
standard_conforming_stringsがoffの場合、バックスラッシュもエスケープ対象になるが、現行バージョンではデフォルトonが推奨。
設定確認クエリ例(MySQL):
SqlSELECT @@sql_mode;
PostgreSQL の確認:
SqlSHOW standard_conforming_strings;
ハマった点やエラー解決
1. SQLSyntaxErrorException: You have an error in your SQL syntax... が発生
- 原因: 手動エスケープ時に
''に置き換え忘れ、文字列が途中で切れた。 - 解決策:
SqlUtil.escapeSingleQuoteを必ず呼び、生成した SQL をログに出力して目視確認。
2. java.sql.SQLIntegrityConstraintViolationException: Duplicate entry
- 原因: エスケープ前の文字列が同一とみなされ、ユニーク制約違反が発生。
- 解決策: データベース側で正規化(大文字小文字統一)やトリム処理を行うか、Java 側で
String.trim()とtoUpperCase()等の正規化を実施。
3. java.sql.SQLFeatureNotSupportedException: PreparedStatement#setString がサポート外
- 原因: 古い JDBC ドライバ(例: SQLite の 3.7 系)で
setStringが不完全に実装されている。 - 解決策: ドライバを最新バージョンにアップデートし、
setObjectに切り替える。
ベストプラクティスまとめ
- 必ず
PreparedStatementを使用 – 手動エスケープは最小限に抑える。 - コネクションプールを導入 –
HikariCPなどでパフォーマンスと安定性を確保。 - エラーログは詳細に – 例外スタックトレースと共に失敗した SQL を出力し、再現性を担保。
- テストケースにシングルクォート入り文字列を必ず含める – ユニットテストや統合テストでカバレッジを確保。
- データベース設定を定期的に確認 –
sql_modeやstandard_conforming_stringsの変更は動作に直結する。
まとめ
本記事では、Java アプリケーションからデータベースへシングルクォートを含む文字列を安全に渡す方法を解説しました。
PreparedStatementによるパラメータバインディング が最も安全かつシンプル。- 手動エスケープ はレガシーコード向けの最終手段で、
replace("'", "''")が基本。 - データベースごとの設定(MySQL の
sql_mode、PostgreSQL のstandard_conforming_strings) を確認し、環境に合わせた対策が必要。
この記事を通じて、シングルクォートに起因する SQL エラーやインジェクションリスクを回避できる知識が身についたはずです。次回は、バッチ処理や大量データ投入時のパラメータバインディング最適化について掘り下げる予定です。
参考資料
- JDBC API Documentation
- MySQL Reference Manual – String Literals
- PostgreSQL Documentation – String Constants
- 「Effective Java」 第3版 – アイテム 58: 文字列の処理は
StringBuilderで行う
