JPA Discriminator using Spring-Data Repositories and JPQL Query - java

I have two entities in MySQL as below. The primary key of nnm_tran is a composite of id and source. The primary key of bargains is actually a foreign key link to the nnm_tran table
I'm trying to use JPA inheritance to represent these.
nnm_tran entity
#Entity
#Table(name = "nnm_tran")
#IdClass(CommonTransactionKey.class)
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "bargain_flag", discriminatorType = DiscriminatorType.CHAR)
#DiscriminatorValue("N")
public class CommonTransaction {
#Id
#Column(name = "id", nullable = false)
private String transactionId;
#Column(name = "plan_number", nullable = false)
private String planNumber;
#Column(name = "tran_date")
private LocalDateTime transactionDatetime;
#Column(name = "bargain_flag")
private String bargainFlag;
...
}
bargains entity
#Data
#EqualsAndHashCode(callSuper = true)
#Entity
#Table(name = "bargains")
#DiscriminatorValue("B")
#PrimaryKeyJoinColumns({ #PrimaryKeyJoinColumn(name = "nnm_tran_id", referencedColumnName = "id"), #PrimaryKeyJoinColumn(name = "nnm_tran_source", referencedColumnName = "source") })
public class Bargain extends CommonTransaction implements Serializable {
#Column(name = "unit_price")
private BigDecimal unitPrice;
#Column(name = "client_price")
private BigDecimal clientPrice;
...
}
I think so far this is all hooked up correctly. My problem comes when I attach a spring-data repository with a custom query.
Repository
public interface CommonTransactionRepository extends CrudRepository<CommonTransaction, CommonTransactionKey> {
#Query("select t from CommonTransaction t left join IoPlan p ON t.planNumber = p.planNumber "
+ "where (p.planNumber is NULL or p.planNumber = '') "
+ "and t.transactionDatetime between ?1 and ?2 "
+ "and t.cancelled = false")
public Iterable<CommonTransaction> findOrphanedTransactionsByTranDate(LocalDateTime fromDate, LocalDateTime toDate);
...
}
When this gets proxied and the method is executed it generates the SQL statement
SELECT DISTINCT nnm_tran.bargain_flag FROM nnm_tran t1 LEFT OUTER JOIN io_plan t0 ON (t1.plan_number = t0.plan_number) WHERE ((((t0.plan_number IS NULL) OR (t0.plan_number = ?)) AND (t1.tran_date BETWEEN ? AND ?)) AND (t1.CANCELLED = ?))
The issue with this is that the nnm_tran table is aliased to t1 but the discriminator column is referencing the full table name nnm_tran.bargain_flag The result is a lovely
UnitOfWork(17171249)--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'nnm_tran.bargain_flag' in 'field list'
Question here is, am I doing something wrong or is this a bug in spring-data and/or eclipselink?
Versions: spring-data 1.7.2, Eclipselink 2.5.2, MySQL 5.6.28

Using #manish's sample app as a starting point I started layering back on the complexity that was missing and quickly stumbled across the thing causing the rogue SQL. It was down to the join I had performed in the JPQL
NOTE: If you've come here from the future then ignore the remainder of this answer and instead use #Chris's comment instead.
Most of the time I don't need to look at or even think about the IoPlan table that can be seen in the #Query
#Query("select t from CommonTransaction t left join IoPlan p ON t.planNumber = p.planNumber "
+ "where (p.planNumber is NULL or p.planNumber = '') "
+ "and t.transactionDatetime between ?1 and ?2 "
+ "and t.cancelled = false")
and so this table is not a part of the CommonTransaction entity as a field. Even the result of this query doesn't really care because it's looking only as a one off for CommonTransaction with no associated join in the IoPlan table.
When I added the join back in to the sample app from #manish it all broke in the same way my app has in EclipseLink, but broke in a different way for Hibernate. Hibernate requires a field for you to join with, which if you ask me defeats the purpose of writing the join in the #Query. In fact in Hibernate you have to define the join purely in JPA so you might as well then use dot notation to access it in the JPQL.
Anyway, going along with this idea I tried adding a dummy field to hold an IoPlan in my CommonTransaction entity and it almost worked. It defaulted some of the join logic but it was closer
SELECT DISTINCT t1.bargain_flag FROM nnm_tran t1 LEFT OUTER JOIN io_plan t0 ON ((t0.ID = t1.IOPLAN_ID) AND (t1.plan_number = t0.plan_number)) WHERE ((((t0.plan_number IS NULL) OR (t0.plan_number = ?)) AND (t1.tran_date BETWEEN ? AND ?)) AND (t1.CANCELLED = ?))
In this case t1.IOPLAN_ID and t0.ID don't exist. So I ended up defining the entire join in my CommonTransaction entity
#OneToOne
#JoinColumn(insertable = false, updatable = false, name = "plan_number", referencedColumnName = "plan_number")
private IoPlan ioPlan;
and voila, it started working. It's not pretty and now I have a redundant join condition
LEFT OUTER JOIN io_plan t1
ON ((t1.plan_number = t0.plan_number) AND (t0.plan_number = t1.plan_number))
but I can fix that. It's still annoying that I have to define a field for it whatsoever, I don't actually want or need it there, not to mention that the result from this query is returning CommonTransaction entities that have no IoPlan so the field will be permanently null.

Related

How to use CriteriaBuilder to join on selection

How to join a table with a selection using CriteriaBuilder?
Suppose I have this query:
SELECT
tbl1.*,
tbl2.total
FROM
table_1 tbl1
LEFT JOIN
(SELECT col_id AS id, SUM(value) AS total FROM table_2 WHERE active = 1 GROUP BY col_id) tbl2
ON
tbl1.id = tbl2.id;
Where the definition of table_1 is:
CREATE TABLE table_1(
id NUMBER(19, 0),
-- other columns
)
... and table_2 ...
CREATE TABLE table_2(
id NUMBER(19, 0),
col_id NUMBER(19, 0),
value NUMBER(14, 2),
-- other columns
FOREING KEY (col_id) REFERENCES table_1(id);
)
This is not possible with plain JPA or Hibernate. With Hibernate you can model this query if the subquery is static, but for dynamic subqueries you will need something like Blaze-Persistence which works on top of JPA/Hibernate and provides support for these things.
For the static query solution you can do this:
#Entity
#Table(name = "table1")
public class Table1 {
#Column(name = "id")
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id", insertable = false, updatable = false)
private Table2 table2;
}
#Entity
#Subselect("SELECT col_id AS id, SUM(value) AS total FROM table_2 WHERE active = 1 GROUP BY col_id")
public class Table2Query {
#Column(name = "id")
private Integer id;
#Column(name = "total")
private BigDecimal total;
}
Here is a nice article by Vlad Mihalcea about Blaze-Persistence if you want a dynamic solution i.e. where the query structure isn't fixed: https://vladmihalcea.com/blaze-persistence-jpa-criteria-queries/
Use the join method in root and use it to get the values from the other table.
Note: you need to add the relation in the entity depending on the relationship of these tables (onetone, manytoone or onetomany). Something like this:
Entity code Table1:
#OneToOne
private Table2 table2;
Search code example:
(Root<Table1> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
Join<Table1, Table2> joinTable2 = root.join("table2");
cb.equal(joinTable2.get("active"), 1);
.. other filters ..
};

JPA select association and use NamedEntityGraph

Our in-house framework built with Java 11, Spring Boot, Hibernate 5 and QueryDSL does a lot of auto-generation of queries. I try to keep everything efficient and load associations only when needed.
When loading full entities, the programmer can declare a NamedEntityGraph to be used. Now there is one case where a query like this is generated:
select user.groups
from User user
where user.id = ?1
Where the Entities in question look like this:
#Entity
#NamedEntityGraph(name = User.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(User.Fields.permissions),
#NamedAttributeNode(value = User.Fields.groups, subgraph = "user-groups-subgraph")
},
subgraphs = #NamedSubgraph(
name = "user-groups-subgraph",
attributeNodes = {
#NamedAttributeNode(Group.Fields.permissions)
}
))
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(name = "USERS_PERMISSIONS", joinColumns = #JoinColumn(name = "uid"))
private Set<Permission> permissions = EnumSet.of(Permission.ROLE_USER);
#ManyToMany(fetch = LAZY)
private Set<Group> groups = new HashSet<>();
}
#Entity
#NamedEntityGraph(name = Group.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(value = Group.Fields.permissions)
})
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(
name = "GROUPS_PERMISSIONS",
joinColumns = #JoinColumn(name = "gid")
)
#NonNull
private Set<Permission> permissions = EnumSet.noneOf(Permission.class);
}
When selecting either User or Group directly, the generated query simply applies the provided NamedEntityGraphs. But for the above query the exception is:
org.hibernate.QueryException:
query specified join fetching, but the owner of the fetched association was not present in the select list
[FromElement{explicit,collection join,fetch join,fetch non-lazy properties,classAlias=user,role=foo.bar.User.permissions,tableName={none},tableAlias=permission3_,origin=null,columns={,className=null}}]
I first tried the User graph, but since we are fetching Groups, I tried the Group graph. Same Exception.
Problem is, there is no easy way to add a FETCH JOIN to the generated query, since I don't know which properties of the association should be joined in anyway. I would have to load the Entitygraph, walk it and any subgraph and generated the right join clauses.
Some more details on Query generation:
// QueryDsl 4.3.x Expressions, where propType=Group.class, entityPath=User, assocProperty=groups
final Path<?> expression = Expressions.path(propType, entityPath, assocProperty);
// user.id = ?1
final BooleanExpression predicate = Expressions.predicate(Ops.EQ, idPath, Expressions.constant(rootId));
// QuerydslJpaPredicateExecutor#createQuery from Spring Data JPA
final JPQLQuery<P> query = createQuery(predicate).select(expression).from(path);
// Add Fetch Graph
((AbstractJPAQuery<?, ?>) query).setHint(GraphSemantic.FETCH.getJpaHintName(), entityManager.getEntityGraph(fetchGraph));
EDIT:
I can reproduce this with a simple JPQL Query. It's very strange, if I try to make a typed query, it will select a List of Sets of Group and untyped just a List of Group.
Maybe there is something conceptually wrong - I'm selecting a Collection and I'm trying to apply a fetch join on it. But JPQL doesn't allow a SELECT from a subquery, so I'm not sure what to change..
// em is EntityManager
List gs = em
.createQuery("SELECT u.groups FROM User u WHERE u.id = ?1")
.setParameter(1, user.getId())
.setHint(GraphSemantic.FETCH.getJpaHintName(), em.getEntityGraph(Group.ENTITY_GRAPH))
.getResultList();
Same Exception:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
So the problem can be distilled down to a resolution problem of the Entit Graphs attributes:
select user.groups
from User user
where user.id = ?1
With the Entity Graph
EntityGraph<Group> eg = em.createEntityGraph(Group.class);
eg.addAttributeNodes(Group.Fields.permissions);
Gives an Exception that shows that Hibernate tries to fetch User.permissions instead of Group.permissions. This is the bug report.
And there is another bug regarding the use of #ElementCollection here.

Hibernate complex 1+N problem fetches multiple instances of same object to the result set

I am running a real life scenario in our database and thus I have no room to alter its structure in case that this comes around as a suggestion. Here is the Structure in question:
EntityA {
#Id
.......
#OneToMany(mappedBy = "xxxx", fetch = FetchType.LAZY)
private Set<EntityB> entityB;
#Column(name = "partition", columnDefinition = "nchar", length = 3)
private String partitionKey;
}
EntityB {
#Id
..........
#Column(name = "partition")
private String partitionKey;
#ManyToOne(fetch = FetchType.LAZY) //EAGER is the default for the to-one
#JoinColumn(name = "bbbb")
private EntityA entityA;
#OneToMany(mappedBy = "EntityCPk.entityB", fetch = FetchType.LAZY)
private Set<EntityC> entityC;
#OneToOne(mappedBy = "cccc", cascade = CascadeType.ALL)
#PrimaryKeyJoinColumn
private EntityD entityD;
}
EntityC {
#EmbeddedId
private EntityCPk entityCPk;
#Embeddable
public static class EntityCPk implements Serializable {
#Column( name = "partition")
private String partitionKey;
#ManyToOne
#JoinColumn(name = "xxxx")
private EntityB entityB;
#ManyToOne
#JoinColumn(name = "xxxx")
private EntityE entityE;
}
}
EntityD {
#id
.........
#MapsId
#OneToOne
#JoinColumn(name = "zzzz", columnDefinition = "nchar")
#PrimaryKeyJoinColumn
private EntityB entityB;
}
EntityE {
#id
.........
#OneToMany(mappedBy = "yyyPk.entityE")
private Set<EntityC> entityC;
}
Now the requirement is to run a query in one go with joins and avoid 1+N scenarios. I assume as far as I have seen that the .LAZY or EAGER annotations are "overwritten" when using the Query annotation within the repository along with the FETCH option. So here is what I have achieved so much (entityXXX and entityYYY do not interfear with our case so I just mention them):
First Attempt with FetchType.LAZY in EntityB.entityC property:
"select a" +
"from EntityA a " +
"join FETCH a.entityB bs" +
"join FETCH bs.entityXXX as xxx " +
"join FETCH bs.entityYYY as yyy " +
"join FETCH bs.entityD latest " +
"where a.aProp in ( :props ) " +
"and xxx.id = 4 " +
"and yyy.id = 10" +
"and latest.entityB.aProp between :date and :date "
Results, as expected. I get 1 query BUT I get no collection returned in EntityB.entityC due to the lazy annotation and of course it is not present in the query. If I change the the EntityB.entityC to FetchType.EAGER then I get as expected 3 queries. One is the main and N per entityC in Set. So I guess the next step is to join entityC:
Second Attempt:
"select a " +
"from EntityA a " +
"join FETCH a.entityB bs" +
"join FETCH bs.entityXXX as xxx " +
"join FETCH bs.entityYYY as yyy " +
"join FETCH bs.entityD as latest " +
"join bs.entityC as cs " + //please note I am not using FETCH yet
"where a.aProp in ( :props ) " +
"and c.entityCPk.partitionKey = a.partitionKey " +
"and xxx.id = 4 " +
"and yyy.id = 10" +
"and latest.entityB.aProp between :date and :date "
The result is unexpected and I think it has been reported here as well. What I get now is multiples of a(s) all references to the same object equals to the sum of the amount of bs.entityC. So if for example a-1 -> has 1-bs -> has 17 cs and a2 -> has 1-bs -> has 67 cs then I end up with a result set of 84 a objects all the same! This is question one. Why is this happening?
Question 2 is that if I use the FETCH in my new join then I am still not getting my 1 query and now I am not getting exactly multiple instances of A but multiple instances of some kind of Wrappers with a handler property that has references to A makred as EntityA_$$_jvstbc0_4#.
Just to give some insight to the database structure, I am more than sure that this schema started as a many-to-many relationship with a lookup table being EntityB between EntityA and EntityC. I may try to tackle the issue using JoinTable on EntityC joining on partitionKey and id of EntityB while EntityA has the partitionkey and its Id to map on EntityB. However i am not very hopeful of this solution as EntityB has been contaminated with other columns over time which needs to be selected uppon and I am not sure how can I do this.
UPDATE 1: I can see that when join FETCH is used for cs it is augmenting the resultant SQL select with the columns that are necessary i.e. to populate the cs children. Running the query manually I am getting correctly the sum of children as rows. Which makes sense SQL wise but hibernate should have been able to aggregate the additional rows based on their properties. Right enough without the join FETCH I am getting only rows equals to the amount of a. So my second though is that somehow I need to instruct Hibernate to aggregate manually(?)
UPDATE 2: Change of strategy. Instead of starting following an SQL logic, we better have to answer to the following question: Which Class/Entity will give us the granularity we are looking for. In the previous examples we were starting from EntityA trying to limit its children to fit our expected results. However as it has been pointed, this is effectively a corruption of the objects. You cannot "limit" the children cause they all belong to the Entity and fetching a subset of them you run the risk of deleting(?) data. So the approach must be to get the children objects we are interested that point to the parent entities. That we don't alter the data. So here is a query that returns the correct amount of object. No distinct or inexplicable multiplicities:
"select c " +
"from EntityC c " +
"inner join c.EntityCPk.EntityB.EntityD latest " +
"join latest.EntityB.EntityXXX xxx " +
"join latest.EntityB.EntityYYY yyy " +
"join fetch c.EntityCPk.EntityB " +
"where latest.EntityB.EntityA.Id in ( :param ) " +
"and latest.EntityB.aField between :paramA and paramB "
So this seems to answer the issue of the multiplicity of the previous examples as every row is based on the "finer" child object that resolves its parent via the -ToOne relationship. Also there are no more dangerous aliases in the join fetch. There is only one more issue. It introduces a 1+N query problem for EntityB that I cannot get rid off.

Retrieveing list from a many-to-many relation with JPA/HQL

I have 3 classes, I am trying to get a list of all the events of an eventhost that a user is subscribed to. I am probably thinking way too complicated but I have very little experience with JPA/HQL.
User class
#ManyToMany
#JoinTable(name = "Subscriptions", joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id") , inverseJoinColumns = #JoinColumn(name = "event_host_id", referencedColumnName = "id") )
private List<EventHost> subscriptions;
EventHost class
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "event_host_id", referencedColumnName = "id", updatable = true)
private List<Event> events;
I tried using this query, but it tells me that subscriptions is not mapped, which it is not since it's not a java class.
String hql = "SELECT o FROM Event WHERE event_host_id IN (SELECT a FROM EventHost WHERE id IN(SELECT b FROM User WHERE = + " + userid + "))";
I know injecting the userid like this is bad practice, I'm just doing it for testing purposes.
Please ask if you need something more, I would really like to understand how to write a query for this.
This question should really be HQL with two join tables, but I'll let you change it. Since its HQL, or JPA, it's database independent.
Anyway, any time you see a OneToMany or ManyToMany relationship you have a join table and so you should be thinking joins. It's always a good idea to look at the sql create table statements to see what's going on. In this case your user_subscriptions join table is:
create table user_subscriptions (user_id integer not null, subscriptions_id integer not null)
and your event_host_events join table is this:
create table event_host_events (event_host_id integer not null, events_id integer not null)
Nothing new there. When you're trying to get something new working that you don't intuitively understand, break it down into things you can do. For example, you can execute two queries, getting a Users subscriptions first, and then getting the Events for those subscriptions:
Query query = session.createQuery("select u.subscriptions from User u where name = :name");
query.setParameter("name", name);
List<EventHost> subscriptions = query.list();
List<Event> events = new ArrayList<Event>();
Query query2 = session.createQuery("select s.events from EventHost s where id = :id");
for (EventHost s: subscriptions ) {
query2.setParameter("id", s.getId());
events.addAll( query2.list());
}
Not elegant, but it works. Then, keeping join in mind, figure out how to make one statement out of the two of them.
Query query = session.createQuery("select s.events from User u join u.subscriptions s where u.name = :name)");
query.setParameter("name", name);
return query.list();
The join will use an inner join by default, so you're ok there. The JPA provider will auto-magically join your three Entity tables and two Join Tables for you:
select
event4_.id as id1_2_
from user user0_
inner join user_subscriptions subscripti1_ on user0_.id=subscripti1_.user_id
inner join event_host eventhost2_ on subscripti1_.subscriptions_id=eventhost2_.id
inner join event_host_events events3_ on eventhost2_.id=events3_.event_host_id
inner join event event4_ on events3_.events_id=event4_.id
where user0_.name=?
Aren't you glad you don't have to write that query?

Hibernate left join constraint in a many-to-many relationship

I have built a list of taggable documents, with a many-to-many relationship between the tags and the documents. I would now like to use the hibernate criteria mechanism to query a "summary" of each tag, which includes a count of how often a particular tag has been used, with an additional restriction on whether or not the document has been published.
The entities I'm using roughly look like this (You'll note an SQL join table in the middle there):
#Entity
public class DocumentTag {
... various things ...
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "tags")
private List<Document> documents = new ArrayList<>();
}
#Entity
public class Document {
... various things ...
#Basic
#Column(name = "published", columnDefinition = "BIT", length = 1)
protected boolean published = false;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "document_tag_joins",
uniqueConstraints = #UniqueConstraint(
columnNames = {"document", "tag"}
),
joinColumns = {#JoinColumn(name = "document")},
inverseJoinColumns = {#JoinColumn(name = "tag")})
private List<DocumentTag> tags = new ArrayList<>();
}
Given the above, I've managed to figure out that building the query should work more or less as follows:
Criteria c = session.createCriteria(DocumentTag.class);
c.createAlias("documents", "docs",
JoinType.LEFT_OUTER_JOIN,
Restrictions.eq("published", true)
);
c.setProjection(
Projections.projectionList()
.add(Projections.alias(Projections.groupProperty("id"), "id"))
.add(Projections.alias(Projections.property("createdDate"), "createdDate"))
.add(Projections.alias(Projections.property("modifiedDate"), "modifiedDate"))
.add(Projections.alias(Projections.property("name"), "name"))
.add(Projections.countDistinct("docs.id"), "documentCount"));
// Custom response entity mapping
c.setResultTransformer(
Transformers.aliasToBean(DocumentTagSummary.class)
);
List<DocumentTagSummary> results = c.list();
Given the above, the hibernate generated SQL query looks as follows:
SELECT
this_.id AS y0_,
this_.createdDate AS y1_,
this_.modifiedDate AS y2_,
this_.name AS y3_,
count(DISTINCT doc1_.id) AS y5_
FROM tags this_
LEFT OUTER JOIN tag_joins documents3_
ON this_.id = documents3_.tag AND (doc1_.published = ?)
LEFT OUTER JOIN documents doc1_
ON documents3_.document = doc1_.id AND (doc1_.published = ?)
GROUP BY this_.id
As you can see above, the publishing constraint is applied to both of the left outer joins. I'm not certain whether that is by design, however what I need is for the published constraint to be applied ONLY to the second left outer join.
Any ideas?
I was able to circumvent this problem by coming at it sideways. First, I had to change the "published" column to use an integer rather than a bit. Then I was able to slightly modify the projection of the result as follows:
// Start building the projections
ProjectionList projections =
Projections.projectionList()
.add(Projections.alias(
Projections.groupProperty("id"), "id"))
.add(Projections.alias(
Projections.property("createdDate"),
"createdDate"))
.add(Projections.alias(
Projections.property("modifiedDate"),
"modifiedDate"))
.add(Projections.alias(
Projections.property("name"), "name"));
if (isAdmin()) {
// Give the raw count.
projections.add(Projections.countDistinct("docs.id"), "documentCount");
} else {
// Use the sum of the "published" field.
projections.add(Projections.sum("docs.published"), "documentCount");
}
I acknowledge that this doesn't actually answer the question about why hibernate criteria constraints on many-to-many tables get applied to all tables, but it solved my problem.

Categories