JPA BULK 처리에 대해 알아보기 - 2편
하이버네이트에서 배치
JDBC는 데이터베이스에서 쿼리를 실행하기 위해 Statement와 PreparedStatement라는 두 가지 클래스를 제공하고 일괄 처리 기능을 제공하는 addBatch() 및 executeBatch() 메서드에 대한 자체 구현을 가지고 있습니다.
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 100
하이버네이트에서는 "hibernate.jdbc.batch_size" 라는 전역 구성 설정 사용시 PreparedStatement.addBatch() 및
PreparedStatement.executeBatch()를 사용하여 실행을 위해 일괄 처리할 수 있는 최대 SQL 문의 수에 도달할 때 까지 쿼리를 추가하고 도달시 일괄처리를 실행합니다. 이때, 지정된 갯수만큼 SQL문을 처리하고 트랜잭션을 닫거나 session.flush() session.clear()를 호출하여 세션의 상태를 데이터베이스와 동기화하고 1차 캐시를 비웁니다.
이때 SQL 배치는 같은 SQL일 때만 유효하고 중간에 다른 처리가 들어가면 SQL 배치를 다시 시작함으로 주의해야 합니다.
SQL 문이 실행되는 순서를 제어하는 구성 속성
spring:
jpa:
properties:
hibernate:
order_inserts : true
order_updates : true
hibernate.order_inserts
INSERT/UPDATE/DELETE와 같이 엔티티의 변경이 생성되면 하이버네이트는 변경사항을 ActionQueue에 추가하고 flush()호출시 ActionQueue에 추가되어 있는 작업들을 순차적으로 실행합니다.
hibernate.order_inserts가 true인 경우 삽입해야 하는 종속성이 있는 엔티티들이 외래 키 제약 조건 위반을 방지하기 위해 하이버네이트에서 데이터베이스의 엔티티를 유지, 업데이트 또는 삭제하는 데 필요한 SQL 문의 순서와 실행을 관리하는 ActionQueue는 삽입 문의 순서를 지정하여 부모 개체가 먼저 삽입된 다음 적절한 외래 키 값과 함께 자식 개체가 삽입되도록 합니다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@Column(name = "member_id", updatable = false)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "team_id")
private Team team;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer age;
@Version
private Integer version;
@Builder
public Member(Long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public void joinTeam(Team team) {
if(this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
this.team.addMember(this);
}
public void increaseAge() {
this.age = this.age + 1;
}
}
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id
@Column(name = "id", updatable = false)
private Long id;
private String name;
@Version
private Integer version;
@OneToMany(mappedBy = "team", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
@Builder
public Team(Long id, String name) {
this.id = id;
this.name = name;
}
public void addMember(Member member) {
members.add(member);
}
public void changeNameAndMemberAge() {
this.name = this.name + "!";
members.forEach(Member::increaseAge);
}
}
여기서 @Version은 엔터티가 업데이트될 때마다 필드 또는 속성 값을 자동으로 증가시켜 JPA 공급자가 삽입하거나 업데이트해야 하는 엔티티를 추적합니다. 그리고 존재하지 않는 엔티티를 선택하려고 시도하거나 두 트랜잭션이 동일한 엔티티를 업데이트하려고 시도하는 경우 예외를 발생시켜 충돌을 방지합니다. 사용하지 않는 경우 영속화 되지 않은 member를 조회함으로써 EntityNotFoundException가 발생합니다.
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
private final static int BATCH_SIZE = 20;
private final static int BATCH_FLUSH_SIZE = 4;
@PersistenceContext
private final EntityManager entityManager;
private final TeamSpringJpaRepository teamSpringJpaRepository;
@Override
@Transactional
public void saveUserAndTeam() {
for (int i = 0; i < BATCH_SIZE; i++) {
Team team = Team.builder().id((long) i).name(String.format("Team %d", i)).build();
for (int j = 0; j < BATCH_FLUSH_SIZE; j++) {
Member member = Member.builder().id((long) (i*BATCH_FLUSH_SIZE + j)).name(String.format("User %d-%d", i,j)).age(j).build();
member.joinTeam(team);
}
teamSpringJpaRepository.save(team);
}
entityManager.flush();
}
}
@SpringBootTest
class MemberServiceImplTest {
@Autowired
private MemberService memberService;
@Test
void saveUserAndTeam() {
memberService.saveUserAndTeam();
}
}
hibernate.order_inserts가 false(기본값)
[QUERY] insert into team (name, version, id) values ('Team 0', 0, 0)
[QUERY] insert into member (age, name, team_id, version, member_id) values (0, 'User 0-0', 0, 0, 0)
[QUERY] insert into member (age, name, team_id, version, member_id) values (1, 'User 0-1', 0, 0, 1)
....
hibernate.order_inserts가 true
[QUERY] insert into team (name, version, id) values ('Team 0', 0, 0),('Team 1', 0, 1),('Team 2', 0, 2),('Team 3', 0, 3),('Team 4', 0, 4),('Team 5', 0, 5),('Team 6', 0, 6),('Team 7', 0, 7),('Team 8', 0, 8),('Team 9', 0, 9),('Team 10', 0, 10),('Team 11', 0, 11),('Team 12', 0, 12),('Team 13', 0, 13),('Team 14', 0, 14),('Team 15', 0, 15),('Team 16', 0, 16),('Team 17', 0, 17),('Team 18', 0, 18),('Team 19', 0, 19)
[QUERY] insert into member (age, name, team_id, version, member_id) values (0, 'User 0-0', 0, 0, 0),(1, 'User 0-1', 0, 0, 1),(2, 'User 0-2', 0, 0, 2),(3, 'User 0-3', 0, 0, 3),(0, 'User 1-0', 1, 0, 4),(1, 'User 1-1', 1, 0, 5),(2, 'User 1-2', 1, 0, 6),(3, 'User 1-3', 1, 0, 7),(0, 'User 2-0', 2, 0, 8),(1, 'User 2-1', 2, 0, 9),(2, 'User 2-2', 2, 0, 10),(3, 'User 2-3', 2, 0, 11),(0, 'User 3-0', 3, 0, 12),(1, 'User 3-1', 3, 0, 13),(2, 'User 3-2', 3, 0, 14),(3, 'User 3-3', 3, 0, 15),(0, 'User 4-0', 4, 0, 16),(1, 'User 4-1', 4, 0, 17),(2, 'User 4-2', 4, 0, 18),(3, 'User 4-3', 4, 0, 19),(0, 'User 5-0', 5, 0, 20),(1, 'User 5-1', 5, 0, 21),(2, 'User 5-2', 5, 0, 22),(3, 'User 5-3', 5, 0, 23),(0, 'User 6-0', 6, 0, 24),(1, 'User 6-1', 6, 0, 25),(2, 'User 6-2', 6, 0, 26),(3, 'User 6-3', 6, 0, 27),(0, 'User 7-0', 7, 0, 28),(1, 'User 7-1', 7, 0, 29),(2, 'User 7-2', 7, 0, 30),(3, 'User 7-3', 7, 0, 31),(0, 'User 8-0', 8, 0, 32),(1, 'User 8-1', 8, 0, 33),(2, 'User 8-2', 8, 0, 34),(3, 'User 8-3', 8, 0, 35),(0, 'User 9-0', 9, 0, 36),(1, 'User 9-1', 9, 0, 37),(2, 'User 9-2', 9, 0, 38),(3, 'User 9-3', 9, 0, 39),(0, 'User 10-0', 10, 0, 40),(1, 'User 10-1', 10, 0, 41),(2, 'User 10-2', 10, 0, 42),(3, 'User 10-3', 10, 0, 43),(0, 'User 11-0', 11, 0, 44),(1, 'User 11-1', 11, 0, 45),(2, 'User 11-2', 11, 0, 46),(3, 'User 11-3', 11, 0, 47),(0, 'User 12-0', 12, 0, 48),(1, 'User 12-1', 12, 0, 49),(2, 'User 12-2', 12, 0, 50),(3, 'User 12-3', 12, 0, 51),(0, 'User 13-0', 13, 0, 52),(1, 'User 13-1', 13, 0, 53),(2, 'User 13-2', 13, 0, 54),(3, 'User 13-3', 13, 0, 55),(0, 'User 14-0', 14, 0, 56),(1, 'User 14-1', 14, 0, 57),(2, 'User 14-2', 14, 0, 58),(3, 'User 14-3', 14, 0, 59),(0, 'User 15-0', 15, 0, 60),(1, 'User 15-1', 15, 0, 61),(2, 'User 15-2', 15, 0, 62),(3, 'User 15-3', 15, 0, 63),(0, 'User 16-0', 16, 0, 64),(1, 'User 16-1', 16, 0, 65),(2, 'User 16-2', 16, 0, 66),(3, 'User 16-3', 16, 0, 67),(0, 'User 17-0', 17, 0, 68),(1, 'User 17-1', 17, 0, 69),(2, 'User 17-2', 17, 0, 70),(3, 'User 17-3', 17, 0, 71),(0, 'User 18-0', 18, 0, 72),(1, 'User 18-1', 18, 0, 73),(2, 'User 18-2', 18, 0, 74),(3, 'User 18-3', 18, 0, 75),(0, 'User 19-0', 19, 0, 76),(1, 'User 19-1', 19, 0, 77),(2, 'User 19-2', 19, 0, 78),(3, 'User 19-3', 19, 0, 79)
hibernate.order_update
업데이트되는 항목의 엔터티 유형 및 기본 키 값에 따라 외래 키 제약 조건이 없는 엔터티를 먼저 업데이트한 다음 해당 엔터티에 종속성이 있는 엔터티를 업데이트합니다. 이렇게 하면 외래 키 제약 조건을 위반하지 않고 업데이트가 올바른 순서로 실행되도록 보장합니다.
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
...
@Override
@Transactional
public void updateUserAndTeam() {
List<Team> teams = teamSpringJpaRepository.findAllTeamAndUser();
teams.forEach(Team::changeNameAndMemberAge);
}
}
public interface TeamSpringJpaRepository extends JpaRepository<Team, Integer> {
@Query("select t from Team t join fetch t.members")
List<Team> findAllTeamAndUser();
}
@SpringBootTest
class MemberServiceImplTest {
...
@Test
void updateUserAndTeam() {
memberService.updateUserAndTeam();
}
}
hibernate.order_update가 true(기본값)
[QUERY] update team set name='Team 0!', version=2 where id=0 and version=1
[QUERY] update member set age=5, name='User 0-0', team_id=0, version=2 where member_id=0 and version=1;update member set age=6, name='User 0-1', team_id=0, version=2 where member_id=1 and version=1;update member set age=7, name='User 0-2', team_id=0, version=2 where member_id=2 and version=1;update member set age=8, name='User 0-3', team_id=0, version=2 where member_id=3 and version=1
[QUERY] update team set name='Team 1!', version=2 where id=1 and version=1
[QUERY] update member set age=5, name='User 1-0', team_id=1, version=2 where member_id=4 and version=1;update member set age=6, name='User 1-1', team_id=1, version=2 where member_id=5 and version=1;update member set age=7, name='User 1-2', team_id=1, version=2 where member_id=6 and version=1;update member set age=8, name='User 1-3', team_id=1, version=2 where member_id=7 and version=1
...
hibernate.order_update가 false
update team set name='Team 0!', version=1 where id=0 and version=0;update team set name='Team 1!', version=1 where id=1 and version=0;update team set name='Team 2!', version=1 where id=2 and version=0;update team set name='Team 3!', version=1 where id=3 and version=0;update team set name='Team 4!', version=1 where id=4 and version=0;update team set name='Team 5!', version=1 where id=5 and version=0;update team set name='Team 6!', version=1 where id=6 and version=0;update team set name='Team 7!', version=1 where id=7 and version=0;update team set name='Team 8!', version=1 where id=8 and version=0;update team set name='Team 9!', version=1 where id=9 and version=0;update team set name='Team 10!', version=1 where id=10 and version=0;update team set name='Team 11!', version=1 where id=11 and version=0;update team set name='Team 12!', version=1 where id=12 and version=0;update team set name='Team 13!', version=1 where id=13 and version=0;update team set name='Team 14!', version=1 where id=14 and version=0;update team set name='Team 15!', version=1 where id=15 and version=0;update team set name='Team 16!', version=1 where id=16 and version=0;update team set name='Team 17!', version=1 where id=17 and version=0;update team set name='Team 18!', version=1 where id=18 and version=0;update team set name='Team 19!', version=1 where id=19 and version=0
update member set age=1, name='User 0-0', team_id=0, version=1 where member_id=0 and version=0;update member set age=2, name='User 0-1', team_id=0, version=1 where member_id=1 and version=0;update member set age=3, name='User 0-2', team_id=0, version=1 where member_id=2 and version=0;update member set age=4, name='User 0-3', team_id=0, version=1 where member_id=3 and version=0;update member set age=1, name='User 1-0', team_id=1, version=1 where member_id=4 and version=0;update member set age=2, name='User 1-1', team_id=1, version=1 where member_id=5 and version=0;update member set age=3, name='User 1-2', team_id=1, version=1 where member_id=6 and version=0;update member set age=4, name='User 1-3', team_id=1, version=1 where member_id=7 and version=0;update member set age=1, name='User 2-0', team_id=2, version=1 where member_id=8 and version=0;update member set age=2, name='User 2-1', team_id=2, version=1 where member_id=9 and version=0;update member set age=3, name='User 2-2', team_id=2, version=1 where member_id=10 and version=0;update member set age=4, name='User 2-3', team_id=2, version=1 where member_id=11 and version=0;update member set age=1, name='User 3-0', team_id=3, version=1 where member_id=12 and version=0;update member set age=2, name='User 3-1', team_id=3, version=1 where member_id=13 and version=0;update member set age=3, name='User 3-2', team_id=3, version=1 where member_id=14 and version=0;update member set age=4, name='User 3-3', team_id=3, version=1 where member_id=15 and version=0;update member set age=1, name='User 4-0', team_id=4, version=1 where member_id=16 and version=0;update member set age=2, name='User 4-1', team_id=4, version=1 where member_id=17 and version=0;update member set age=3, name='User 4-2', team_id=4, version=1 where member_id=18 and version=0;update member set age=4, name='User 4-3', team_id=4, version=1 where member_id=19 and version=0;update member set age=1, name='User 5-0', team_id=5, version=1 where member_id=20 and version=0;update member set age=2, name='User 5-1', team_id=5, version=1 where member_id=21 and version=0;update member set age=3, name='User 5-2', team_id=5, version=1 where member_id=22 and version=0;update member set age=4, name='User 5-3', team_id=5, version=1 where member_id=23 and version=0;update member set age=1, name='User 6-0', team_id=6, version=1 where member_id=24 and version=0;update member set age=2, name='User 6-1', team_id=6, version=1 where member_id=25 and version=0;update member set age=3, name='User 6-2', team_id=6, version=1 where member_id=26 and version=0;update member set age=4, name='User 6-3', team_id=6, version=1 where member_id=27 and version=0;update member set age=1, name='User 7-0', team_id=7, version=1 where member_id=28 and version=0;update member set age=2, name='User 7-1', team_id=7, version=1 where member_id=29 and version=0;update member set age=3, name='User 7-2', team_id=7, version=1 where member_id=30 and version=0;update member set age=4, name='User 7-3', team_id=7, version=1 where member_id=31 and version=0;update member set age=1, name='User 8-0', team_id=8, version=1 where member_id=32 and version=0;update member set age=2, name='User 8-1', team_id=8, version=1 where member_id=33 and version=0;update member set age=3, name='User 8-2', team_id=8, version=1 where member_id=34 and version=0;update member set age=4, name='User 8-3', team_id=8, version=1 where member_id=35 and version=0;update member set age=1, name='User 9-0', team_id=9, version=1 where member_id=36 and version=0;update member set age=2, name='User 9-1', team_id=9, version=1 where member_id=37 and version=0;update member set age=3, name='User 9-2', team_id=9, version=1 where member_id=38 and version=0;update member set age=4, name='User 9-3', team_id=9, version=1 where member_id=39 and version=0;update member set age=1, name='User 10-0', team_id=10, version=1 where member_id=40 and version=0;update member set age=2, name='User 10-1', team_id=10, version=1 where member_id=41 and version=0;update member set age=3, name='User 10-2', team_id=10, version=1 where member_id=42 and version=0;update member set age=4, name='User 10-3', team_id=10, version=1 where member_id=43 and version=0;update member set age=1, name='User 11-0', team_id=11, version=1 where member_id=44 and version=0;update member set age=2, name='User 11-1', team_id=11, version=1 where member_id=45 and version=0;update member set age=3, name='User 11-2', team_id=11, version=1 where member_id=46 and version=0;update member set age=4, name='User 11-3', team_id=11, version=1 where member_id=47 and version=0;update member set age=1, name='User 12-0', team_id=12, version=1 where member_id=48 and version=0;update member set age=2, name='User 12-1', team_id=12, version=1 where member_id=49 and version=0;update member set age=3, name='User 12-2', team_id=12, version=1 where member_id=50 and version=0;update member set age=4, name='User 12-3', team_id=12, version=1 where member_id=51 and version=0;update member set age=1, name='User 13-0', team_id=13, version=1 where member_id=52 and version=0;update member set age=2, name='User 13-1', team_id=13, version=1 where member_id=53 and version=0;update member set age=3, name='User 13-2', team_id=13, version=1 where member_id=54 and version=0;update member set age=4, name='User 13-3', team_id=13, version=1 where member_id=55 and version=0;update member set age=1, name='User 14-0', team_id=14, version=1 where member_id=56 and version=0;update member set age=2, name='User 14-1', team_id=14, version=1 where member_id=57 and version=0;update member set age=3, name='User 14-2', team_id=14, version=1 where member_id=58 and version=0;update member set age=4, name='User 14-3', team_id=14, version=1 where member_id=59 and version=0;update member set age=1, name='User 15-0', team_id=15, version=1 where member_id=60 and version=0;update member set age=2, name='User 15-1', team_id=15, version=1 where member_id=61 and version=0;update member set age=3, name='User 15-2', team_id=15, version=1 where member_id=62 and version=0;update member set age=4, name='User 15-3', team_id=15, version=1 where member_id=63 and version=0;update member set age=1, name='User 16-0', team_id=16, version=1 where member_id=64 and version=0;update member set age=2, name='User 16-1', team_id=16, version=1 where member_id=65 and version=0;update member set age=3, name='User 16-2', team_id=16, version=1 where member_id=66 and version=0;update member set age=4, name='User 16-3', team_id=16, version=1 where member_id=67 and version=0;update member set age=1, name='User 17-0', team_id=17, version=1 where member_id=68 and version=0;update member set age=2, name='User 17-1', team_id=17, version=1 where member_id=69 and version=0;update member set age=3, name='User 17-2', team_id=17, version=1 where member_id=70 and version=0;update member set age=4, name='User 17-3', team_id=17, version=1 where member_id=71 and version=0;update member set age=1, name='User 18-0', team_id=18, version=1 where member_id=72 and version=0;update member set age=2, name='User 18-1', team_id=18, version=1 where member_id=73 and version=0;update member set age=3, name='User 18-2', team_id=18, version=1 where member_id=74 and version=0;update member set age=4, name='User 18-3', team_id=18, version=1 where member_id=75 and version=0;update member set age=1, name='User 19-0', team_id=19, version=1 where member_id=76 and version=0;update member set age=2, name='User 19-1', team_id=19, version=1 where member_id=77 and version=0;update member set age=3, name='User 19-2', team_id=19, version=1 where member_id=78 and version=0;update member set age=4, name='User 19-3', team_id=19, version=1 where member_id=79 and version=0
기본키 생성 전략에 따른 BATCH 처리
JPA에서는 기본키 자동 생성을 데이터베이스에 위임하는 IDENTITY, 데이터베이스 시퀀스를 사용해서 기본 키를 할당하는 SEQUENCE, 키 생성 테이블을 사용하는 TABLE 전략이 존재합니다.
Hibernate는 ID 식별자 생성기(identity identifier generator)를 사용하는 경우 기본 키 값을 INSERT시 데이터베이스에 의존하여 알 수 있고 삽입되는 각 행에 대해 생성된 기본 키 값을 SELECT last_insert_id();와 같은 쿼리를 통해 검색해야 하므로 여러 INSERT 문을 단일 SQL 문으로 일괄 처리할 수 없어 JDBC 수준에서 배치 처리를 비활성화 합니다.
create table hibernate_sequences (
sequence_name varchar2(255 char) not null,
next_val number(19,0),
primary key (sequence_name)
)
MySQL에는 SEQUENCE가 존재하지 않음으로 따로 설정하지 않을 시 Hibernate ORM에서 자동으로 생성하고 사용하는 데이터베이스 시퀀스인 hibernate_sequence를 생성하여 TABLE 방식으로 동작합니다.
1. 현재의 시퀀스를 조회합니다.
[QUERY] select next_val as id_val from hibernate_sequence for update
2. 현재 값에서 1을 추가하여 업데이트를 진행합니다.
[QUERY] update hibernate_sequence set next_val= 2 where next_val=1
3. 해당 값을 Entity의 ID 값으로 사용합니다.
데이터베이스 식별자 할당을 최적화하기 위한 전략
SequenceGenerator.allocationSIze
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my_seq")
@SequenceGenerator(name = "my_seq", sequenceName = "my_sequence", allocationSize = 10)
private Integer id;
....
}
[QUERY] create table my_sequence (next_val bigint) engine=InnoDB
[QUERY] insert into my_sequence values ( 1 )
[QUERY] select next_val as id_val from my_sequence for update
[QUERY] update my_sequence set next_val= 11 where next_val=1
...
@SequenceGenerator.allocationSIze를 사용해서 설정한 값 만큼 JPA Provider가 데이터베이스에 요청해 한번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당하여 새 시퀀스 값의 블록을 요청하기 전까지 해당 블록을 사용합니다.
optimizer
엔티티의 사용 가능한 식별자 값으로 구성된 풀에서 식별자 할당을 관리하는 방식으로 식별자 할당을 위해 데이터베이스를 방문하는 횟수를 줄이고, 여러 트랜잭션이 동시에 식별자를 할당하려고 시도할 때 발생할 수 있는 충돌이나 경합을 피합니다.
종류
매번 데이터베이스에 요청하여 시퀀스 값을 받아 최적화를 하지 않는 none, 다음 값을 요청할 때 마자 지정한 값 만큼 메모리에 풀을 정의하고 가장 높은 값을 저장하는 pooled, pooled과 동일하게 동작하지만 가장 낮은 값을 저장하는 pooled-lo, 시퀀스 풀 값을 가장 낮은 값으로 저장하고 ThreadLocal을 사용하여 생성 상태를 캐시하는 pooled-lotl가 있습니다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my_seq")
@GenericGenerator(
name = "my_seq",
strategy = "sequence",
parameters = {
@org.hibernate.annotations.Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "my_sequence"),
@org.hibernate.annotations.Parameter(name = SequenceStyleGenerator.INCREMENT_PARAM, value = "10"),
@org.hibernate.annotations.Parameter(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "pooled-lo")
// @org.hibernate.annotations.Parameter(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "pooled")
// @org.hibernate.annotations.Parameter(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "pooled-loti")
}
)
}
출저
https://techblog.woowahan.com/2695/
http://devdoc.net/javaweb/hibernate/Hibernate-5.1.0/userGuide/en-US/html/ch11.html
https://www.baeldung.com/jpa-hibernate-batch-insert-update
https://www.baeldung.com/hibernate-entitynotfoundexception
https://techblog.woowahan.com/2663/
https://vladmihalcea.com/hibernate-hidden-gem-the-pooled-lo-optimizer/