Reputation: 988
I am developing a REST service using Spring Boot and Hibernate using JPA for persistence. There is a set of classes the UML guys defined that look like they'll be difficult to implement, and I don't yet know how to do it.
These classes are for storing typed data. One set of classes derive from a class named XBRLUnitType and includes a class named monetaryUnitType that holds a currency code. The other set of classes hold a Quantity, which is a "value" plus the data type. One of those Quantities is Monetary, which has a Float to hold the value, and it has monetaryUnitType, as so:
@Embeddable
public class Monetary extends Quantity {
@JsonProperty("itemUnit")
@Embedded
private monetaryItemType itemUnit = null;
}
@Embeddable
public class Quantity {
@JsonProperty("uncertainty")
private Float uncertainty = null;
@JsonProperty("value")
private Float value = null;
@JsonProperty("itemUnit")
@Embedded
private XBRLItemType itemUnit = null;
}
@Embeddable
public class monetaryItemType extends XBRLItemType {
}
@Embeddable
public class XBRLItemType {
@JsonProperty("symbol")
protected String symbol = null;
@JsonProperty("unitID")
private String unitID = null;
@JsonProperty("unitName")
private String unitName = null;
}
And then in one of the classes I have this field declared:
@Entity
public class Device {
... many other fields
@Embedded
private Monetary currency = null;
... many other fields
}
To create an item, I POST some JSON and I see that the JSON is properly interpreted, with the currency field in the resulting Java object being filled in correctly.
BUT ... in the Hibernate debugging output Hibernate does not create columns in the database matching the currency column.
create table device (dtype varchar(31) not null, deviceid binary not null, description varchar(255), meta varchar(255), name varchar(255), documentation varchar(255), type integer, cost float, manual varchar(255), manufacture_date_time binary(255), manufacturer varchar(255), model_number varchar(255), purchase_date_time binary(255), serial_number varchar(255), warranty_promise varchar(255), has_built_in_meter boolean, inverter_type integer, phase_type integer, ground_coverage_ratio float, hasdcoptimizer boolean, has_micro_inverter boolean, module_type integer, firmwareid integer, nameplateid binary, pv_arrayid binary, sub_arrayid binary, pv_stringid binary, systemid binary, primary key (deviceid))
Further the database table does not have matching columns.
When I retrieve the object using a GET request, the currency field is null. Presumably that's because it doesn't have the database columns.
In this online e-book I found a statement that Java Persistence does not support complex @Embedded objects: https://en.wikibooks.org/wiki/Java_Persistence/Embeddables
That is "The JPA spec does not allow inheritance in embeddable objects" ... obviously I have inheritance. And, "The JPA 1.0 spec only allows Basic relationships in an embeddable object, so nested embeddables are not supported", and I believe what I have is a nested embeddable.
One thing I looked into is a Converter:
@Embedded
@Column(name="currency", columnDefinition="VARCHAR")
@Convert(converter = MonetaryConverter.class, attributeName="currency")
private Monetary currency = null;
But, the methods on the MonetaryConverter class were never invoked.
And this:
@Type(type="com.amzur.orangebuttonapi.model.primitives.Monetary")
@Columns(columns = {
@Column(name="value"), @Column(name="uncertainty"), @Column(name="currencyCode")
})
@ColumnTransformers(value = {
@ColumnTransformer(
forColumn="value",
read="value",
write="?"
),
@ColumnTransformer(
forColumn="uncertainty",
read="uncertainty",
write="?"
),
@ColumnTransformer(
forColumn="currencyCode",
read="itemUnit.symbol",
write="?"
)
})
private Monetary currency = null;
It's supposed to create some Columns to store some values that will fix things up.
Basically I'm looking for ways to persist an Embeddable which must be complex.
Otherwise I'll need to convert these classes to @Entity at the cost of many more tables?
UPDATE: Did a little clarification above.
In the javax.persistence annotations (https://javaee.github.io/javaee-spec/javadocs/) I see that @Convert applies to Basic properties. Therefore @Convert would not work with the non-Basic property I'm dealing with.
Upvotes: 2
Views: 5536
Reputation: 3633
You want to convert an instance of Monetary class to float to store the value
in cost
column of device
table when storing data as rest of the properties are absent in database table. While loading you want to convert cost
to an instance of Monetary. I will come to this later in my answer.
Before that I have few observations for you:
@Embeddable
classes. So you don't again need to use @Embedded
annotation inside an entity when using that class to store a property. Both doesn't hurt but that isn't necessary.They are alternatives to each other. Mark either the component class as @Embeddable
or the property in the owning entity class as @Embedded
.To map property name of an entity to a column name use @Column
annotation.
@ColumnTransformer
should be used for some transformation of value stored in any column. For example converting celsius to fahrenheit and vice-versa. They should be used over individual fine grained property(which has a mapped column in database) of your embedded entity class and not over an embedded class. Hibernate will use them at runtime to convert a column value shown in application layer to a value using the expression written in read
and write
.
Finally coming to converters!!!
Converters are used to convert application layer entity into Db layer entity. Pretty much the same as an ORM,eh. Nopes. Your application layer has a Monetary Entity which has value,unitId,symbol and other such properties. But your database table has only cost column which is a classic case for using AttributeConverter but not @Embeddable
.
Right now you should be removing all @Embeddable
and @Embedded
annotations from
Monetary, Quantity, XBRLItemType and monetaryItemType ( class names should always start with upper case in Java, grumbles !!!! ) classes because they don't have all the propeties mapped to database columns. They should be treated as simple POJO classes implementing Serializable interface. Then you should write MonetaryConverter.class
to convert from Monetary
to cost and vice versa.
Later when your DBA will upgrade your database table to include columns like symbol, uncertainty, unitId,etc in your device
table, you can drop the MonetaryConverter.class
from your project and make Monetary, Quantity, XBRLItemType and monetaryItemType classes @Embeddable
. It will automatically map to your database columns. If property names and database column names will vary, you can just add @Column
over the property and map them. Simple!!!
Assuming you will write your MonetaryConverter.class
correctly by implementing javax.persistence.AttributeConverter
interface and overriding the two methods
convertToEntityAttribute()
and convertToDatabaseColumn()
. Your code should look something like this:
@Convert(converter = MonetaryConverter.class, disableConversion=false)
@Column(name="cost")
private Monetary currency;
This will convert your Monetary property into cost and vice-versa.
Upvotes: 3
Reputation: 988
After some research and reflecting upon the JPA annotations, I found an answer using advice in this post: http://www.concretepage.com/java/jpa/jpa-entitylisteners-example-with-callbacks-prepersist-postpersist-postload-preupdate-postupdate-preremove-postremove
@Entity
@EntityListeners(DeviceEntityListener.class)
public class Device {
... other fields
// Because these are not marked @JsonProperty("xyzzy")
// Jackson won't serialize these to/from JSON
private Float currencyUncertainty = null;
private Float currencyValue = null;
private String currencyCode = null;
@JsonProperty("currency")
@Transient
private Monetary currency = null;
... getters/setters etc
}
This much sets up a Listener to catch Entity events. The new fields will act to shadow the values in the Monetary object. The Monetary object is marked with Transient so that Hibernate won't persist it.
public class DeviceEntityListener {
@PrePersist
public void devicePrePersist(Device device) {
if (device.getCurrency() != null) {
Monetary currency = device.getCurrency();
device.setCurrencyUncertainty(currency.getUncertainty());
device.setCurrencyValue(currency.getValue());
device.setCurrencyCode(currency.getItemUnit().getSymbol());
}
}
@PostUpdate
@PostLoad
public void devicePostLoad(Device device) {
if (device.getCurrencyCode() != null
|| device.getCurrencyUncertainty() != null
|| device.getCurrencyValue() != null) {
Monetary currency = new Monetary();
currency.setUncertainty(device.getCurrencyUncertainty());
currency.setValue(device.getCurrencyValue());
currency.setItemUnit(new monetaryItemType());
currency.getItemUnit().setSymbol(device.getCurrencyCode());
device.setCurrency(currency);
}
}
}
This code is then executed on those events. Before persisting to DB, it copies values out of the Monetary object into the shadow fields. And after recreating from DB, it creates a new Monetary object from those shadow fields.
Upvotes: 2