はじめに
特定のDBの機能に依存した実装をしてしまうと、将来に大きな負担となる場合があります。
AUTO_INCREMENTは便利な機能ですが、機能を有していないDBも存在します。
同等の機能をアプリケーション側で実装し、
各種DBに対応できるようにするとともに、欠番が発生しないようにしてみます。
実装例
ID(シーケンス番号)を管理するためのテーブルを定義します。
カラム:NAMEには、この機能で発行するIDを使うテーブル名、
カラム:SEQUENCEには、テーブルごとのID(数値)が入ります。
桁数などは適当に調整してください。
sequence.sql
# MySQL CREATE TABLE SEQUENCE ( NAME VARCHAR(255) BINARY NOT NULL, SEQUENCE BIGINT DEFAULT 0 NOT NULL, PRIMARY KEY PK_SEQUENCE (NAME) ); # Oracle CREATE TABLE SEQUENCE ( NAME VARCHAR2(256) NOT NULL, SEQUENCE NUMBER(16) DEFAULT 0 NOT NULL, CONSTRAINT PK_SEQUENCE PRIMARY KEY (NAME) ); # PostgreSQL / SQL Server / DB2 CREATE TABLE SEQUENCE ( NAME VARCHAR(256) NOT NULL, SEQUENCE BIGINT DEFAULT 0 NOT NULL, CONSTRAINT PK_SEQUENCE PRIMARY KEY (NAME) );
次に、IDを発行するクラスを定義します。
SequenceTest.java
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
/**
 *
 * @author tool-taro.com
 */
public class SequenceTest {
	public static long create(Connection connection, String name) throws SQLException {
		long id = 0;
		PreparedStatement preparedStatement;
		ResultSet resultSet;
		int count;
		int update;
		Savepoint savepoint;
		connection.setAutoCommit(false);
		//初回はレコードがないのでINSERTする
		savepoint = connection.setSavepoint();
		count = 0;
		update = 0;
		preparedStatement = connection.prepareStatement("INSERT INTO SEQUENCE (NAME, SEQUENCE) VALUES (?, ?)");
		preparedStatement.setString(++count, name);
		preparedStatement.setLong(++count, 0);
		try {
			update = preparedStatement.executeUpdate();
		}
		catch (SQLException e) {
			connection.rollback(savepoint);
		}
		preparedStatement.close();
		if (update == 0) {
			//INSERTに失敗した場合はレコードがあるのでUPDATEする
			count = 0;
			preparedStatement = connection.prepareStatement("UPDATE SEQUENCE SET SEQUENCE = SEQUENCE + 1 WHERE NAME = ?");
			preparedStatement.setString(++count, name);
			update = preparedStatement.executeUpdate();
			preparedStatement.close();
			//INSERTもUPDATEも失敗したということは最初にINSERTした側のTransactionがrollbackされてレコードが存在しないということなので最初からやり直す
			if (update == 0) {
				return SequenceTest.create(connection, name);
			}
			count = 0;
			preparedStatement = connection.prepareStatement("SELECT SEQUENCE FROM SEQUENCE WHERE NAME = ?");
			preparedStatement.setString(++count, name);
			resultSet = preparedStatement.executeQuery();
			if (resultSet.next()) {
				id = resultSet.getLong("SEQUENCE");
			}
			resultSet.close();
			preparedStatement.close();
		}
		return id;
	}
}
ID発行機構の準備が終わりました。
サンプルでは、動作確認しやすいようにjspで実装しています。
sequence_test.jsp
<%-- 
    Author     : tool-taro.com
--%>
<%@page import="SequenceTest"%>
<%@page import="javax.sql.DataSource"%>
<%@page import="java.sql.Connection"%>
<%@page import="javax.naming.InitialContext"%>
<%@page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<%
        //コネクションを取得するjndi
        String jndi = "java:comp/env/jdbc/MySQL";
        //String jndi = "java:comp/env/jdbc/Oracle"; //←Oracleの場合
        InitialContext context = null;
        Connection connection = null;
        //コネクション取得処理
        long id_1 = -1;
        long id_2 = -1;
        try {
                context = new InitialContext();
                DataSource dataSource = (DataSource) context.lookup(jndi);
                connection = dataSource.getConnection();
                //トランザクション分離レベルはDBによって異なるので設定を統一
                connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                connection.setAutoCommit(false);
                //IDを取得してわざとrollbackする
                SequenceTest.create(connection, "TEST_TABLE_NAME");
                connection.rollback();
                //IDを取得してcommitする(初回アクセスなら"0"を得られるはず)
                id_1 = SequenceTest.create(connection, "TEST_TABLE_NAME");
                //このタイミングで対象のテーブルにデータを挿入するなどの処理を行う
                connection.commit();
                //IDを取得してわざとrollbackする
                SequenceTest.create(connection, "TEST_TABLE_NAME");
                connection.rollback();
                //IDを取得してcommitする(初回アクセスなら"1"を得られるはず)
                id_2 = SequenceTest.create(connection, "TEST_TABLE_NAME");
                //このタイミングで対象のテーブルにデータを挿入するなどの処理を行う
                connection.commit();
        }
        finally {
                if (context != null) {
                        try {
                                context.close();
                        }
                        catch (Exception e) {
                        }
                }
                if (connection != null) {
                        try {
                                connection.close();
                        }
                        catch (Exception e) {
                        }
                }
        }
%>
<!DOCTYPE html>
<html>
    <head>
        <title>tool-taro.com</title>
    </head>
    <body>
        取得したID_1="<%= id_1%>"<br>
        取得したID_2="<%= id_2%>"<br>
    </body>
</html>
動作確認
sequence_test.jspの実行結果を見てみましょう。
取得したID_1="0" 取得したID_2="1"
想定通りの結果を得られました。
2回目のアクセスでは次のような結果となります。
取得したID_1="2" 取得したID_2="3"
環境
- 開発
- Windows 10 Pro
 - JDK 1.8.0_74
 - NetBeans IDE 8.1
 
 - 動作検証
- CentOS Linux release 7.2
 - JDK 1.8.0_74
 
 
Webツールも公開しています。
Web便利ツール@ツールタロウ