kagamihogeの日記

kagamihogeの日記です。

spring-bootでOracleのIDENTITY列値の取得

たとえばoracleでは下記で連番を割り振れる。

CREATE TABLE sample_gen( 
    id number GENERATED ALWAYS AS IDENTITY
    , value VARCHAR2(100) NOT NULL
    , CONSTRAINT sample_gen_pk PRIMARY KEY (id)
);
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;

@Data
@Entity(name = "sample_gen")
public class SampleGenEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  int id;

  @Column
  String value;

}

これをJPA経由でinsertするとその連番の値も取得できる。sequence値を別途nextvalで取得してないのに連番が取れてしまう。旧来のoracleユーザからすると不思議だったので少し調べた。

  @PersistenceContext
  EntityManager em;

  @Transactional
  public void execute() {
    SampleGenEntity e = new SampleGenEntity();
    e.setValue("value");
    em.persist(e);

    System.out.println(e); 
Hibernate: insert into sample_gen (value,id) values (?,default)
SampleGenEntity(id=23, value=value)

調べた事とか

環境

  • Oracle Database 23ai Free Release 23.0.0.0.0
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.2'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.oracle.database.jdbc:ojdbc11'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

prepareStatementで自動生成列名を指定

Connection#prepareStatement(String sql, String[] columnNames))という自動生成列名を指定するメソッドが存在する。これを使用するとinsertと同時に連番の値も取得できる。おそらくは内部的にこのようなロジックになっているのだと思われる。

    try (Connection c = ds.getConnection();
        PreparedStatement ps = c
            .prepareStatement(
                "insert into sample_gen(id, value) values (default, ?)",
                new String[]{"id"});) {
      ps.setString(1, "asdfasdfa111");

      ResultSet resultSet = ps.executeQuery();
      resultSet.next();
      System.out.println(resultSet.getInt(1));
    }

JdbcTemplateも同様

JdbcTemplateの場合もほぼ同様。

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

    JdbcTemplate jdbc = new JdbcTemplate(ds);
    KeyHolder keyHolder = new GeneratedKeyHolder();

    jdbc.update(connection -> {
      PreparedStatement ps = connection
          .prepareStatement(
            "insert into sample_gen(id, value) values (default, ?)",
            new String[]{"id"});
      ps.setString(1, "asdfasdsdff");
      return ps;
    }, keyHolder);

    System.out.println(keyHolder.getKey());

  }

参考;https://www.baeldung.com/spring-jdbc-autogenerated-keys

はまった点

The generated key type is not supported. Unable to cast [oracle.sql.ROWID] to [java.lang.Number].

Connection#prepareStatementにはautoGeneratedKeysというint型を渡すメソッドもあり、たとえば.prepareStatement("insert ...", Statement.RETURN_GENERATED_KEYS);のように使う。ただ、このケースでは使えないようだ。下記の通りROWIDを返そうとする。おそらくは、RDBMSだとかJDBCだとかバージョンだとかによって異なってくるのだろう(これ以上は調べてない)。

org.springframework.dao.DataRetrievalFailureException: The generated key type is not supported. Unable to cast [oracle.sql.ROWID] to [java.lang.Number].
    at org.springframework.jdbc.support.GeneratedKeyHolder.getKeyAs(GeneratedKeyHolder.java:86) ~[spring-jdbc-6.2.2.jar:6.2.2]
    at org.springframework.jdbc.support.GeneratedKeyHolder.getKey(GeneratedKeyHolder.java:65) ~[spring-jdbc-6.2.2.jar:6.2.2]