Spring / JPA join on namespaced key - java

I am creating a Spring 4 / Spring Data application for an existing database. The database structure and data are defined by a closed source software.
One aspect of the existing system is that you can create a comment on any other item in the system. This means, that an article, a document, a media file (all entities in the system) can have any number of comments, and each comment is exactly for one entity in the system. All comments are in the same comment table.
The way this is implemented is that the table comment has a column comment_for that holds a concatenated/namespaced/prefixed reference to the actual entity it is a comment for. The current system seems to just builds the join query by prefixing the primary key with the table name:
+----+-------------------+----------------+
| id | comment_for | comment |
+----+-------------------+----------------+
| 1| article:12345 | This is nice...|
| 2| document:42 | Cool doc! |
+----+-------------------+----------------+
This sample shows two comments, one for an Article with an article.id of 12345 and one for a document with document.id of 42. I created #Entities matching the database tables and the corresponding Repository Interfaces with the query methods I need.
I would like to make use of Spring Data Repositories / Entities to populate the collections of my entities with the corresponding comments, like this (pseudocde) for Entity Article.
#OneToMany(mappedBy = "comment_for", prefix = "article:")
private List<Comment> comment = new ArrayList<>();
I only need it unidirectional. My entities (at the moment Article, Document and Mediafile) should hold a collection of their comments. I don't need comments to hold a reference back to the entity.
Is there a way to do this? The resulting SQL query should be something like
SELECT * FROM .... WHERE comment.comment_for = concat('<entityname>:', <entity>.id);
I looked at #JoinColumn but I can't modify the used value for the join, only the column name. The only solution I have at the moment are manual #Querys on the CommentRepository Interface, which gives me an ArrayList of all comments for a certain Entity / ID combination. But I would like to have the comments automatically joined as part of my Business Entity.
Update : It looks like I am able to split the namespace and id from comment_for into two new columns without interrupting the existing software. The two columns are now comment_for_id and comment_for_entityname

You could also break out comment_for to contain only the id like your entities. Adding an additional column like entity_type would allow you to avoid duplicate id values between different entities.
Also you could use #JoinColumn on the owner side of the relationship between Entity and Comments. It looks like in your case that would be the Comment entity/table, since there are many comments per each entity.
Example:
#Entity
#NamedQueries({ #NamedQuery(name = "Comments.findAll", query = "select o from Comments o") })
#IdClass(CommentsPK.class)
public class Comments implements Serializable {
private static final long serialVersionUID = 4787438687752132432L;
#Id
#Column(name = "COMMENT_TEXT", nullable = false, length = 30)
private String commentText;
#Id
#Column(name = "ENTITY_TYPE", nullable = false, length = 30)
private String entityType;
#ManyToOne
#Id
#JoinColumn(name = "COMMENT_FOR")
private EntityDemo entityDemo;
Note that I set the combination of all three fields as the primary key, I am not sure what criteria is used as the PK in your current set up.
Here is an example of an Entity. The attributes have been made up for the purpose of demonstration.
#Entity
#NamedQueries({ #NamedQuery(name = "EntityDemo.findAll", query = "select o from EntityDemo o") })
#Table(name = "ENTITY_DEMO")
public class EntityDemo implements Serializable {
private static final long serialVersionUID = -8709368847389356776L;
#Column(length = 1)
private String data;
#Id
#Column(nullable = false)
private BigDecimal id;
#OneToMany(mappedBy = "entityDemo", cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private List<Comments> commentsList;

Related

Java JPA Hibernate OneToOne vs HashMap - Which One Should I Use and Why?

I have a question about performance and common practice, if someone could explain this to me.
I have recently started using JPA and hibernate and have come across an Entity that has a foreign key and I need to get some data from it. So for example: CustomerAddress has a City and that city has a lot of detail and also a name.
SQL:
select
CA.Id, CI.Name
from
CustomerAddress as CA
inner join City as CI
on CA.CityID = CI.Id
So now in Java JPA Entity I can have a one-to-many annotation:
#Entity
#Table(name = "CustomerAddress")
public class CustomerAddressEntity {
#Id
#Column(name = "Id", unique = true, nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumn(name = "city", referencedColumnName = "id", insertable = false, updatable = false)
private City city;
}
Where City is also an #Entity with #Id and simple object.
Which in my opinion does not turn out the best because it makes a lot of SQL requests.
And then I have the option having just two findAll() calls at the beginning, where I would collect all the City Entities in a HashMap<String, City> and when needing the name I would just call hashmap.get(key).getName().
EDIT (thanks for the heads up :)):
And when using this HashMap I can use a simpler Entity without the #JoinColumn
#Entity
#Table(name = "CustomerAddress")
public class CustomerAddressEntity {
#Id
#Column(name = "Id", unique = true, nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "CityID")
private Long cityId;
}
In the hashmap case I only get two SQL calls and I think it works much faster. Is there a way to get this behavior also using JPA and hibernate?
If my question and code needs some more refinement please let me know.. I can edit the question with more details and perhaps if necessary provide a working example. Thank you for your thoughts :)
And the same would go for OneToMany, where the hashmap would be: new HashMap<String, List<City>> for example - I mean the whole example should be created a bit differently - I guess it could even be a HashMap<String, HashMap<String,City>> - if one would need quick access to the City by Id or sth... but i digress :) I will edit the question and respond to comments as I will go.. and refine the question if necessary.. I would just like to hear some thoughts and where my thinking is wrong :) and what am I failing to see and missing :)
EDIT: For example a code that would create a lot of SQL requests:
public interface CustomerAddressRepository extends JpaRepository<CustomerAddressEntity, Long> {
#Override
List<CustomerAddressEntity> findAll();
}
This for example creates an SQL Query (I would use findAll() at the beginning to list all - or most of the Entities for the user) and you would get an SQL query for every Entity because it would want to find the Name of the City as well - because the ID of the City Entity does not really help to the user.
Also - I like to have all the Entities in my RAM so I can do a quick search for the user more responsive - So a search does not always do SQL Query + #(found results) Queries.
The HashMap has nothing to do with the fact that Hibernate issues a query when you want to get the name of the City object.Here's why it's happening.
In your CustomerAddressEntity you have a OneToOne with City , and since you have a #JoinColumn there ,it means that CustomerAddressEntity database Table will have the Primary Key of the City table as a foreign key , and since you specified in your class that it should be fetched LAZY,Hibernate will create a Proxy object wrapping the City object,ready to get queried from the database in case you call any getMethod ,like getName() ,(excluding the getId() method since the ID exists prealably in the proxy object,you can check the sql query logs and see that the query selects the foreign key with all the other fields of CustomerAddressEntity ),that's why when you trigger the getName() method Hibernate will fetch that entity from the database.

Hibernate join-table two composite keys that are both foreign keys

I have two different tables, both of which have composite embedded keys. Both composite keys have in composition the same id A_ID.
I want to join table M with table D in a one to many relationship using a join-table.
The following are some pseudo-java code converted from XML ORM mappings. So please excuse any mistakes written here. The mappings in the final code work so the typos here are not to blame.
#Entity()
public class M {
#EmbeddedId()
private EmbeddedMId id;
#OneToMany(name="d", #JoinTable(name="M-D",
joinColumns={
#JoinColumn(name="M_ID", referencedColumnName="M_ID"),
#JoinColumn(name="A_ID", referencedColumnName="A_ID", table="M")
},
inverseJoinColumns={
#InverseJoinColumn(name="D_ID", referencedColumnName="D_ID"),
#InverseJoinColumn(name="A_ID", referencedColumnName="A_ID", table="D", insertable="false", updatable="false")
}
))
private Set<D> dSet;
}
#Embeddable()
public class EmbeddedMId {
#Basic() private String A_ID;
#Basic() private String M_ID;
}
#Embeddable()
public class EmbeddedDId {
#Basic() private String A_ID;
#Basic() private String D_ID;
}
As you can see, the embeddables both use A_ID therefore we tried to make the 2nd A_ID in the join-table be readonly. The application starts and the mappings seem to be okay.
The problem is whenever I want to insert a new D object in the M entity, hibernate throws an SQL error invalid column index because while the prepared statement is correct as seen bellow, hibernate only provides the first 2 parameters instead of all three. (Values provided by hibernate are (VALID_M_ID, VALID_A_ID) instead of providing 3 values)
INSERT INTO M_D("M_ID", "A_ID", "D_ID") VALUES (?, ?, ?)
If I rename the 2nd inverseJoinColumn to have a new column name and make it insertable/updatable, the problem is solved. But this means that the A_ID is duplicated in both A_ID and A_REPEAT_ID column and this is what I want to avoid.
#InverseJoinColumn(name="A_REPEAT_ID", referencedColumnName="A_ID", table="D")
Is there a way to tell Hibernate that my EmbeddedDId needs to be mapped over the D_ID and A_ID (readonly) correctly when doing the insertions?
I hope my explanation is clear enough, but feel free to ask for any clarifications.
Hibernate version is 5.2.17-FINAL
EDIT
The only other entity that is important in this case is pretty simple. But as requested I'll write it here
#Entity()
public class D {
#EmbeddedId()
private EmbeddedDId id;
/* other basic fields here */
}
I don't think insertable = false, updatable = false does what you want here. If you want the target column A_ID on D to be readonly, then you will have to map the column in the target entity D and specify there that the column is insertable = false, updatable = false but not on this association.

JPA #OnetoOne self reference relationship with both columns non null

I have an existing database table For e.g. T_STUDENTS on top of which I have to create a JPA entity. All three columns in the table are NON NULL and the table has a self-reference as mentor_id
id | name | mentor_id
-----|---------|----------
1 | John | 1
-----|---------|----------
2 | Marc | 1
-----|---------|----------
3 | Abby | 2
-----|---------|----------
4 | Jimy | 3
-----|---------|----------
5 | Boni | 4
-----|---------|----------
Each student has a mentor who is also a student. There is a strict OneToOne relationship between the student and the mentor. For id 1, there can't be any mentor, therefore it has the mentor id as it's own id. The ids are generated using a database sequence.
The problem is that while generating the first record with id 1, hibernate is not assigning the same id as mentor id even though I have created necessary relationships. Since columns can't be null and hibernate is not assigning mentor_id, SQLConstraint nonnull exception is thrown.
Following is how I have created the relationship.
#Entity
#Table(name = 'T_STUDENTS')
public class Student implements Serializable {
#Id
#SequenceGenerator(name = 'S_STUDENTS_SEQUENCE', allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 'S_STUDENTS_SEQUENCE')
#Column(name = "id")
private Long studentId;
#Column(name = "name", length = 20)
private String studentName;
#OneToOne(optional = false, cascade = CascadeType.NONE)
#JoinColumn(name = "mentor_id")
private Student mentor;
// getters and setters
}
I have set CascadeType.NONE because else hibernate tries to retrieve 2 id's from sequence and tries to create 2 records which are not desirable.
The problem is how can I insert the very first record. Following is how the insert is being done.
Student student = Student.builder()
.setName('John')
.build();
student = student.toBuilder().setMentor(student).build();
return studentRepository.save(student);
If I change the relationship annotation to #ManyToOne since technically mentor_id is 1 is mapped to 2 students, I get the following exception
.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation
Edit 1: If relationship type changed to #ManyToOne and cascade is removed following error is observed.
org.hibernate.action.internal.UnresolvedEntityInsertActions.logCannotResolveNonNullableTransientDependencies - HHH000437: Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity. The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
Edit 2: Changed the cascade type to cascade = CascadeType.PERSIST and hibernate tries to persist the mentor as a separate record. I verified from logs that it tries to retrieve 2 different sequence ids and creates 2 insert queries, with both mentor_id as null.
NOTE: Finally I found the root cause. I was using Lombok builder in the JPA entity and it does not support the self-reference relationship yet.
I switched to public setters and it worked fine. See the link below for more details
https://github.com/rzwitserloot/lombok/issues/2440#event-3270871969
You can ignore the below solution.
I'm not very proud of the solution, but here is how I achieved it.
1.Removed auto sequence generation from the id.
#Id
#SequenceGenerator(name = 'S_STUDENTS_SEQUENCE', allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 'S_STUDENTS_SEQUENCE')
#Column(name = "id")
private Long studentId
to
#Id
#Column(name = "id")
private Long studentId;
2.Changed the mapping to the simple foreign key field.
#OneToOne(optional = false, cascade = CascadeType.NONE)
#JoinColumn(name = "mentor_id")
private Student mentorId;
to
#Column(name = "mentor_id")
private Long mentorId;
3.Created a method to retrieve the sequence manually and then assigned the value to both 'id' and 'mentorId'
#Override
public Student saveExtended(Student student) {
Object sequence =
em.createNativeQuery(
"SELECT NEXT VALUE FOR S_STUDENTS_SEQUENCE AS VALUE FROM SYSIBM.SYSDUMMY1")
.getSingleResult();
BigInteger sequenceLong = (BigInteger) sequence;
student = student.toBuilder().id(sequenceLong.longValue()).mentorId(sequenceLong.longValue()).build();
em.persist(student);
em.flush();
return student;
}

Is there a way to disable fetching the entire value set for a column in Hibernate?

In our "Process" table there is a "Type" column. This column's valueset is defined in an enum in our code. However there are obsolete rows in this table. Meaning that there are rows where "type" is a value that is not present in the code's enum. The problem is whenever we acces ANY (not the obsolete ones) of the rows in this table we get an error that there is an unkown value for that column. Is there a way to disable this feature in hibernate as we do not want to delete these rows.
#EqualsAndHashCode(callSuper = true, of = {})
#Table(name = ProcessEntity.TABLE_NAME)
public class ProcessEntity extends BaseEntity implements ValidityHolder {
public static final String TABLE_NAME = "PROCESS";
#OneToMany(mappedBy = "consent", cascade = CascadeType.ALL)
private Set<ConsentAnswerEntity> consentAnswers;
#OneToMany(mappedBy = "consent", cascade = CascadeType.ALL)
private List<ProcessConsentEntity> processConsents;
#OneToMany(mappedBy = "consent", cascade = CascadeType.ALL)
private Set<ProcessTypeConsentEntity> processTypeConsents;
#Enumerated(EnumType.STRING)
#Column(name = "TYPE_ID")
private Type TpyeId;
If these "obsolete" records no longer fit into your Hibernate data model, then I recommend just moving them to some archive table. After all, you can't really select them now anyway using Hibernate, so at least at the application level, they serve no purpose.
For a more general way to logically delete a record without physically removing it, look into soft deletion. Using soft deletion, you would add a single boolean column to the table which, if marked, would indicate that the record is logically no longer there.

QueryDSL, Hibernate, JPA — using .fetchJoin() and getting data in first SELECT, so why N+1 queries after?

I'm trying to query for a list of entities (MyOrders) that have mappings to a few simple sub-entities: each MyOrder is associated with exactly one Store, zero or more Transactions, and at most one Tender. The generated SELECT appears correct - it retrieves all the columns from all four joined tables - but afterwards, two more SELECTs are executed for each MyOrder, one for Transactions and one for Tender.
I'm using QueryDSL 4.1.3, Spring Data 1.12, JPA 2.1, and Hibernate 5.2.
In QueryDSL, my query is:
... = new JPAQuery<MyOrder>(entityManager)
.from(qMyOrder)
.where(predicates)
.join(qMyOrder.store).fetchJoin()
.leftJoin(qMyOrder.transactions).fetchJoin()
.leftJoin(qMyOrder.tender).fetchJoin()
.orderBy(qMyOrder.orderId.asc())
.transform(GroupBy
.groupBy(qMyOrder.orderId)
.list(qMyOrder));
which is executed as:
SELECT myorder0_.ord_id AS col_0_0_,
myorder0_.ord_id AS col_1_0_,
store1_.sto_id AS sto_id1_56_1_, -- store's PK
transactions3_.trn_no AS trn_no1_61_2_, -- transaction's PK
tender4_.tender_id AS pos_trn_1_48_3_, -- tender's PK
myorder0_.ord_id AS ord_id1_39_0_,
myorder0_.app_name AS app_name3_39_0_, -- {app_name, ord_num} is unique
myorder0_.ord_num AS ord_num8_39_0_,
myorder0_.sto_id AS sto_id17_39_0_,
store1_.division_num AS div_nu2_56_1_,
store1_.store_num AS store_nu29_56_1_,
transactions3_.trn_cd AS trn_cd18_61_2_,
tx2myOrder2_.app_name AS app_name3_7_0__, -- join table
tx2myOrder2_.ord_no AS ord_no6_7_0__,
tx2myOrder2_.trn_no AS trn_no1_7_0__,
tender4_.app_name AS app_name2_48_3_,
tender4_.ord_num AS ord_num5_48_3_,
tender4_.tender_cd AS tender_cd_7_48_3_,
FROM data.MY_ORDER myorder0_
INNER JOIN data.STORE store1_ ON myorder0_.sto_id=store1_.sto_id
LEFT OUTER JOIN data.TX_to_MY_ORDER tx2myOrder2_
ON myorder0_.app_name=tx2myOrder2_.app_name
AND myorder0_.ord_num=tx2myOrder2_.ord_no
LEFT OUTER JOIN data.TRANSACTION transactions3_ ON tx2myOrder2_.trn_no=transactions3_.trn_no
LEFT OUTER JOIN data.TENDER tender4_
ON myorder0_.app_name=tender4_.app_name
AND myorder0_.ord_num=tender4_.ord_num
ORDER BY myorder0_.ord_id ASC
which is pretty much what I'd expect. (I cut out most of the data columns for brevity, but everything I need is SELECTed.)
When querying an in-memory H2 database (set up with Spring's #DataJpaTest annotation), after this query executes, a second query is made against the Tender table, but not Transaction. When querying a MS SQL database, the initial query is identical, but additional queries happen against both Tender and Transaction. Neither makes additional calls to load Store.
All the sources I've found suggest that the .fetchJoin() should be sufficient (such as Opinionated JPA with Query DSL; scroll up a few lines from the anchor) and indeed if I remove them, the initial query only selects columns from MY_ORDER. So it appears that .fetchJoin() does force generation of a query that fetches all the side tables in one go, but for some reason that extra information isn't being used. What's really weird is that I do see the Transaction data being attached in my H2 quasi-unit test without a second query (if and only if I use .fetchJoin() ) but not when using MS SQL.
I've tried annotating the entity mappings with #Fetch(FetchMode.JOIN), but the secondary queries still fire. I suspect there might be a solution involving extending CrudRepository<>, but I've had no success getting even the initial query correct there.
My primary entity mapping, using Lombok's #Data annotations, other fields trimmed out for brevity. (Store, Transaction, and Tender all have an #Id a handful of simple numeric and string field-column mappings, no #Formulas or #OneToOnes or anything else.)
#Data
#NoArgsConstructor
#Entity
#Immutable
#Table(name = "MY_ORDER", schema = "Data")
public class MyOrder implements Serializable {
#Id
#Column(name = "ORD_ID")
private Integer orderId;
#NonNull
#Column(name = "APP_NAME")
private String appName;
#NonNull
#Column(name = "ORD_NUM")
private String orderNumber;
#ManyToOne
#JoinColumn(name = "STO_ID")
private Store store;
#OneToOne
#JoinColumns({
#JoinColumn(name = "APP_NAME", referencedColumnName = "APP_NAME", insertable = false, updatable = false),
#JoinColumn(name = "ORD_NUM", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)})
#org.hibernate.annotations.ForeignKey(name = "none")
private Tender tender;
#OneToMany
#JoinTable(
name = "TX_to_MY_ORDER", schema = "Data",
joinColumns = { // note X_to_MY_ORDER.ORD_NO vs. ORD_NUM
#JoinColumn(name = "APP_NAM", referencedColumnName = "APP_NAM", insertable = false, updatable = false),
#JoinColumn(name = "ORD_NO", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)},
inverseJoinColumns = {#JoinColumn(name = "TRN_NO", insertable = false, updatable = false)})
#org.hibernate.annotations.ForeignKey(name = "none")
private Set<Transaction> transactions;
/**
* Because APP_NAM and ORD_NUM are not foreign keys to TX_TO_MY_ORDER (and they shouldn't be),
* Hibernate 5.x saves this toString() as the 'owner' key of the transactions collection such that
* it then appears in the transactions collection's own .toString(). Lombok's default generated
* toString() includes this.getTransactions().toString(), which causes an infinite recursive loop.
* #return a string that is unique per order
*/
#Override
public String toString() {
// use appName + orderNumber since, as they are the columns used in the join, they must (?) have
// already been set when attaching the transactions - primary key sometimes isn't set yet.
return this.appName + "\00" + this.orderNumber;
}
}
My question is: why am I getting redundant SELECTs, and how can I not do that?
I'm a little too late on the answer, but today the same problem happened to me. This response might not help you, but at least it would save someone the headache we went through.
The problem is on the relations between the entities, not in the query. I tried with QueryDSL, JPQL, and even native SQL but the problem was always the same.
The solution was to trick JPA into believing that the relations were there via annotating the child classes with #Id on those joined fields.
Basically you'll need to set Tender's id like this and use it from MyOrder like if it was a normal relationship.
public class Tender {
#EmbeddedId
private TenderId id;
}
#Embeddable
public class TenderId {
#Column(name = "APP_NAME")
private String appName;
#Column(name = "ORD_NUM")
private String orderNumber;
}
The same would go for the Transaction entity.

Categories