Moving #ElementCollection to #Embeddable class not working - java

I am trying to move an #ElementCollection annotated field from my #Entity class into a different class that I annotated with #Embeddable. When the field is in the entity, Hibernate will issue the proper INSERT statements. When the field is moved to the #Embeddable class, no INSERT statements are generated.
So this works:
#Entity
public class MyEntity extends AbstractEntity<MyEntityId> {
#ElementCollection
#OrderColumn
private List<Double> doubles;
}
But this does not:
#Entity
public class MyEntity extends AbstractEntity<MyEntityId> {
#Embedded
private Doubles doubles;
}
#Embeddable
public class Doubles {
#ElementCollection
#OrderColumn
private List<Double> doubles;
}
There is no exception when saving a MyEntity instance (and flushing the EntityManager). If I disable creating the appropriate table in Flyway, the Hibernate validation does trigger a "missing table [my_entity_doubles]" error, so Hibernate does seem to correct interpret the setup.
Using Spring Boot 2.3.4 that uses Hibernate 5.4.21.
UPDATE: There is a reproducible testcase at https://github.com/wimdeblauwe/so-64336648

I don't know why. But assigning with a collection works.
#Embeddable
public class Doubles {
#ElementCollection
private List<Double> doubles = new ArrayList<>();
}

As it stated in the JPA specification (see 2.5 Embeddable Classes section):
An embeddable class (including an embeddable class within another embeddable class) may contain a collection of a basic type or other embeddable class. Direct or indirect circular containment dependencies among embeddable classes are not permitted.
And simple example (based on Hibernate 5.4.21).
Assuming that we have the following schema:
create table T1_MY_ENTITY
(
tm_id int not null,
primary key (tm_id)
);
insert into T1_MY_ENTITY values (1);
create table T1_MY_COLL
(
tc_tm_id int not null,
tc_ord_id int not null,
tc_code varchar(100),
foreign key (tc_tm_id) references T1_MY_ENTITY(tm_id)
);
insert into T1_MY_COLL values (1, 1, 'CL1'), (1, 0, 'CL2');
and the following mapping:
#Entity
#Table(name = "T1_MY_ENTITY")
public class MyEntity
{
#Id
#Column(name = "tm_id")
private Long id;
#Embedded
private Doubles dou;
// constructor, getters, setters
}
#Embeddable
public class Doubles
{
#OrderColumn(name = "tc_ord_id")
#ElementCollection
#CollectionTable(name = "T1_MY_COLL", joinColumns = #JoinColumn(name = "tc_tm_id"))
#Column(name = "tc_code")
private List<String> colls;
// constructor, getters, setters
}
the following code:
MyEntity myEntity = entityManager.find(MyEntity.class, 1L);
myEntity.getDou().getColls().add("CL5");
entityManager.merge(myEntity);
will generate insert:
19:51:29,612 DEBUG SQL:128 -
/* insert collection
row com.my.hibernate.entities.MyEntity.dou.colls */ insert
into
TEST_SCHEMA.T1_MY_COLL
(tc_tm_id, tc_ord_id, tc_code)
values
(?, ?, ?)
Hibernate:
/* insert collection
row com.my.hibernate.entities.MyEntity.dou.colls */ insert
into
TEST_SCHEMA.T1_MY_COLL
(tc_tm_id, tc_ord_id, tc_code)
values
(?, ?, ?)
19:51:29,612 TRACE ResourceRegistryStandardImpl:83 - Registering statement [/* insert collection row com.my.hibernate.entities.MyEntity.dou.colls */ insert into TEST_SCHEMA.T1_MY_COLL (tc_tm_id, tc_ord_id, tc_code) values (?, ?, ?)]
19:51:29,612 TRACE BasicBinder:64 - binding parameter [1] as [BIGINT] - [1]
19:51:29,613 TRACE BasicBinder:64 - binding parameter [2] as [INTEGER] - [2]
19:51:29,613 TRACE BasicBinder:64 - binding parameter [3] as [VARCHAR] - [CL5]
So, It looks like your problem lies in the other plane.
UPDATE: As for your test case, please try to correct it in the following way:
#Test
void test() {
MyEntity2 entity = new MyEntity2();
Doubles doubles = new Doubles();
doubles.setDoubles(List.of(1.0, 1.5));
entity.setDoubles(doubles); // It was absent !!!
repository.save(entity);
entityManager.flush();
assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM my_entity2_doubles", Long.class)).isEqualTo(2);
}

Related

One To Many referencing an entity mapped with Inheritance mapping leads to insert, then update query

I am using Hibernate / JPA 2.1 for persistence in my project. I try to save a FinancialInstrument, which has an embedded field interestRate, which has several PlanSteps The following mapping is set up:
#Entity
#Table(name = "tbl_financial_instrument")
public class FinancialInstrument {
#Embedded
private InterestRate interestRate;
// ...
}
#Embeddable
public class InterestRate {
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "ir_id")
private Set<InterestPaymentPlanStep> interestPaymentPlanSteps = new LinkedHashSet<>();
// ...
}
The plan steps can be reused by different classes, I use inheritance here (single table type).
#Entity
#DiscriminatorValue("INTEREST_PAYMENT")
public class InterestPaymentPlanStep extends PlanStep {
// ...
}
#Entity
#Table(name = "tbl_plan_step")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "PLAN_STEP_TYPE", discriminatorType = DiscriminatorType.STRING)
public abstract class PlanStep extends AbstractBaseObject {
// ...
}
I have filled a Financial Instrument with an InterestRate, containing plan steps. Now I'm trying to persist everything to the database I use this code:
private void persistFinancialInstrument(FinancialInstrument financialInstrument) {
financialInstrument = financialInstrumentRepository.save(financialInstrument);
if (financialInstrument.getInterestRate() != null) {
Set<InterestPaymentPlanStep> interestRatePaymentPlanSteps = financialInstrument.getInterestRate().getInterestPaymentPlanSteps();
for (PlanStep planStep : interestRatePaymentPlanSteps) {
planStepRepository.save(planStep);
}
}
}
The strange thing now is that when I turn on the logging of my queries I see that it executed 2 queries for persisting the plan steps:
This is the first one, note that it does not contain the ID of the financial instrument while I already would expect it here:
insert into tbl_plan_step (creation_datetime, modification_datetime, amount, anchor_date, cap, cycle, floor, rate_add, rate_value, plan_step_type, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, 'INTEREST_PAYMENT', ?)
And the second one:
update tbl_plan_step set ir_id=? where id=?
I have no clue why the saving is being executed in 2 queries. Can anyone find an explanation for this?
This likely is due to PlanStep not having the relation to InterestRate. Make the link bidirectional.
#ManyToOne
private InterestRate interestRate
in PlanStep to perform a single insert.
see hibernate documentation for extensive explanation

Hibernate (on postgres) dropping Schema from #JoinTable

I'm using Hibernate 4.3.10.Final (with SpringData JPA) running on a Postgres 4 database and have run into a very strange bug. Our app utilizes a database outside of the default "public" schema, and when we try to insert data Hibernate drops the correct schema.
Our model consists of an abstract "Log" class that uses single class inheritance to allow many different object types to insert a associated log message. See code below.
The schema already exists (hibernate doesn't create it) and booting validation runs fine, but when try to insert a new record we get the error relation "booking_log" does not exist -- which is missing the schema modifier (say customapp for our purposes). See the first line from the logs below to get an idea of what other insert statements look like.
I've dug through the mapping phase and verified Hibernate is indeed picking up the schema from the #JoinTable annotation, but not sure how we're losing it.
Any help debugging or possible solutions would be appreciated.
Thanks!
Log - Abstract super class
#MappedSuperclass
#Table(name="log", schema=Constants.DB_SCHEMA)
#Inheritance(strategy=InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name="log_type_id", discriminatorType = DiscriminatorType.INTEGER)
public abstract class Log {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="log_seq_gen")
#SequenceGenerator(allocationSize = 1, name="log_seq_gen", sequenceName=Constants.DB_SCHEMA + ".log_id_seq")
private Long id;
// ...
}
BookingLog
#Entity
#DiscriminatorValue("2")
public class BookingLog extends Log implements TenantResource<Company,Long> {
#JoinTable(name="booking_log",
schema = Constants.DB_SCHEMA,
joinColumns = { #JoinColumn(name="log_id", referencedColumnName="id", nullable=false, updatable=false)},
inverseJoinColumns = { #JoinColumn(name="booking_id", referencedColumnName="id", nullable=false, updatable=false)})
#ManyToOne(cascade=CascadeType.ALL)
private Booking booking;
///...
}
** Logs **
2015-07-20_18:14:09.055 DEBUG org.hibernate.SQL - insert into customapp.booking_product (created_dt, created_by, modified_dt, modified_by, include_in_payroll, include_in_revenue, booking_id, description, payroll_percent, price, product_id, qty, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2015-07-20_18:14:09.072 DEBUG org.hibernate.SQL - insert into booking_log (log_date, details, log_time, user_id, booking_id, id) values (?, ?, ?, ?, ?, ?)
2015-07-20_18:14:09.176 DEBUG o.h.e.jdbc.spi.SqlExceptionHelper - could not execute statement [n/a]
org.postgresql.util.PSQLException: ERROR: relation "booking_log" does not exist
Based on your #JoinTable configuration and the insert statement that hibernate generates looks like the problem is the way you are triyng to add extra fields/data to the booking_log table.
I would need more details about your model to be sure but I think you are using a join-table and something else instead of create a class that models the join-table.
I mean, you have this
BookingLog (*) --------------------------------------> (1) Booking
but I think you really need this
BookingLog (1) ---> (1) BookingLogAssociation (*) ---> (1) Booking
Then the mapping will be like this,
#Entity
#DiscriminatorValue("2")
public class BookingLog extends Log implements TenantResource<Company,Long> {
#OneToOne(mappedBy="bookingLog")
private BookingLogAssociation booking;
}
Note the attributes of BookingLogAssociation, they are the extra field/data you want to add in booking_log table.
#Entity
#Table(name="booking_log")
#IdClass(BookingLogAssociationId.class)
public class BookingLogAssociation {
#Id
private long log_id;
#Id
private long booking_id;
#OneToOne
#PrimaryKeyJoinColumn(name="log_id", referencedColumnName="id")
private BookingLog bookingLog;
#ManyToOne
#PrimaryKeyJoinColumn(name="booking_id", referencedColumnName="id")
private Booking booking;
#Column(name="log_date")
#Temporal(TemporalType.DATE)
private Calendar logDate;
#Column(name="log_time")
#Temporal(TemporalType.TIME)
private Calendar logTime;
#ManyToOne
#JoinColumn(name="user_id")
private User user;
// Could be just an attribute too
//#Column(name="user_id")
//private long userId;
...
}
The BookingLogAssociationId class that represents the BookingLogAssociation's composite key.
public class BookingLogAssociationId implements Serializable {
private long log_id;
private long booking_id;
public int hashCode() {
return (int)(log_id + booking_id);
}
public boolean equals(Object object) {
if (object instanceof BookingLogAssociationId) {
BookingLogAssociationId otherId = (BookingLogAssociationId) object;
return (otherId.log_id == this.log_id) && (otherId.booking_id == this.booking_id);
}
return false;
}
You can read more about this option here

Why is JPA ignoring my #OrderColumn in a basic use case?

Inspection Entity:
#Entity
#Table(name="INSPECTION")
public class Inspection implements Serializable
{
...
#OneToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval=true)
#OrderColumn(name="LIST_INDEX", nullable=false)
#JoinColumn(name="INSPECTION_ID")
private List<RecommendationInstance> recommendations;
...
}
RecommendationInstance Entity
#Entity
#Table(name = "RECOMMENDATION_INSTANCE")
public class RecommendationInstance implements Serializable
{
#SequenceGenerator(name="RECOMMENDATION_INST_SEQ_GEN", sequenceName="RECOMMENDATION_INST_SEQ", allocationSize=1, initialValue=100)
#Id #GeneratedValue(generator="RECOMMENDATION_INST_SEQ_GEN", strategy=GenerationType.SEQUENCE)
private Long id;
#Column(name="INSPECTION_ID")
private Long inspectionId;
#ManyToOne
#JoinColumn(name="RECOMMENDATION_ID")
private Recommendation recommendation;
#Column(name="DESCRIPTION")
private String description;
...
}
And the table is created as follows:
CREATE TABLE "RECOMMENDATION_INSTANCE"
( "ID" NUMBER(19,0) NOT NULL,
"INSPECTION_ID" NUMBER(19,0) NOT NULL,
"RECOMMENDATION_ID" NUMBER(19,0) NOT NULL,
"DESCRIPTION" VARCHAR2(4000 BYTE) NOT NULL,
"LIST_INDEX" NUMBER(4,0) NOT NULL
) ;
When a new RecommendationInstance is created and I attempt to save the InspectionEntity I get the following error:
Caused by: org.eclipse.persistence.exceptions.DatabaseException:
Internal Exception: java.sql.SQLIntegrityConstraintViolationException: integrity constraint violation: NOT NULL check constraint; SYS_CT_10161 table: "RECOMMENDATION_INSTANCE" column: "LIST_INDEX"
Error Code: -10
Call: INSERT INTO RECOMMENDATION_INSTANCE (ID, DESCRIPTION, INSPECTION_ID, RECOMMENDATION_ID) VALUES (?, ?, ?, ?)
bind => [102, Sprinkler System DESCRIPTION, 110, 40]
Am I missing some relationship here? It looks as though the list_index is being ignored completely.
To give further information, if needed, I did have this working using a join table. However I am doing a refactor since the join table is not needed. This moved the LIST_INDEX column from the join table to the RecommendationInstance table.
I have done this before but using the #OrderBy annotation, for instance, an piece of code I wrote recently:
#OneToMany(mappedBy = "product")
#OrderBy("createdDateTime ASC")
private Collection<SkuUpc> skuUpcs;
Where SkuUpc has a fied
#Column(name = "created_dt")
private Date createdDateTime = new Timestamp(new Date().getTime());
I found that when I removed the NOT NULL constraint then everything worked (duh), but I decided I can deal with that for now. Looking at the logs, JPA first inserts the row without the list_index (thus the constraint violation) then immediately after runs an update to set the list_index.
This answer really creates a more specific question as to why it doesn't set the list_index upon insertion of the row, even when I specify nullable=false
I asked the more specific question here: Why does JPA update the OrderColumn instead of setting it on creation?

java hibernate override enum/string mapping values

I've got a following Hibernate model:
#Entity
#Table(name = "category")
public class Category {
#Enumerated(EnumType.STRING)
#Column(name = "type")
private CategoryType type;
This is the enumeration referenced by hibernate:
public enum CategoryType {
INCOME, OUTCOME;
}
THe corresponding database field is a varchar which takes 2 possible values: "CategoryIncome" and "CategoryOutcome".
This method actually calls hibernate:
public List<Category> findAllByType(CategoryType type) {
session = sessionFactory.openSession();
tx = session.beginTransaction();
Query query = session.createQuery(
"FROM Category WHERE type = :type");
query.setParameter("type", type);
List list = query.list();
tx.commit();
session.close();
return list;
}
I managed to get my code work (I mean it compiles), but it works badly - it executes following SQL query:
WHERE type = "INCOME"
whereas I would like it to be:
WHERE type = "CategoryIncome"
How can I map enum values into string values for hibernate? I know that EnumType.STRING tells hibernate to cast the enum values to string (could be EnumType.ORDINAL to cast it to integers). But how can I override the default enum-string mapping?
You will have to use your customized usertype for Hibernate persistance, Hibernate uses name() function of enum to get string representation, not toString().
See this
Hibernate #Enumerated mapping

How to map two tables to one entity using foreign-key?

I have a problem very similar to this: How do I join tables on non-primary key columns in secondary tables?
But I'm not sure if I can apply the same solution.
I have two tables like these:
CREATE TABLE CUSTOMER
(
CUSTOMER_ID INTEGER NOT NULL,
DETAIL_ID INTEGER NOT NULL,
PRIMARY KEY( CUSTOMER_ID ),
CONSTRAINT cust_fk FOREIGN KEY( DETAIL_ID ) REFERENCES DETAILS( DETAIL_ID )
)
CREATE TABLE DETAILS
(
DETAIL_ID INTEGER NOT NULL,
OTHER INTEGER NOT NULL,
PRIMARY KEY( DETAIL_ID )
)
I'd like to map these tables to a single class called Customer, so I have:
#Entity
#Table(name = "CUSTOMERS")
#SecondaryTable(name = "DETAILS", pkJoinColumns=#PrimaryKeyJoinColumn(name="DETAIL_ID"))
public class Customer {
#Id
#GeneratedValue
#Column(name = "CUSTOMER_ID")
private Integer id;
#Column(table = "DETAILS", name = "OTHER")
private Integer notes;
// ...
}
but this works only if DETAIL_ID matches CUSTOMER_ID in the primary table.
So my question is: how can i use a foreign-key field in my primary table to join on the primary-key of the secondary table?
UPDATE
I tried to set:
#SecondaryTable(name = "DETAILS", pkJoinColumns=#PrimaryKeyJoinColumn(name="DETAIL_ID", referencedColumnName="DETAIL_ID"))
but when I run the application I get this exception:
org.hibernate.MappingException: Unable to find column with logical name: DETAIL_ID in org.hibernate.mapping.Table(CUSTOMERS) and its related supertables and secondary tables
For anyone looking for an answer to this, using #SecondaryTable is not the way to join two tables with non-primary key columns, because Hibernate will try to assosiate the two tables by their primary keys by default; you have to use #OneToMany review http://viralpatel.net/blogs/hibernate-one-to-many-annotation-tutorial/ for a solution, here's a code snippet in case that url stops working:
Customer Class:
#Entity
#Table(name="CUSTOMERS")
public class Customer {
#Id
#GeneratedValue
#Column(name="CUSTOMER_ID")
private Integer id;
#ManyToOne
#JoinColumn(name="DETAIL_ID")
private Details details;
// Getter and Setter methods...
}
Details Class:
#Entity
#Table(name="DETAILS")
public class Details {
#Id
#GeneratedValue
#Column(name="DETAIL_ID")
private int detailId;
#Column(name="OTHER")
private String other;
#OneToMany(mappedBy="details")
private Set<Customer> customers;
// Getter and Setter methods...
}
This is easily accessible through hibernate with the following code:
Session session = HibernateUtil.getSessionFactory().openSession();
Query query = session.createQuery("select id, details.other from Customer");
I hope this helps anyone out there spending hours searching for a way to achieve this like I did.
You can use the referenceColumnName attribute of the #PrimaryKeyJoinColumn annotation to define the join column to the referenced table. In fact, by combining use of name/referencedColumnName you can join on arbitrary on both sides, with the constraint that if duplicates are found your ORM provider will throw an exception.

Categories