Рубрики
Без рубрики

Как сопоставить вычисляемые свойства с помощью JPA и аннотации Hibernate @Formula

Узнайте, как сопоставить свойства вычисляемых объектов с аннотациями Hibernate @Formula или аннотациями JPA @postLoad ad @Transient.

Автор оригинала: Vlad Mihalcea.

Вступление

Как я объяснил в этом вопросе StackOverflow , сопоставление вычисляемых свойств очень просто с помощью JPA и Hibernate.

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

Модель предметной области

Давайте рассмотрим, что у нас есть таблица учетная запись в базе данных, которая выглядит следующим образом:

Мы хотим сопоставить учетную запись таблицу со следующей Вложение сущность:

Сумма денег хранится в центах , и существует атрибут годовой процентной ставки , который показывает, сколько вы получите за хранение денег на этом сберегательном счете.

Однако нам необходимо рассчитать следующие атрибуты сущности на основе вышеупомянутых центов и процентной ставки :

  • getDollars() – дает вам сумму депозита в долларах, а не в центах.
  • getInterestCents() – дает вам проценты в центах, накопленные с тех пор, как вы открыли этот сберегательный счет.
  • getInterestDollars() – дает вам проценты в долларах, накопленные с тех пор, как вы открыли этот сберегательный счет.

Рассчитанные свойства с использованием JPA

Если вы используете JPA, вы можете сопоставить Учетную запись сущность следующим образом:

@Entity(name = "Account")
@Table(name = "account")
public class Account {

    @Id
    private Long id;

    @ManyToOne
    private User owner;

    private String iban;

    private long cents;

    private double interestRate;

    private Timestamp createdOn;

    public Account() {
    }

    public Account(
            Long id, User owner, String iban,
            long cents, double interestRate, Timestamp createdOn) {
        this.id = id;
        this.owner = owner;
        this.iban = iban;
        this.cents = cents;
        this.interestRate = interestRate;
        this.createdOn = createdOn;
    }

    public double getDollars() {
        return cents / 100D;
    }

    public long getInterestCents() {
        long months = createdOn.toLocalDateTime().until(
            LocalDateTime.now(),
            ChronoUnit.MONTHS
        );

        double interestUnrounded = (
            (interestRate / 100D) * cents * months
        ) / 12;

        return BigDecimal.valueOf(interestUnrounded)
            .setScale(0, BigDecimal.ROUND_HALF_EVEN)
            .longValue();
    }

    public double getInterestDollars() {
        return getInterestCents() / 100D;
    }
}

Обратите внимание, что свойства доллары , процентные центы и процентные доллары будут рассчитываться при каждом вызове метода, и это может быть не очень эффективно, если вам нужно вызвать данный метод несколько раз.

Чтобы преодолеть эту проблему, вы можете просто рассчитать эти значения при загрузке объекта из базы данных (при условии, что центы и проценты не будут изменены, так как в противном случае сохранение Учетной записи будет деактивировано).

К счастью, спецификация JPA определяет прослушиватель @postLoad сущности, который мы можем использовать для вычисления этих свойств при загрузке сущности:

@Entity(name = "Account")
@Table(name = "account")
public class Account {

    @Id
    private Long id;

    @ManyToOne
    private User owner;

    private String iban;

    private long cents;

    private double interestRate;

    private Timestamp createdOn;

    @Transient
    private double dollars;

    @Transient
    private long interestCents;

    @Transient
    private double interestDollars;

    public Account() {
    }

    public Account(
            Long id, User owner, String iban, 
            long cents, double interestRate, Timestamp createdOn) {
        this.id = id;
        this.owner = owner;
        this.iban = iban;
        this.cents = cents;
        this.interestRate = interestRate;
        this.createdOn = createdOn;
    }

    @PostLoad
    private void postLoad() {
        this.dollars = cents / 100D;

        long months = createdOn.toLocalDateTime().until(
            LocalDateTime.now(),
            ChronoUnit.MONTHS)
        ;

        double interestUnrounded = (
            (interestRate / 100D) * cents * months
        ) / 12;

        this.interestCents = BigDecimal.valueOf(interestUnrounded)
            .setScale(0, BigDecimal.ROUND_HALF_EVEN)
            .longValue();

        this.interestDollars = interestCents / 100D;
    }

    public double getDollars() {
        return dollars;
    }

    public long getInterestCents() {
        return interestCents;
    }

    public double getInterestDollars() {
        return interestDollars;
    }
}

Свойства доллары , процентные центы и процентные доллары используются с аннотацией @Transient , чтобы избежать их сохранения. Таким образом, эти атрибуты отображаются только на стороне сущности, а не в таблице базы данных.

Вычисленные свойства с использованием аннотации Hibernate @Formula

Hibernate предлагает аннотацию @Formula , которую можно использовать для вычисления заданного атрибута сущности с помощью выражения SQL-запроса:

@Formula("cents::numeric / 100")
private double dollars;

@Formula(
    "round(" +
    "   (interestRate::numeric / 100) * " +
    "   cents * " +
    "   date_part('month', age(now(), createdOn)" +
    ") " +
    "/ 12)")
private long interestCents;

@Formula(
    "round(" +
    "   (interestRate::numeric / 100) * " +
    "   cents * " +
    "   date_part('month', age(now(), createdOn)" +
    ") " +
    "/ 12) " +
    "/ 100::numeric")
private double interestDollars;

@Transient
public double getDollars() {
    return dollars;
}

@Transient
public long getInterestCents() {
    return interestCents;
}

@Transient
public double getInterestDollars() {
    return interestDollars;
}

При извлечении Учетной записи сущности Hibernate выполнит следующий SQL – запрос:

Account account = entityManager.find(Account.class, 1L);
SELECT a.id AS id1_0_0_,
       a.cents AS cents2_0_0_,
       a.createdOn AS createdO3_0_0_,
       a.iban AS iban4_0_0_,
       a.interestRate AS interest5_0_0_,
       a."owner_id" AS owner_id6_0_0_,
       a.cents::numeric / 100 AS formula0_0_,
       round((a.interestRate::numeric / 100) * a.cents * date_part('month', age(now(), a.createdOn)) / 12) AS formula1_0_,
       round((a.interestRate::numeric / 100) * a.cents * date_part('month', age(now(), a.createdOn)) / 12) / 100::numeric AS formula2_0_
FROM account a
WHERE a.id = 1

Вот и все!

Время тестирования

Предполагая, что у нас есть следующая Учетная запись организация:

doInJPA(entityManager -> {
    User user = new User();
    user.setId(1L);
    user.setFirstName("John");
    user.setFirstName("Doe");

    entityManager.persist(user);

    Account account = new Account(
        1L,
        user,
        "ABC123",
        12345L,
        6.7,
        Timestamp.valueOf(
            LocalDateTime.now().minusMonths(3)
        )
    );
    entityManager.persist(account);
});

Мы можем проверить вычисленные свойства следующим образом:

doInJPA(entityManager -> {
    Account account = entityManager.find(Account.class, 1L);

    assertEquals(123.45D, account.getDollars(), 0.001);
    assertEquals(207L, account.getInterestCents());
    assertEquals(2.07D, account.getInterestDollars(), 0.001);
});

Код доступен на GitHub .

Вывод

Сопоставление вычисляемых свойств объектов довольно просто как с JPA, так и с Hibernate, и вам определенно следует воспользоваться этой функцией.