Я использую JPA и Hibernate 5.
Обычно, если Jiber-бин сохраняется в Hibernate, я ожидаю, что все сохраненные значения, включая сгенерированные, такие как ID, по базе данных или по коду, будутзаполняется на оригинальной сущности.Говоря «сгенерированные значения», я ссылаюсь либо на сгенерированный идентификатор методом @PerPersist
(если это UUID, я делаю это таким образом), либо на @GeneratedValue
с выделенной последовательностью в базе данных;также я ссылаюсь на временные метки, сгенерированные @CreationTimestamp
и @UpdateTimestamp
, которые отвечают за заполнение временной метки в поле при вставке / обновлении строки.
И это действительно работает, когда сущностьимеет простое поле @Id
.
Но, когда у меня есть компонент с составным идентификатором, и когда составной идентификатор является другим компонентом @Embeddable
, метка времени создания и обновления времени выполняется правильно и сохраняетсяв БД, но не заполняется в исходный объект.
Сущность с составным идентификатором:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.OffsetDateTime;
@Entity
@Table(name = "balance_transaction")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BalanceTransaction {
@EmbeddedId
private CompositeIdMerchantProvider compositeIdMerchantProvider;
@Column(name = "rate")
private Integer rate;
@CreationTimestamp
@Column(name = "created")
private OffsetDateTime created;
@UpdateTimestamp
@Column(name = "modified")
private OffsetDateTime modified;
}
Встраиваемый компонент id:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Embeddable
@EqualsAndHashCode
public class CompositeIdMerchantProvider implements Serializable {
@Column(name = "merchant_id")
private Short merchantId;
@Column(name = "provider_id")
private Short providerId;
}
В тесте я сначала сохраняю Provider
, затем Merchant
, затем извлеките идентификатор, назначьте его компоненту идентификатора, установите для него значение BalanceTransaction
и сохраните его в БД.
@Before
public void setup() {
resetMerchant(); // create instance, set values, etc.
resetProvider(); // create instance, set values, etc.
resetBalanceTransaction(); // create instance, set values, etc.
merchantRepository.save(merchant); // NOTE: here `merchant` has all the values saved to DB; I don't have to assign the returned one to another entity of type `Merchant`, i.e., they are populated correctly.
providerRepository.save(provider); // NOTE: this also applies to `provider`
composite.setProviderId(provider.getId());
composite.setMerchantId(merchant.getId());
balanceTransaction.setCompositeIdMerchantProvider(composite);
}
Обратите внимание, что Merchant
и Provider
обаиметь Short
тип @Id
, сгенерированный выделенной последовательностью базы данных Postgresql.И у них обоих есть эти поля:
Например, Merchant
:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Entity
@Table(name = "merchant")
@EqualsAndHashCode(exclude = {"id", "transactions"})
@ToString(exclude = {"transactions"})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Merchant implements Serializable {
@Id
@SequenceGenerator(name="merchant_id_seq", allocationSize=1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="merchant_id_seq")
@Column(name = "id")
private Short id; // limited possibilities
@NotNull
@NotEmpty
@Size(max = 45)
@Column(name = "name")
private String name; // mandatory, max 45 characters
@Column(name = "status")
private Boolean status; // whether merchant is active or not
@CreationTimestamp
@Column(name = "created")
private OffsetDateTime created;
@UpdateTimestamp
@Column(name = "modified")
private OffsetDateTime modified;
@OneToMany(mappedBy = "merchant", fetch = FetchType.LAZY) // the name of property at "many" end
private Set<Transaction> transactions;
/**
* Add Transaction to the set; must be called after any of two constructors,
* because we need to initialize the set first.
* @param t Transaction entity to add
*/
public void addTransaction(Transaction t) {
if (transactions == null) {
transactions = new HashSet<>();
}
this.transactions.add(t);
t.setMerchant(this);
}
/**
* Remove an <code>Transaction</code> from the list of this class.
* Caution: {@link Set#iterator()#remove()} does not work when iterating. We must
* construct a new Set and {@link #setTransactions(Set)} again.
* @param t the <code>Transaction</code> to remove.
*/
public void removeTransaction(Transaction t) {
setTransactions(
transactions.stream().filter(x -> !x.equals(t)).collect(Collectors.toCollection(HashSet::new))
);
}
}
А при сохранении метки времени создаются и заполняются в исходной сущности.
Но, похоже, это не работает для этого теста:
@Test
public void givenABalanceTransactionEntity_WhenISaveItIntoDb_ShouldReturnSavedBalanceTransactionEntity() throws IllegalArgumentException {
//given (in @Before)
//when
serviceClient.saveBalanceTransaction(balanceTransaction); // <-------- "balanceTransaction" will not have timestamp populated
//then
Assert.assertNotNull(balanceTransaction); // <-------- this line passes
Assert.assertNotNull(balanceTransaction.getCreated()); // <------- this line does not pass
Assert.assertNotNull(balanceTransaction.getModified()); // <------- this neither
// cleanup
repository.delete(balanceTransaction);
}
Если я сделаю
BalanceTransaction saved = serviceClient.saveBalanceTransaction(balanceTransaction);
и проверим saved
, этот тест не пройдёт.
Итак:
заполняет сохраненные значения в исходную сущность, не является ли это стандартом?Очень склонен к сбоям?Кажется, это ничего не гарантирует.