I am using Spring Boot, Groovy and JPA/Hibernate to migrate an old application. The only restriction is that the database model must not be changed and I found myself with a weird situation in which a OneOnOne relationship:
Please look at the following model setup:
#Entity
#Table(name='table1')
class Table1 {
#Id
Table1Id id
#Column(name='sequence_num')
Integer seqNum
#Column(name='item_source')
String itemSource
#Column(name='source_type')
String sourceType
#OneToOne(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name='key_field_2', insertable=false, updatable=false)
#NotFound(action=NotFoundAction.IGNORE)
// #Fetch(FetchMode.JOIN)
Table2 table2
}
#Embeddable
class Table1Id implements Serializable {
#Column(name='key_field_1')
String key1
#Column(name='key_field_2')
String key2
}
#Entity
#Table(name='table2')
class Table2 {
#Id
#Column(name='key_id')
String keyId
#Column(name='field1')
String field1
#Column(name='field2')
String field2
#Column(name='field3')
String field3
}
My Spock test looks as follows:
def "Try out the JOIN select with Criteria API"() {
given:
CriteriaBuilder cb = entityManager.getCriteriaBuilder()
CriteriaQuery<Object[]> cQuery = cb.createQuery(Object[].class)
Root<Table1> t1 = cQuery.from(Table1.class)
Path<Table2> t2 = t1.get('table2')
Join<Table1, Table2> lanyonLeftJoin = t1.join('table2', JoinType.INNER)
Predicate where = cb.equal(t1.get('itemSource'), 'ABC')
cQuery.multiselect(t1, t2)
cQuery.where(where)
when:
List<Object[]> result = entityManager.createQuery(cQuery).getResultList()
then:
result.each{ aRow ->
println "${aRow[0]}, ${aRow[1]}"
}
}
This configuration successfully generates an INNER JOIN between Table1 and Table2, NOTE that even the constant on the "where" clause is correctly interpreted.
However for some strange reason Table2 gets re-queried for every row returned in the first query.
The output that I see is:
Hibernate:
select
table10_.key_field_1 as key_field_11_3_0_,
table10_.key_field_2 as key_field_22_3_0_,
table21_.key_id as key_id1_5_1_,
table10_.item_source as item_source3_3_0_,
table10_.sequence_num as sequence_num4_3_0_,
table10_.source_type as source_type5_3_0_,
table21_.field2 as field23_5_1_,
table21_.field3 as field34_5_1_,
table21_.field1 as field15_5_1_
from
table1 table10_
inner join
table2 table21_
on table10_.key_field_2=table21_.key_id
where
table10_.item_source=?
Hibernate:
select
table20_.key_id as key_id1_5_0_,
table20_.field2 as field23_5_0_,
table20_.field3 as field34_5_0_,
table20_.field1 as field15_5_0_
from
table2 table20_
where
table20_.key_id=?
Hibernate:
select
table20_.key_id as key_id1_5_0_,
table20_.field2 as field23_5_0_,
table20_.field3 as field34_5_0_,
table20_.field1 as field15_5_0_
from
table2 table20_
where
table20_.key_id=?
// 500+ more of these
As we can see the first query successfully returns all rows from both tables and it is actually the exact query I am looking for. However there is all those unnecessary extra queries being performed.
Is there any reason why JPA would do such thing and is there a way to prevent it??
I have the impression I am missing something very obvious here.
Thanks in advance for your help
Update 1
If I replace
cQuery.multiselect(t1, t2)
for
cQuery.multiselect(t1.get('id').get('key1'), t1.get('id').get('key2'),
t1.get('fieldX'), t1.get('fieldY'), t1.get('fieldZ'),
t2.get('fieldA'), t2.get('fieldB'), t2.get('fieldC') ...)
It generates the exact same inner join query and DOES NOT re-queries Table2 again.
In other words, looks like (at least for this case) I need to explicitly list all the fields from both tables. Not a nice workaround as it can get very ugly very quickly for tables with a lot of fields. I wonder if there is a way to retrieve all the #Column annotated fields/getters without resourcing to a bunch of reflection stuff?
I think I've got it!
#JoinFormula:
Primary Key in Table2 is INT and the field in Table1 that is used as FK is String (I completely missed that! duh!). So, the solution for that was to apply a #JoinFormula instead of a #JoinColumn in the form of:
#OneToOne(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumnsOrFormulas([
#JoinColumnOrFormula(formula=#JoinFormula(value='CAST(key_field_2 AS INT)'))
])
#NotFound(action=NotFoundAction.IGNORE)
Table2 table2
This strangely returns a List<Object[]> each item of the List contains an array of 2 elements: one instance of Table1 and one instance of Table2.
Join Fetch:
Following your suggestion I added "join fetch" to the query, so it looks like:
select t1, t2 from Table1 t1 **join fetch** t1.table2 t2 where t1.itemSource = 'ABC'
This causes Hibernate to correctly return a List<Table1>
Either with the #JoinFormula alone or the #JoinFormula + "join fetch" hibernate stopped generating the n+1 queries.
Debugging Hibernate code I've found that it correctly retrieves and stores in Session both entities the first time it queries the DB with the join query, however the difference between PK and FK data types causes Hibernate to re-query the DB again, once per each row retrieved in the first query.
Related
So there is this similar (almost identical) question: How to select just the foreign key value using Criteria Query? but I need to avoid doing the join.
So I have two tables, one with a FK to the other
(Service)
id
name
account_id
(Account)
id
name
Service class has the FK defined this way:
#Entity
public class Service extends BaseDbEntity implements Serializable {
private static final long serialVersionUID = 1L;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Account account;
...
and what I want to do is query all the Service's that correspond to a given account, having the accountId, without doing a JOIN, because the id is already in the Service table.
Now, my criteria query looks like this:
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Service> criteria = cb.createQuery(Service.class);
Root<Service> root = criteria.from(Service.class);
criteria
.select(root)
.where(cb.equal(root.join("account").get("id"), accountId));
session.createQuery(criteria).getResultStream();
This ends up generating this query:
Hibernate: select service0_.id as id1_3_, service0_.name as name4_3_, service0_.account_id as account_id6_3_ from Service service0_ inner join Account account1_ on service0_.account_id=account1_.id where account1_.id=?
Which doesn't make sense.. it does a join on a field and then just creates a where clause for that field.
If I do:
root.get("account_id")
it just throws an exception saying the field is not available.
What is the correct way to avoid this?
Ok I just found the answer to my question:
Instead of doing
root.join("account").get("id")
I just needed to do:
root.get("account").get("id")
which avoids performing the JOIN.
I need to execute several different queries and I want to use the same POJO to get the results. What I get with those queries are combinations of same columns, can I reuse the same sqlResultSetMapping?
I am working with JPA-2.1 and Java 8
My queries are something like:
select tableA.a, tableB.b, tableA.c
from tableA
inner join tableB
select tableA.a, tableB.b
from tableA
inner join tableB
select tableB.b, tableA.c
from tableA
inner join tableB
My POJO is something like:
public class Result {
String a;
String b;
String c;
}
and finally, my SqlResultSetMapping is:
#SqlResultSetMapping(
name="GeneralResult",
classes = {
#ConstructorResult(
targetClass = Result.class,
columns = {
#ColumnResult(name="a", type=String.class), #ColumnResult(name="b", type=String.class), #ColumnResult(name="c", type=String.class)
}
)
}
)
When I execute the first query, with fields a,b, the call works fine. The problem is when I execute one of the other two queries.
Can I use the same SqlResultSetMapping for those queries?
I tried with several ConstructorResult but the problem is all fields are String (VARCHAR in DB).
Thanks in advance.
I have two tables in my database:
1. Warehouse
2. WarehouseItem
Relation between them are like listed below:
#Entity
#Table(name = "warehouse")
public class WarehouseModel {
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy="warehouse")
private List<WarehouseItemModel> _items;
and
#Entity
#Table(name = "warehouseItem")
public class WarehouseItemModel {
#ManyToOne
public WarehouseModel warehouse;
and now I want to SELECT all the objects of the entity WarehouseModel:
public List getObjects(Class pClass)
{
startTime();
connect();
Session session = getSession();
Transaction lTransaction = session.beginTransaction();
List lRet = session.createCriteria(pClass).list();
lTransaction.commit();
endTime("getObjects: " + lRet.size() + " objects");
Collections.reverse(lRet);
return lRet;
}
In my database I have:
1x object in the table: Warehouse (WarehouseModel.java)
5x objects in the table: WarehouseItem (WarehouseItemModel.java)
When I want to retrive all the Warehouses including related WarehouseItems:
databaseConnector.eDocumentConnector.getObjects(WarehouseModel.class)
the result is:
- 5x the same object of WarehouseModel
It seems that there is dependancy that I always get as much entities of the same WarehouseModel as there is WarehouseItemModels inside field WarehouseModel._items
How to fix it and why it happens? (I have more relations like this one in my project and if it happends here, maybe it happends also in the other places)
Project details:
- Java 1.8
- Hibernate 5.0.7
- database: PostgreSQL 9.5.2
It is solved by using DISTINCT in hibernate query:
session.createCriteria(pClass).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).list();
More importantly, you should understand how a 1-to-many relationship works in SQL.
The raw select statement is going to be something like
SELECT
.
.
<what columns you want>
.
FROM
warehouse w
JOIN warehouseItem wi ON (w.id = wi.warehouse_id)
WHERE
<whatever you need to filter on>
When you join, you're always going to get the number of rows that are related to the primary table's ID. Since there are 5 rows in the child table, 5 rows are returned, where the warehouse columns are duplicated each time but the warehouseItem columns are unique to each row.
I have a classes like this:
class EntryDB {
#Id #GeneratedValue
int id;
#ManyToMany(cascade = CascadeType.ALL)
List<Category> cats;
}
class Category {
#Id #GeneratedValue
int id;
String name;
}
So each entry can be in Zero or more categories. It works fine. But i need to retrive a list of Entries in format entry_id->(cat_id, cat_id, cat_id)
I am trying
select id, cats from EntryDB
but it does not work and i see Exception like:
DEBUG OUTPUT: Hibernate: select entrydb0_.id as col_0_0_, {non-qualified-property-ref} as col_1_0_, category2_.id as id1_, category2_.name as name1_ from my_entry_table entrydb0_ inner join entrydb_category categories1_ on entrydb0_.id=categories1_.id inner join category category2_ on categories1_.id=category2_.id
org.hibernate.exception.SQLGrammarException: could not execute query
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL serve...near 'as col_1_0_, category2_.id as id1_, category2_.name as name1_ from my_entry_table' at line 1
But HQL like
"select cats from EntryDB"
works fine. But i need to know id's of Entries.
Just to use
session.load(EntryDB.class, id).getCats();
is not an option, because the "real" EntryDB is VERY heavy and i just want to know "wich entries are in wich category". It would be very simple, if i could direct access join table, but it's can not be done in HQL.
May be you know some walkaround, but using JDBC to query a join table.
select entry.id, category.id from EntryDB entry
left join entry.cats category
I wrote a hsql:
String queryString = "select t1.a, t1.b, t2.c from table1 t1, table2 t2 where t1.id = t2.id";
and then I have a class:
class test{
String a;
String b;
String c
....//other getter and setter
}
I tried:
List = getHibernateTemplate().find(queryString);
this doesn't work, when I use test object in jsp page, it will throw out exception.
I have to manually create a test object:
List<Object[]> list = getHibernateTemplate().find(queryString);
test.seta(list.get(0)[0]);
is it possible for hibernate to automatically map the class for me in hsql ?
If you have a mapping for both table1 and table2 (see Prashant question above) you can do something like:
String queryString = "select t1 from table1 t1
inner join t1.table2 t2";
After you run the query you should have a list of t1 objects.
for(Table1 t1:listOfTable1Objects) {
t1.getA(); //for example or whatever you want to do with your object.
}
The Problem is that you do not write a HQL query. You just write a normal SQL query. In HQL, because the hibernate make the mapping from table to class, you cannot make a projection. So, if you write something like
String query = "FROM Class1 WHERE ome_condition;
without the SELECT clause, the Hibernate will be able to convert the result in the proper object.
You can see more about this here: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/queryhql.html
If you dont have a mapping, you may create a auxiliary class for this. Say ResultClass. Then you add #NamedNativeQuery and #SqlResultSetMapping annotations to the class:
#NamedNativeQuery(name="queryHehehe", query="select t1.field1 f1, t2.field2 f2 from table1 t1, table2 t2", resultSetMapping="mappingHehehe")
#SqlResultSetMapping(name="mappingHehehe", entities={
#EntityResult(entityClass=my.clazz.AuxiliaryClass.class, fields = {
#FieldResult(name="id", column="f1"),
#FieldResult(name="other_property", column="f2")
}),
})
public class AuxiliaryClass {
public Long id;
public String other_property;
}
I have never used this, but can work. Good luck.
If you need a query to return values from multiple tables and create an object of an unmapped class, then you need to either do what you're doing here, or use a ResultTransformer.
In order to do this with HibernateTemplate, you'll need to change the way you use the template, possibly using execute(HibernateCallback action), as you'll need to convert the sql query to a Criteria as described in Hibernate Reference Native SQL Chapter.
If you do want to try this, you'll probably want to use an AliasToBeanResultTransformer or AliasToBeanConstructorResultTransformer rather than writing your own transformer.