[Spring Data JPA] saveAndFlush 後 find しても DB の値が取れない

2021-08-29JavaJPA,SpringBoot

Spring Data JPA の saveAndFlush() で DB 登録後、find メソッドを実行しても DB に登録された値が取得できなかった。例えば、DEFAULT CURRENT_TIMESTAMP で自動登録された日時など。その解決方法と、原因を調べたのでメモ。

発生した問題

問題のコード

Entity

package com.example.demo.infrastructure.entity;

import java.math.BigDecimal;
import java.time.ZonedDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table
public class Item {
    @Id
    @GeneratedValue
    private int id;

    @Column(nullable = false)
    private String name;

    @Column(nullable= false, precision=7, scale=2)
    private BigDecimal price;

    @Column(nullable = false, updatable = false, insertable = false, 
    columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    private ZonedDateTime created;

    @Column(nullable = false, updatable = false, insertable = false, 
    columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
    private ZonedDateTime updated;
}

Repository

package com.example.demo.infrastructure.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.infrastructure.entity.Item;

public interface ItemRepository extends JpaRepository<Item, Integer> {}

Service

package com.example.demo.domain.service;

import java.math.BigDecimal;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.infrastructure.entity.Item;
import com.example.demo.infrastructure.repository.ItemRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository repository;

    public void saveAndFind() throws Exception {
        Item item = new Item();
        item.setName("Iten001");
        BigDecimal price = new BigDecimal("100");
        item.setPrice(price);

        Item saved = saveAndFlush(item);
        System.out.println("saved: " + item);

        Item foundItem = findById(saved.getId());
        // DBに登録された値が出力されることを期待
        System.out.println("found: " + foundItem);
    }

   public Item saveAndFlush(Item item) {
        return repository.saveAndFlush(item);
    }

    public Item findById(int id) throws Exception {
        return repository.findById(id).orElseThrow(() -> new Exception());
    }
}

出力

saved: Item(id=1, name=Iten001, price=100, created=null, updated=null)
found: Item(id=1, name=Iten001, price=100, created=null, updated=null)

DB の値

idnamepricecreatedupdated
1Iten001100.002021-08-29 18:58:052021-08-29 18:58:05

DEFAULT CURRENT_TIMESTAMP などを使って自動登録しているので、 DB には日時が登録される。なのに出力は null になっている。

また、 price カラムは DECIMAL 型で、小数点の桁数を指定しているので、元の値に小数点がなくても、小数点付きで登録される。なのに出力は、小数点が付いていない値になっている。

Spring Data JPA の find() 系メソッドを使っても、 DB に登録された値が取れない。
flush しているから、find() 実行時には DB にコミットされているはず。
( Spring Data JPA で DB 登録後、 MyBatis で select すると、 DB に登録した値が取得できる。)

解決策

EntityManager#refresh(Object) で最新化する。

EntityManager の持つ refresh メソッドは、管理状態の Entity を、 DB と同期し、最新化する。
※ Spring Data JPA には、 refresh メソッドを呼び出す仕組みはない。

refresh メソッド

Service

package com.example.demo.domain.service;

import java.math.BigDecimal;

import javax.persistence.EntityManager;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.infrastructure.entity.Item;
import com.example.demo.infrastructure.repository.ItemRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository repository;
    // EntityManagerをDI
    private final EntityManager entityManager;

    // トランザクションを張る
    @Transactional
    public void saveAndFind() throws Exception {
        Item item = new Item();
        item.setName("Iten001");
        BigDecimal price = new BigDecimal("100");
        item.setPrice(price);

        Item saved = saveAndFlush(item);
        System.out.println("saved: " + item);
        // refreshする
        entityManager.refresh(saved);
        System.out.println("refreshed: " + saved);
    }

    public Item saveAndFlush(Item item) {
        return repository.saveAndFlush(item);
    }
}

出力

saved: Item(id=4, name=Iten001, price=100, created=null, updated=null)
refreshed: Item(id=4, name=Iten001, price=100.00, created=2021-08-30T06:27:49+09:00[Asia/Tokyo], updated=2021-08-30T06:27:49+09:00[Asia/Tokyo])

refresh メソッドを使うには、トランザクションを張る必要がある。
トランザクション張っていないと以下のエラーが発生する。

No EntityManager with actual transaction available for current thread - cannot reliably process 'refresh' call

flush ( DB にコミット)していないと、以下のエラーが発生する。

No row with the given identifier exists: [this instance does not yet exist as a row in the database#3]

問題が起こった理由

EntityManager による Entity のライフサイクル管理が関係している。

Spring Data JPA の saveAndFlush メソッドを実行すると、 selectinsert or update が実施される。
EntityManager の動きとしては、 insert なら persistupdate なら merge メソッドが呼び出される。この時に、対象エンティティ(上記コードだと、 item )は EntityManager の管理下に置かれる。「管理下に置かれる」と、このエンティティはキャッシュされるようだ。(管理下に置かれる = キャッシュされると考えてよさそう。) 

どうやら、 saveAndFlush メソッドの戻り値は、キャッシュされたエンティティのようだ。

EntityManager の flush メソッドは、 DB 側で行われた変更は、 Entity には同期しないとの記述がある。

EntityManagerのflushメソッドを呼び出すと、persist、merge、removeメソッドによって、蓄積されたEntityへの操作が、リレーショナルデータベースに反映される。
このメソッドを呼び出すことで、Entityに対して行った変更内容が、リレーショナルデータベースのレコードに同期される。
ただし、リレーショナルデータベース側のレコードに対してのみ行われた変更は、Entityには同期されない。

項番(5)

また、 Spring Data JPA の find 系メソッドも、キャッシュからエンティティを取得している。

永続層から取得されたEntityは EntityManager によって管理(キャッシュ)されるため、2回目以降のアクセスでは永続層へのアクセスは発生せず、キャッシュされているEntityが返却される。

Optional<T> findById(ID id)

saveAndFlush メソッド実行後は、対象エンティティは EntityManager の管理下に置かれている(キャッシュされている)。そのため、後続で JPA の find 系メソッドを実行しても、キャッシュを見ているため、 DB 側で変更した内容は取得できない。

EntityManager の管理 (キャッシュ)の期間は、トランザクションの生存期間だと考えられる。

・参考サイト

6.3. データベースアクセス(JPA編)

JavaEE使い方メモ(JPA その1 – 基本

Posted by Agopeanuts