Hibernate не заполняет сгенерированные @CreationTimestamp и @UpdateTimestamp для объекта с составным идентификатором

#java #hibernate #jpa #hibernate-annotations

#java #переход в спящий режим #jpa #hibernate-аннотации

Вопрос:

Я использую JPA и Hibernate 5.

Обычно, если java-компонент сохраняется с помощью 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;
}
  

В тесте я сначала сохраняю a Provider , затем a Merchant , затем извлекаю ID и присваиваю их компоненту ID, устанавливаю для него значение BalanceTransaction и сохраняю в DB.

 @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);
}
  

Note that Merchant and Provider both have a Short type @Id , generated by a dedicated sequence of a Postgresql database. And, they both have these fields:

For example, 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 , этот тест не завершается ошибкой.

Итак:

заполнение сохраненных значений в исходную сущность, не является стандартом? Очень подвержен сбоям? Кажется, что это ничего не гарантирует.

Комментарии:

1. NPE обычно означает, что экземпляр, для которого вы вызываете методы, имеет значение null. Это означало бы, что balanceTransaction равно нулю

2. Нет, обратите внимание, что предыдущая строка проходит. Я использовал отладчик, и временная метка равна нулю.

3. Вы вызываете, balanceTransaction.setCompositeIdMerchantProvider(composite); где composite указаны только идентификаторы поставщика и продавца. Как вы пришли к выводу, что balanceTransaction.getCreated() это должно быть заполнено значением, например, от продавца? Фактически только на serviceClient.saveBalanceTransaction(balanceTransaction); должны быть созданы и установлены временные метки создания и обновления. Хотя они должны быть сохранены в базе данных. Я не уверен, заполнены ли значения на стороне клиента, но это должно сработать, если вы обновите balanceTransaction перед утверждением

4. Возможно, я был не совсем ясен; посмотрите, у Merchant and BalanceTransaction есть два поля, created и updated , и они оба снабжены @CreationTimestamp и @UpdateTimestamp , которые должны отвечать за заполнение текущей временной метки в таблице при вставке / обновлении. И, как я вижу в Merchant , эти временные метки заполняются для исходного объекта. В Merchant при сохранении это происходит. Но в BalanceTransaction при сохранении этого не происходит. И я подозреваю, что это связано с полем составного идентификатора в BalanceTransaction .

5. Не уверен насчет ваших транзакций, но вполне может быть, что ваш модульный тест на самом деле никогда не записывает данные в базу данных, поэтому вы никогда не сможете увидеть там значения. Не зная, что именно saveBalanceTransaction() делает, трудно сказать, в чем может быть проблема. Если я вас правильно понимаю, вы уже знаете, что вам нужно ( refresh the entity ). У меня такое чувство, что вы смешиваете данные базы данных с локальными данными. На самом деле я почти уверен, что вы делаете что-нибудь в saveBalanceTransaction() , что приводит к сбою