現象
spring-data-jpaでidを明示的に指定してsave
する場合、以下のログのようにselectしてからinsertになる。
Sample newEntity = new Sample(1L, "name");
repository.save(newEntity);
Hibernate: select sample0_.id as id1_0_0_, sample0_.name as name2_0_0_ from sample sample0_ where sample0_.id=?
Hibernate: insert into sample (name, id) values (?, ?)
idがすでに永続化済みかどうかをselectしてからinsertするのはjpaの挙動である。しかし、ある種のバッチ処理などでは、仕様としてそのidが存在しない事が明らかな場合がある。この場合はselectは単に無駄となるので防ぎたい。以下はこの事前selectを防ぐ方法について。
ソースコード
build.gradle
plugins {
id 'org.springframework.boot' version '2.3.3.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
内部的に発行されるSQLを確認するプロパティを追加する。
src/main/resources/application.properties
spring.jpa.show-sql=true
エンティティクラス。Persistable
は後述。
import javax.persistence.Entity;
import javax.persistence.Id;
import org.springframework.data.domain.Persistable;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Sample implements Persistable<Long> {
@Id
Long id;
String name;
@Override
public Long getId() {
return id;
}
@Override
public boolean isNew() {
return true;
}
}
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SampleRepository extends JpaRepository<Sample, Long> {
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.Transactional;
@EnableJpaRepositories
@SpringBootApplication
public class JpaSample implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(JpaSample.class, args);
}
@Autowired
SampleRepository repository;
@Transactional
@Override
public void run(String... args) throws Exception {
Sample newEntity = new Sample(1L, "name");
repository.save(newEntity);
}
}
実行ログ
上記コードを実行すると以下のようなSQLログに変化する。
Hibernate: insert into sample (name, id) values (?, ?)
解説
spring-data-jpaのsave
は基本的には以下のSimpleJpaRepository#save
を通過し、その実装は以下となっている。
SimpleJpaRepository
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
デフォルトではisNew
はfalse
なのでem.merge
となり、結果としてselect-insertになる。この挙動はエンティティクラスにPersistable
を実装することで変えられる。今回は事前selectを防ぐ、つまりJPAに新規エンティティとして認識させたいので、isNew
をtrue
を返すようにしている。