JpaRepository: using repositories in #PostPersist - spring

I have two entities
class A {
...
Integer totalSum;
Set<B> b;
}
class B {
...
Integer value;
A container;
}
I want a.totalSum to be sum of each a.b.value;
Maybe best solution is view in db, but I want listen changes in BRepository and update A-records.
I do:
#PostPersist
#PostUpdate
#PostRemove
private void recalculateSums(B b) {
AutowireHelper.autowire(this, this.aRepository);
AutowireHelper.autowire(this, this.bRepository);
A a = b.getContainer();
a.setTotalSum(bRepository.sumByA(s));
aRepository.save(s);
}
And in BRepository:
#Query("select sum(b.value) from B b where b.container = :a")
Long sumByA(#Param("a") A a);
And I have error: org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.B(ID)"; SQL statement:
insert into b (VALUE, CONTAINER_ID, ID) values (?, ?, ?)
What I'm doing wrong?
If I do
a.setTotalSum(a.getTotalSum()+1);
aRepository.save(s);
All works, but if I do
a.setTotalSum(a.getTotalSum()+1);
aRepository.saveAndFlush(s);
I have the same error.

I can see why you might want to do this but I think it creates a whole load of potential issues around data integrity.
If you want to have the sum of B available for an A without having to load and iterate all B there are three other options which you could implement all of which would be more robust than your proposal.
As you noted, create a view. You can then Map an Entity say, SummaryData, to this view and map a one-to-one from A to SummaryData so that you can do a.getSummaryData().getTotalSum();
Alternatively, you could use the Hibernate specific #Formula annotation which will issue an inline select when the entity is loaded.
#Formula("(select sum(value) from B b inner join A a where b.a_id= a.id and a.id =id
private int totalSum;
Finally, and depending on the capabilities of your database, you could create a Virtual Column and Map a property as you would for any other field.
1 and 3 obviously require schema changes but your app would remain JPA compliant. 2 does not require any schema change but breaks strict JPA compliance if that was important.

Related

Criteria API with ODER BY CASE expression throws SQLException: ORA-12704 "character set mismatch"

I am using criteria API to create my query. Because of special sorting algorithm I use an "order by case" expression. My Unit-Tests using in memory H2 DB and are working. In the development stage we are using Oracle DB and there I get an "SQLException: ORA-12704" when executing the query.
Assume my root entity 'Foo' has a Set of 'Bar's. Bar has an attribute 'myOrderByColumn'
public class Bar {
...
#NotBlank
#javax.validation.constraints.Size(max = 255)
#Column(name = "MYORDERBYCOL")
private java.lang.String myOrderByColumn;
...
}
Here is the code which produces the exception. It creates the Order object later used in CriteriaQuery.orderBy(..)
private Order buildOrderBy(final CriteriaBuilder cb,
final Root<Foo> rootEntity,
final List<String> somehowSpecialOrderedList) {
final Expression<String> orderByColumn =
rootEntity.join(Foo_.bars, JoinType.LEFT).get(Bars.myOrderByColumn);
CriteriaBuilder.SimpleCase<String, Integer> caseRoot = cb.selectCase(orderByColumn);
IntStream.range(0, somehowSpecialOrderedList.size())
.forEach(i -> caseRoot.when(somehowSpecialOrderedList.get(i), i));
final Expression<Integer> selectCase = caseRoot.otherwise(Integer.MAX_VALUE);
return cb.asc(selectCase);
}
I took a look into the Oracle DB. The type of the column 'myOrderByColumn' ist NVARCHAR2(255).
I guess the problem here ist that the "when" part in the SQL query must match with the type of the 'MYORDERBYCOL' database column, which is NVARCHAR2. In Java I use Strings. Probably Hibernate is not casting this correctly!?
I can produce the database ORA-12704 error by
SELECT FOO.id
FROM FOO
LEFT OUTER JOIN BAR ON FOO.id = BAR.fk_id
ORDER BY
CASE FOO.myorderbycol
WHEN '20' THEN 1
ELSE 2
END ASC;
This SQL works
SELECT FOO.id
FROM FOO
LEFT OUTER JOIN BAR ON FOO.id = BAR.fk_id
ORDER BY
CASE FOO.myorderbycol
WHEN cast('20' as NVARCHAR2(255))
THEN 1ELSE 2
END ASC;
How do I have to adjust my oder by case expression with criteria API so that the query is working with any database? (must later work with at least H2, Oracle, MS SQL, PostgreSQL)
Looks like an Oracle issue to me. Which version are you using? You can try a different approach which might work.
CriteriaBuilder.SearchedCase<Integer> caseRoot = cb.selectCase();
IntStream.range(0, somehowSpecialOrderedList.size())
.forEach(i -> caseRoot.when(cb.equal(orderByColumn, somehowSpecialOrderedList.get(i)), i));
final Expression<Integer> selectCase = caseRoot.otherwise(Integer.MAX_VALUE);

DTO Projection of Query with #ElementCollection causes 'Unable to locate appropriate constructor on class' error

I'm writing custom query using projection to reduce amount of queries in one session, when couple of field from antity are needed and using Fetch join.
Unfortunately got stuck into a problem when one types in returned dto is a collection.
I have following class with #ElementCollection (siplified version for this purpose):
#Entity
class MyClass(
val someString: String,
#ElementCollection
#Enumerated(EnumType.STRING)
var enums: MutableSet<Enum>,
#ManyToOne
var condition: Condition
//And so on...
)
And DTO used for projection:
data class ProjectionDTO(
val aString: String,
val enumList: List<Enum>
)
But when using query:
fun query(condition: Condition): List<ProjectionDTO> =
entityManager.createQuery(
"""SELECT NEW com.xxx.ProjectionDTO( m.someString, e ) FROM MyClass m
INNER JOIN FETCH m.enums e
WHERE m.condition = :condition""", ProjectionDTO::class.java)
.setParameter("condition", condition)
.resultList
}
I get following exception:
Exception:[org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate appropriate constructor on class [com.xxx.ProjectionDTO]. Expected arguments are: com.xxx.String, com.xxx.Enum [SELECT NEW com.xxx.ProjectionDTO( m.someString, e ) FROM MyClass m
INNER JOIN FETCH m.enums e
WHERE m.condition = :condition]]
Already tried different types of collection, additional constructors and calling field of given #ElementCollection like e.enum in query params.
Is it possible to return a list (or other collection) from this kind of query? If so, how do I tackle it?
It is not allowed to use collection path expressions in constructor query. link
Put just root entity into constructor:
SELECT NEW com.xxx.ProjectionDTO(m) WHERE m.condition = :condition
In constructor assign m.enums, m.someString to fields.

QueryDSL Paging Sorting duplicates

I have got an Entity A say Car with a OneToMany relationship to an Entity B say CarProperty.
My Car Repository extends QueryDslPredicateExecutor to support paging and sorting:
Page<T> findAll(Predicate predicate, Pageable pageable);
I'm trying to execute a query where I sort the results, by a column of CarProperty, defining the sort on pageable variable.
However since it is a One To Many relationship between Car and CarProperty, I have returned duplicates of Car.
Is it possible to obtain distinct results on Car using this structure?
If it is not possible to use distinct on the Predicate, how could I represent the following query in the predicate (using exists to eliminate duplicates):
SELECT Car.*
FROM Car C LEFT JOIN CarProperty CP ON (C.ID = CP.CAR_ID)
WHERE EXISTS (SELECT 1
FROM CarProperty CP2
WHERE CP2.CAR_ID = C.ID AND CP2.ID = CP.ID)
ORDER BY CP.PROPERTY_NAME ASC;
Thanks in advance
You can use named entity graph to avoid duplicate records, in my case it worked.
#NamedEntityGraph(name = "Car.carProperty" ,attributeNodes = #NamedAttributeNode("carProperties"))
#Entity
public class Car {
#OneToMany
#JoinColumn(name = "carProperties")
private List<CarProperty> carProperties;
}
and then override findall method
#EntityGraph(value = "Car.carProperty" , type = EntityGraphType.LOAD)
Page<Car> findAll(Predicate predicate, Pageable pageable);

Filter with Spring-Data and QueryDSL on nullable reference attribute

I have following issue. I am using Jquery Datatable serverside and I am now implementing the search box. But I have an issue there in special case, when a dataset has an attribute, what is "null". So the dataset will not be found although it should found cos it matches on one attibute.
The situtation in beginning is like follows. You see there is a dataset with apprentice Fabio Bartels, who has not Fachrichtung. And a dataset with Viktoria.
Now when I search for Viktoria, the filter works as expected:
When I search for Fabio, then Dataset is not found:
=====
The problem I have is, that I don't know how to handle the filter, that a attribute will only be validated against the search string when the attribute is not null.
=====
Serverside Java Classes see like follows:
QueryClass:
class ContractSearchQuery {
private static QContract contract = QContract.contract;
static BooleanExpression getPredicate(final ContractSearch filter) {
BooleanExpression predicate;
if (filter == null || filter.isEmpty()) {
// SHOW ALL PREDICATE ...
} else {
final String search = filter.getSearch();
final List<BooleanExpression> expressions = new ArrayList<BooleanExpression>();
// EXPRESSIONS CURRENTLY ONLY ON AUSZUBILDENDER AND FACHRICHTUNG
// FOR SHOWCASE
expressions.add(containsApprenticeName(search)); // AUSZUBILDENDER
expressions.add(containsSpecialisation(search)); // FACHRICHTUNG
BooleanExpression expression = expressions.get(INTEGER_ZERO);
for (int i = 1; i < expressions.size(); i++) {
expression = expression.or(expressions.get(i));
}
predicate = expression;
}
return predicate;
}
private static BooleanExpression containsApprenticeName(final String search) {
final BooleanExpression expLastName = contract.apprentice.lastName.containsIgnoreCase(search);
final BooleanExpression expFirstName = contract.apprentice.firstName.containsIgnoreCase(search);
return expLastName.or(expFirstName);
}
private static BooleanExpression containsSpecialisation(final String search) {
return contract.companyOccupationCombination.occupationCombination.specialisation.name.containsIgnoreCase(search);
}
}
Spring-Data-Repository Call:
final PageRequest pageRequest = new PageRequest(firstResult / maxResults, maxResults, orderSort);
final Page<Contract> page = contractRepository.findAll(predicate, pageRequest);
return page.getContent();
=======
Database:
By the way I recognized when I do direct request against my db with joining Specialisation Table, then I only get Fabio as record, when not joining Specialisation, I get all three persons. Maybe somethings to do with my issue:
select a.first_name, a.last_name from contract c
join company_occupation_combination coc on c.company_occupation_combination = coc.id
join occupation_combination oc on coc.occupation_combination = oc.id
join apprentice a on c.apprentice = a.id
Result:
"Fabio";"Bartels"
"Viktoria";"Kruczek"
"Lina";"Ehleiter"
With Join:
select a.first_name, a.last_name from contract c
join company_occupation_combination coc on c.company_occupation_combination = coc.id
join occupation_combination oc on coc.occupation_combination = oc.id
join specialisation s on oc.specialisation = s.id
join apprentice a on c.apprentice = a.id
Result: "Viktoria";"Kruczek"
====
EDIT:
Okay, on db site I found out (with Hibernate and JPA I start forgetting SQL-Basices ;-)), that I need a left join for the nullable relation, so my query should result to an sql like:
select a.first_name, a.last_name from contract c
join company_occupation_combination coc on c.company_occupation_combination = coc.id
join occupation_combination oc on coc.occupation_combination = oc.id
left join specialisation s on oc.specialisation = s.id
join apprentice a on c.apprentice = a.id
====
So my question is, how can I manage left Join when I have a Query-Class using QueryDSL and Spring-Data-Repository like mentioned above?
If you really need left join, you can't achieve that via predicate (instead it is possible via sub-query)
To be able to do left-join, you will need JPAQuery.
Assuming you have already configured repositories, and able to use EntitiManager, implement ContractRepositoryCustom , so that in your implementation you can have
#PersistenceContext(unitName = "unitname")
protected EntityManager entityManager;
public List<Contract> findAllContracts() {
return new JPAQuery(entityManager, HQLTemplates.DEFAULT)
.from(QContract.contract)
.join(QContract.contract.companyOccupationCombination, QCompanyOccupationCombination.companyOccupationCombination)
.join(QCompanyOccupationCombination.companyOccupationCombination.occupationCombination, QOccupationCombination.occupationCombination)
.leftJoin(QOccupationCombination.occupationCombination.specialization, QSpecialization.specialization)
.join(QSpecialization.specialization.apprentice, QApprentice.apprentice)
.list(QContract.contract);
}
And for pagination you always apply limit(maxResults) and offset(firstResult)
I really like working with Spring-Data and Query-DSL, cos it makes my code really tidy. But I am really suprised, that for the case of nullable references there seems no solution. Sure you can use another solution like #vtorosyan mentioned and thank you again for that solution, but when you project is builded up with combination of QueryDSL and Spring-Data, you really don't want to bring a second style in your application.
But I needed a solution, so I did now the follows.
The point of the issue was, that when I used data from a nullable entity, a join has been executed what hided the datasets, who had a null reference on it, see examples above. What I now did and I hope I will not get another issue then with that solution on later time of that project. I did the null references to not null and defined something like null-record.
Example I added a record for specialisation like
ID NAME
0 Keine
Instead of null I now use that record what has until now following effects:
First my table shows now "Keine" (engl. "None") for all attributes what are not set. It looks more consistent when having a textoutput then empty string.
Now I can explicitly search for "Keine", when I am interested for data records what have no specialisation set.
And my searchbox works as expected for records, which have no speciafication set. (THAT WAS MY ISSUE FROM BEGINNING WHAT I WANTED TO SOLVE):
Additional to that searchbox I use a modal dialog for filtering. Now I can explicitly filter "Keine" for "nullable" records:
If you think there is another good solution for that issue without rebuild code using Spring-Data and QueryDSL konsequently, don't hesitate to post ;-)

Tests with DBunit and Oracle 10g autogenerated primary key identifiers not synchronized (JPA/Hibernate)

I'm testing a JPA/Hibernate application with DBunit and Oracle 10g. When I start my test I load to the database 25 rows with an identifier.
That's the xml where I have my data, that I insert with DBUnit
<entity entityId="1" ....
<entity entityId="2" ....
<entity entityId="3" ....
<entity entityId="4" ....
That's my entity class with JPA annotations (not hibernate specific)
#Entity
#Table(name = "entity")
public class Entity{
#Id
#GeneratedValue(strategy=GenerationType.Auto)
private Integer entityId;
...}
Those are the parameter values of the database connection with Oracle10g
jdbc.driverClassName=oracle.jdbc.OracleDriver
jdbc.url=jdbc:oracle:thin:#192.168.208.131:1521:database
jdbc.username=hr
jdbc.password=root
hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
dbunit.dataTypeFactoryName=org.dbunit.ext.oracle.Oracle10DataTypeFactory
After insert this data in Oracle I run a test where I make Entity entity = new Entity() (I don't have to set manually the identifier because it's autogenerated)
#Test
public void testInsert(){
Entity entity = new Entity();
//other stuff
entityTransaction.begin();
database.insertEntity(entity);//DAO call
entityTransaction.commit();
}
and when the test makes the commit of the transaction I get the following error
javax.persistence.RollbackException: Error while commiting the transaction
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:71)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
...
Caused by: org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:94)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:275)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:266)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:167)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1027)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:365)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:54)
... 26 more
Caused by: java.sql.BatchUpdateException: ORA-00001: restricción única (HR.SYS_C0058306) violada
at oracle.jdbc.driver.DatabaseError.throwBatchUpdateException(DatabaseError.java:345)
at oracle.jdbc.driver.OraclePreparedStatement.executeBatch(OraclePreparedStatement.java:10844)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
... 34 more
I have debugged it and the problem is that the entityId of the new object is 1, and already exists a entity with that Id. So, I don't know who is the responsable DBunit ? Oracle ? Why are not synchronized the identifiers of Oracle database and the identifier that JPA/hibernate gives to my entity in my testing code?
Thanks for your time
I think the AUTO generation type, in Oracle, is in fact a sequence generator. If you don't specify which sequence it must use, Hibernate is probably creating one for you and using it, and its default start value is 1.
Using AUTO is useful for quick prototyping. For a real application, use a concrete generation type (SEQUENCE, for Oracle), and create your sequences yourself with an appropriate start value to avoid duplicate keys.
You could use ids < 0 in your test data sets. Not only will your sequences never come in conflict with test records, but also you'll easily distinguish records that were inserted by the tests.
The AUTO sequencing strategy usually defaults to the TABLE sequencing strategy, but in the case of Oracle, the sequencing strategy uses an Oracle sequence named hibernate_sequence (which is the default, unless you specify a sequence name in the strategy). The starting value of the sequence happens to be 1, which conflicts with the existing entity that is loaded using DbUnit, hence resulting in the ConstraintViolationException exception being thrown.
For the purpose of unit tests, you could perform either of the following:
Issue a ALTER SEQUENCE... command to set the next value of the sequence, after loading data into the database. This will ensure that the JPA provider will use a sequence value that does not conflict with the existing Ids of the entities populated from your current XML file, by DbUnit.
Specify the name of the sequence in XML file loaded as the IDataSet eventually used by DbUnit. The actual sequence values will have to be replaced in the IDataSet using a SELECT <sequence_name>.nextval FROM DUAL. The following section is reproduced as is and is credited to this site:
I spend a couple of hours reading the dbUnit docs/facs/wikis and
source code trying to figure out how to use Oracle sequences, but
unless I overlooked something, I think this is not possible with the
current implementation.
So I took some extra time to find a workaround to insert Oracle
sequence generated IDs into dbUnit's datasets, muchlike what
ReplacementDataSet does. I subclassed DatabaseTestCase already earlier
in a abstract class (AbstractDatabaseTestCase) to be able to use a
common connection in case of insertion of my testcases in a testsuite.
But I added the following code just now. It looks up the first row of
each table in the dataset to determine which columns need sequence
replacement. The replacement is done on the "${…}" expression value.
This code is "quick and dirty" and surely needs some cleanup and
tuning.
Anyways, this is just a first try. I'll post further improvements as I
go, if this can be of any help to anyone.
Stephane Vandenbussche
private void replaceSequence(IDataSet ds) throws Exception {
ITableIterator iter = ds.iterator();
// iterate all tables
while (iter.next()) {
ITable table = iter.getTable();
Column[] cols = table.getTableMetaData().getColumns();
ArrayList al = new ArrayList(cols.length);
// filter columns containing expression "${...}"
for (int i = 0; i < cols.length; i++) {
Object o = table.getValue(0, cols[i].getColumnName());
if (o != null) {
String val = o.toString();
if ((val.indexOf("${") == 0) && (val.indexOf("}") == val.length() - 1)) {
// associate column name and sequence name
al.add(new String[]{cols[i].getColumnName(), val.substring(2, val.length()-1)});
}
}
}
cols = null;
int maxi = table.getRowCount();
int maxj = al.size();
if ((maxi > 0) && (maxj > 0)) {
// replace each value "${xxxxx}" by the next sequence value
// for each row
for (int i = 0; i < maxi; i++) {
// for each selected column
for (int j = 0; j < maxj; j++) {
String[] field = (String[])al.get(j);
Integer nextVal = getSequenceNextVal(field[1]);
((DefaultTable) table).setValue(i, field[0], nextVal);
}
}
}
}
}
private Integer getSequenceNextVal(String sequenceName) throws SQLException, Exception {
Statement st = this.getConnection().getConnection().createStatement();
ResultSet rs = st.executeQuery("SELECT " + sequenceName + ".nextval FROM dual");
rs.next();
st = null;
return new Integer(rs.getInt(1));
}
My AbstractDatabaseTestCase class has a boolean flag
"useOracleSequence" which tells the getDataSet callback method to call
replaceSequence.
I can now write my xml dataset as follows :
<dataset>
<MYTABLE FOO="Hello" ID="${MYTABLE_SEQ}"/>
<MYTABLE FOO="World" ID="${MYTABLE_SEQ}"/>
<OTHERTABLE BAR="Hello" ID="${OTHERTABLE_SEQ}"/>
<OTHERTABLE BAR="World" ID="${OTHERTABLE_SEQ}"/>
</dataset>
where MYTABLE_SEQ is the name of Oracle sequence to be used.

Resources