Cześć,
Próbuję zaimplementować w efektywny sposób obsługę waluty z wykorzystaniem Java Money/Currency API, które jest specjalnie do tego stworzone: https://www.baeldung.com/java-money-and-currency
W swoim projekcie korzystam z Spring Data JPA oraz Postgresa jako bazy danych.
W swoim modelu domeny Venue chciałbym mieć obsługę kwoty oraz waluty jako jedno pole/atrybut z typem danych właśnie jako jedna z dostępnych klas Java Money API: Money.
Od strony Postgresa chciałbym żeby Spring Data JPA mapował pole
private Money money;
na 2 osobne kolumny w tabeli Venue, z tym że moim zdaniem można zrobić to jeszcze lepiej.
Po dość długim szukaniu rozwiązań, udało mi się to zaimplementować właśnie w taki sposóbp poprzez stworzenie konwertera/mappera z wykorzystaniem biblioteki Jadira:
https://i.ibb.co/DGpgFXd/currency-implementation.png
Niestety, jak widzicie, moim zdaniem można byłoby zrobić to jeszcze lepiej, bo w obecnej sytuacji w tabeli Venue symbol waluty jest bez sensu powtarzany, co sprawia, że tabela nie jest w pełni znormalizowana, bo symbol waluty np. PLN powtarza się za każdym razem, co powoduje niepotrzebne zwiększenie pamięci w bazie.
Myślałem nad tym, żeby ulepszyć to na następujące sposoby.
- Zamiast tych dwóch kolumn tj. money_currency i money, stworzyć obiekty domenowe Currency i Price lub użyć junction table/tabeli łącznikowej, oraz zrobić odpowiednie mapowania w entity Venue.
W ten sposób miałbym jedną tabelę Currency z walutami, która nie zmieniałaby się, przez co uniknąłbym powtarzania się symbolu waluty jak w obecnym rozwiązaniu. - Zrezygnować całkowicie z Java Money/Currency API, bo mapowanie jak widać jest dość problematyczne i użyć klasycznego rozwiązania z osobnymi tabelami Currency, Price i Venue lub tabelą łącznikową, jak wyżej, tylko bez tego API.
Czy mielibyście jakiś pomysł w jaki sposób można byłoby zmapować typ Money z API Java Money właśnie na taką postać zamiast 2 kolumn?
Wyglądałoby to mniej więcej w taki sposób:
table venue
--------------
id
name
table price
------------
id
venue_id
amount
currency_id
table currencies
----------------
id
name
abbreviation_sign
Czy mielibyście jakieś ciekawe pomysły na rozwiązanie tego problemu?
Venue Entity
import lombok.*;
import org.hibernate.annotations.*;
import org.javamoney.moneta.Money;
import javax.persistence.*;
import javax.persistence.Entity;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TypeDefs(value = {
@TypeDef(name = "moneyAmountWithCurrencyType", typeClass = CustomPersistentMoneyAmountAndCurrency.class)
})
public class Venue {
@Id
@GeneratedValue
@Column(columnDefinition = "uuid", updatable = false)
private UUID id;
private String name;
@Basic( optional = false )
@Columns(columns = {
@Column( name = "money_currency", nullable = false, length = 3 ),
@Column( name = "money", nullable = false )
})
@Type(type = "moneyAmountWithCurrencyType")
private Money money;
private int capacity;
@Column(updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
VenueDto dto() {
MoneyDto moneyDto = MoneyDto.builder()
.amount(money.getNumber().toString())
.currency(money.getCurrency().getCurrencyCode())
.build();
return VenueDto.builder()
.uuid(id.toString())
.name(name)
.money(moneyDto)
.capacity(capacity)
.build();
}
}
CustomPersistentMoneyAmountAndCurrency (Mapper/Konwerter typu Java Money/Currency API klasy Money na dwie kolumny w tabeli Venue)
import org.jadira.usertype.moneyandcurrency.legacyjdk.columnmapper.BigDecimalBigDecimalColumnMapper;
import org.jadira.usertype.moneyandcurrency.moneta.columnmapper.StringColumnCurrencyUnitMapper;
import org.jadira.usertype.spi.shared.AbstractMultiColumnUserType;
import org.jadira.usertype.spi.shared.ColumnMapper;
import org.javamoney.moneta.Money;
import javax.money.CurrencyUnit;
import javax.money.MonetaryAmount;
import java.math.BigDecimal;
public class CustomPersistentMoneyAmountAndCurrency extends AbstractMultiColumnUserType<MonetaryAmount> {
private static final ColumnMapper<?, ?>[] COLUMN_MAPPERS = new ColumnMapper<?, ?>[] { new StringColumnCurrencyUnitMapper(), new BigDecimalBigDecimalColumnMapper() };
private static final String[] PROPERTY_NAMES = new String[]{ "currency", "number" };
@Override
protected ColumnMapper<?, ?>[] getColumnMappers() {
return COLUMN_MAPPERS;
}
@Override
protected Money fromConvertedColumns(Object[] convertedColumns) {
CurrencyUnit currencyUnitPart = (CurrencyUnit) convertedColumns[0];
BigDecimal amountPart = (BigDecimal) convertedColumns[1];
return Money.of(amountPart, currencyUnitPart);
}
@Override
protected Object[] toConvertedColumns(MonetaryAmount value) {
return new Object[] { value.getCurrency(), value.getNumber().numberValue(BigDecimal.class) };
}
@Override
public String[] getPropertyNames() {
return PROPERTY_NAMES;
}
}
VenueDto
@EqualsAndHashCode(callSuper = true)
@ToString
@Builder
@Value
@AllArgsConstructor
@Relation(collectionRelation = "venues")
public class VenueDto extends ResourceSupport {
private final String uuid;
@NotNull
@NotEmpty
private final String name;
@Valid
private final MoneyDto money;
@NotNull
private final int capacity;
}
MoneyDto
@Getter
@Builder
@Value
@EqualsAndHashCode
@ToString
public class MoneyDto {
@NotEmpty
@NotNull
@Digits(integer=7, fraction=2)
@NumberFormat(style = NumberFormat.Style.NUMBER)
private final String amount;
@NotEmpty
@NotNull
private final String currency;
}
VenueRepository
interface VenueRepository extends Repository<Venue, Integer> {
Venue save(Venue venue);
Optional<Venue> findById(UUID id);
// Optional<List<Venue>> findByMoneyLessThan(Money money);
Page<Venue> findAll(Pageable pageable);
long deleteById(UUID id);
default Venue findOneOrThrow(UUID uuid) {
Optional<Venue> venue = findById(uuid);
if (venue.isPresent()) {
return venue.get();
} else {
throw new VenueNotFoundException(uuid.toString());
}
}
}
Z góry dzięki za pomoc! ;)