В моей модели базы данных у каждого объекта есть поле версии. У меня есть три простых объекта (клиент, долг, платеж) с отношениями @OneToMany. У клиента много долгов. Долг имеет много выплат. Я также использую Orika для сопоставления классов (dto -> entity и entity -> dto).
У меня есть 2 теста:
- Я полностью заполнил и сохранил ClientEntity. Затем я меняю несколько раз только одно свойство в ClientEntity и сохраняю после каждого изменения. В конце теста версия ClientEntity увеличивается. Версия дочерних сущностей равна 0 (поскольку они не изменяются).
- Когда я выполняю тот же тестовый пример, но используя ClientDto с отображением Orika, в конце тестового примера я вижу, что увеличивается не только версия клиента, но и дочерняя сущность, которая не была изменена.
Я не могу понять это поведение, и я не знаю, почему это происходит. Я создал простой проект с написанным тестом, в котором легко понять, что не так, см.: https://github.com/Kondziqq/jpa-versioning-problem.git.
Наиболее важные части кода:
Объект клиента:
// necessary annotations
public class ClientEntity extends BaseEntity {
@Id
@SequenceGenerator(sequenceName = "CLIENT_SEQUENCE", name = "CLIENT_SEQUENCE", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CLIENT_SEQUENCE")
private Long id;
private String firstName;
private String lastName;
private String language;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "client_id")
private Set<DebtEntity> debts;
}
Задолженность:
// necessary annotations
public class DebtEntity extends BaseEntity {
@Id
@SequenceGenerator(sequenceName = "DEBT_SEQUENCE", name = "DEBT_SEQUENCE", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "DEBT_SEQUENCE")
private Long id;
private BigDecimal amount;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "debt_id")
private Set<PaymentEntity> payments;
}
Базовая сущность:
// necessary annotations
public abstract class BaseEntity implements Serializable {
@Version
private Integer version;
@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
private Date creationDate;
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
private Date lastModificationDate;
}
Преобразователь Орика:
@Component
@Getter
public class Converter {
private MapperFacade mapper;
@PostConstruct
public void init() {
var mapperFactory = new DefaultMapperFactory.Builder().build();
registerMappings(mapperFactory);
mapper = mapperFactory.getMapperFacade();
}
private void registerMappings(DefaultMapperFactory mapperFactory) {
mapperFactory.classMap(BaseDto.class, BaseEntity.class).byDefault().register();
mapperFactory.classMap(Client.class, ClientEntity.class).byDefault().register();
mapperFactory.classMap(Debt.class, DebtEntity.class).byDefault().register();
mapperFactory.classMap(Payment.class, PaymentEntity.class).byDefault().register();
mapperFactory.classMap(BaseEntity.class, BaseDto.class).byDefault().register();
mapperFactory.classMap(ClientEntity.class, Client.class).byDefault().register();
mapperFactory.classMap(DebtEntity.class, Debt.class).byDefault().register();
mapperFactory.classMap(PaymentEntity.class, Payment.class).byDefault().register();
}
}
КлиентДао:
@RequiredArgsConstructor
@Component
public class ClientDao implements ClientPersistence {
private final ClientRepository clientRepository;
private final Converter converter;
@Override
public long save(Client client) {
var clientEntity = converter.getMapper().map(client, ClientEntity.class);
return clientRepository.save(clientEntity).getId();
}
@Override
public Client getClientById(long id) {
var client = clientRepository.getOne(id);
return converter.getMapper().map(client, Client.class);
}
}
КлиентДаоТест:
// this test fails
@Test
public void shouldNotIncrementChildVersionAfterParentDtoSaved() {
// given
var client = Client.builder()
.firstName("John")
.lastName("Smith")
.language("pl")
.debts(Set.of(
Debt.builder()
.amount(BigDecimal.valueOf(25000))
.payments(Set.of(Payment.builder().amount(BigDecimal.valueOf(2500)).date(Date.from(Instant.now())).build()))
.build()))
.build();
// when
long id = clientDao.save(client);
var clientCopy1 = clientDao.getClientById(id);
clientCopy1.setLanguage("en");
id = clientDao.save(clientCopy1);
var clientCopy2 = clientDao.getClientById(id);
clientCopy2.setLanguage("en");
id = clientDao.save(clientCopy2);
var clientCopy3 = clientDao.getClientById(id);
// then
assertEquals(3, clientCopy3.getVersion());
assertEquals(0, clientCopy3.getDebts().iterator().next().getVersion());
}
// this test passes
@Test
public void shouldNotIncrementChildVersionAfterParentEntitySaved() {
// given
var clientEntity = ClientEntity.builder()
.firstName("John")
.lastName("Smith")
.language("pl")
.debts(Set.of(
DebtEntity.builder()
.amount(BigDecimal.valueOf(25000))
.payments(Set.of(PaymentEntity.builder().amount(BigDecimal.valueOf(2500)).date(Date.from(Instant.now())).build()))
.build()))
.build();
// when
var clientEntityCopy = clientRepository.save(clientEntity);
clientEntityCopy.setLanguage("en");
clientEntityCopy = clientRepository.save(clientEntityCopy);
clientEntityCopy.setLanguage("ru");
clientEntityCopy = clientRepository.save(clientEntityCopy);
// then
assertEquals(2, clientEntityCopy.getVersion());
assertEquals(0, clientEntityCopy.getDebts().iterator().next().getVersion());
}
ИЗМЕНИТЬ
Наконец-то я нашел обходной путь для этой проблемы. Я изменил однонаправленное отношение @OneToMany с @JoinColumn на двунаправленное @OneToMany. Но я до сих пор не понимаю, почему это не работает с однонаправленным @OneToMany.