[Spring Data JPA] saveAndFlush 後 find しても DB の値が取れない
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 の値
id | name | price | created | updated |
1 | Iten001 | 100.00 | 2021-08-29 18:58:05 | 2021-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
メソッドを実行すると、 select
後 insert
or update
が実施される。
EntityManager の動きとしては、 insert
なら persist
、 update
なら merge
メソッドが呼び出される。この時に、対象エンティティ(上記コードだと、 item
)は EntityManager の管理下に置かれる。「管理下に置かれる」と、このエンティティはキャッシュされるようだ。(管理下に置かれる = キャッシュされると考えてよさそう。)
どうやら、 saveAndFlush
メソッドの戻り値は、キャッシュされたエンティティのようだ。
EntityManager の flush
メソッドは、 DB 側で行われた変更は、 Entity には同期しないとの記述がある。
EntityManagerのflushメソッドを呼び出すと、persist、merge、removeメソッドによって、蓄積されたEntityへの操作が、リレーショナルデータベースに反映される。
項番(5)
このメソッドを呼び出すことで、Entityに対して行った変更内容が、リレーショナルデータベースのレコードに同期される。
ただし、リレーショナルデータベース側のレコードに対してのみ行われた変更は、Entityには同期されない。
また、 Spring Data JPA の find
系メソッドも、キャッシュからエンティティを取得している。
永続層から取得されたEntityは EntityManager によって管理(キャッシュ)されるため、2回目以降のアクセスでは永続層へのアクセスは発生せず、キャッシュされているEntityが返却される。
Optional<T> findById(ID id)
saveAndFlush
メソッド実行後は、対象エンティティは EntityManager の管理下に置かれている(キャッシュされている)。そのため、後続で JPA の find
系メソッドを実行しても、キャッシュを見ているため、 DB 側で変更した内容は取得できない。
EntityManager の管理 (キャッシュ)の期間は、トランザクションの生存期間だと考えられる。
・参考サイト