How do you parse Java Persistence Query Languare (JPQL) with EclipseLink? - jpa

I am developing a NamedQueryManager EJB that receives a JPQL string from a client and returns a query that only selects data that the user is authorized to read. For example, if the client program wants Companies, it would create the following query and send to the NamedQueryManager.
SELECT c FROM Company c
The NamedQueryManager would consider the query and the current user and if the user doesn't have full permissions to read Company entities would return a query similar to the following:
SELECT c FROM Company c where c.reader = :user
Instead of creating my own parser, I investigated EclipseLink 2.6.1 and found that I could perform the following to parse the query:
JPQLExpression jpql = new JPQLExpression(jpqlString, new JPQLGrammar2_1());
if (jpql.getQueryStatement() instanceof SelectStatement) {
logger.debug("JPQL Expression is a Select statement.");
} else if (jpql.getQueryStatement() instanceof UpdateStatement) {
logger.debug("JPQL Expression is a Update statement.");
} else if (jpql.getQueryStatement() instanceof DeleteStatement) {
logger.debug("JPQL Expression is a Delete statement.");
} else {
logger.debug("JPQL Expression is an unknown statement.");
}
I could then loop through the children of the QueryStatement and determine the pieces of the statement. For example, the following loops through the Select clause.
SelectStatement st = (SelectStatement) jpql.getQueryStatement();
logger.debug("JPQL Select Statement: {}", st);
logger.debug("********************");
SelectClause sc = (SelectClause) st.getSelectClause();
logger.debug("Select Clause: {}", sc);
for (Expression e : sc.children()) {
logger.debug("Child: {}", e);
}
for (Expression e : sc.orderedChildren()) {
logger.debug("Ordered Child: {}", e);
}
So my question is am I on the correct path for using EclipseLink to parse and modify my JPQL?
I am comfortable with the parsing but how do I modify? Should I build a new JPQL string using the parsed JPQL or can I add information to the parsed JPQL directly to create the new query?
I began investigating how I could determine if the passed jpql string is valid and found references to the HermesParser class in the org.eclipse.persistence.internal.jpa.jpql package.
Should I stay away from "internal" packages?
I don't think that the following code which I am using to parse the jpql string uses the HermesParser but the org.eclipse.persistence.jpa.jpql.parser package-info states that "This is the core of Hermes,..."
JPQLExpression jpql = new JPQLExpression(jpqlString, new JPQLGrammar2_1());
So this leads me to ask what is Hermes?
Any big picture help or references would be greatly appreciated.
Note: This is a continuation of the following question.
Should I use #NamedQuery annotation or addNamedQuery method?

Related

Spring JPA repository casting error when using JPQL

I have a PagingAndSorting JPA repository declared. I am using the #Query annotation.
I am getting an exception when I call the get() method on an Optional object from the findById(id) method of the repository.
The weird thing is it only happens when I use JPQL.
The code works if my query is native:
#Override
public BatchDto findById(String id) {
Optional<Batch> findResult = this.batchRepository.findById(id);
if (!findResult.isPresent()) return null;
Batch entity = findResult.get(); **<-------- Cast Exception Here**
BatchDto dto = this.mapper.toDto(entity, BatchDto.class);
List<BatchTransaction> transactions = entity.getTransactions();
dto.setTransactionDtos(mapper.toListDto(transactions, TransactionDto.class));
return dto;
}
Inspecting the findResult object with a breakpoint - I can see:
Optional[net.domain.data.batch#4b8bb6f]
when I have nativeQuery = true in the #Query annotation.
#Query(value = Sql.FindBatchById, nativeQuery = true)
Here is the query being used:
SELECT DISTINCT(B.batchNumber), COUNT(B.batchNumber) as TransactionCount FROM BATCH B WHERE B.batchReferenceNumber = :id GROUP BY B.batchNumber
However if I change it to JPQL and remove the nativeQuery=true attribute - the findResult is
Optional[[Ljava.lang.Object;#76e04327].
and I get a ClassCastException:
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to net.domain.data.batch
So bottom line - this works when specify nativeQuery=true and fails when I try to use JPQL.
I would prefer not to specify nativeQuery as we will eventually port this db to Oracle.
First of all the query shown below doesn't return a single Batch instance. Since there are distinct and count aggregate functions, the query will return a List of aggregates.
To be able to read that statistics you can add appropriate method into the batchRepository. Something like this:
#Query("SELECT DISTINCT(B.batchNumber) as dist, COUNT(B.batchNumber) as cnt FROM BATCH B GROUP BY B.batchNumber")
List<Map<Long, Long>> findStatistics();
and then iterate through the list.
UPD
If the id parameter exactly guarantee that will return a single record, you can change a return type to a Map
#Query("SELECT DISTINCT(B.batchNumber) as dist, COUNT(B.batchNumber) as cnt FROM BATCH B WHERE B.batchReferenceNumber = :id GROUP BY B.batchNumber")
Map<Long, Long> findStatisticsById(#Param("id") Long id);

Eclipselink history of related objects

I can create history of an entity with a HistoryCustomizer
#Entity
#Customizer(MyHistoryCustomizer.class)
public class Employee {..}
the HistoryCustomizer is something like this one:
public class MyHistoryCustomizer implements DescriptorCustomizer {
public void customize(ClassDescriptor descriptor) {
HistoryPolicy policy = new HistoryPolicy();
policy.addHistoryTableName("EMPLOYEE_HIST");
policy.addStartFieldName("START_DATE");
policy.addEndFieldName("END_DATE");
descriptor.setHistoryPolicy(policy);
}
}
The history objects can be fetched with the "AS_OF" hint
javax.persistence.Query historyQuery = em
.createQuery("SELECT e FROM Employee e", Employee.class)
.setParameter("id", id)
.setHint(QueryHints.AS_OF, "yyyy/MM/dd HH:mm:ss.SSS")
.setHint(QueryHints.READ_ONLY, HintValues.TRUE)
.setHint(QueryHints.MAINTAIN_CACHE, HintValues.FALSE);
just fine BUT, if you start accessing objects referenced by this historical object, the referenced objects will be the actual version of them. So the Employee from last year (fetched by a historical query) will have the current Address assigned to it and no the one it used to have last year.
How can I tell EclipseLink (2.5.0) to fetch the related object from the past as well?
In order to query the historical state of several - not just one like above - entities, we have to create an EclipseLink specific HistoricalSession. Queries run through this session will use the same historical timestamp and represent the proper historical state of the object graph.
I am using JPA in other parts of the code, so I will start with converting the JPA Query to an EclipseLink ReadAllQuery.
The HistoricalSession has its own entity cache, so that the historical entities do not mix with the normal ones.
// Get the EclipseLink ServerSession from the JPA EntitiyManagerFactory
Server serverSession = JpaHelper.getServerSession(emf);
// Only a ClientSession can give us a HistoricalSession so ask one from the ServerSession
ClientSession session = serverSession.acquireClientSession();
// Create the HistoricalSessions. A HistoricalSession is sticked to a point in the past and all the queries are executed at that time.
Session historicalSessionAfterFirstChild = session.acquireHistoricalSession(new AsOfClause(afterFirstChildAdded));
ReadAllQuery q;
Query jpaQuery = em.createQuery(query);
jpaQuery.setParameter("root", "parent");
// Extract the EclipseLink ReadAllQuery from the JPA Query. We can use named queries this way.
q=JpaHelper.getReadAllQuery(jpaQuery);
// This is a possible EclipseLink bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=441193
List<Object> arguments = new Vector<Object>();
arguments.add("parent");
q.setArgumentValues(arguments);
Vector<Parent> historyAwareParents ;
// Execute the query
historyAwareParents = (Vector<Parent>) historicalSessionAfterFirstChild.executeQuery(q);
for (Child c : historyAwareParents.get(0).children) {
System.out.println(c.getExtension() + " " + c.getRoot());
}

playframework selecting from h2 database

I am trying downloading all records from table in h2 buildin database in playframework.
I am facing an error:
[IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: * near line 1, column 8 [SELECT * FROM TABLE]]
Method CODE i class Table:
#Transactional(readOnly=true)
public static Result view() {
Query query = JPA.em().createQuery("SELECT * FROM TABLE");
List<Table> downloaded_from_db = query.getResultList();
System.out.println(downloaded_from_db.getClass());
return ok(view.render("none"));
}
Please help me. I would like to see downloaded records in console in simple view.
Please give me some tips or good tutorial.
After changing my class loooks like this:
#Transactional(readOnly=true)
public static Result view() {
List<MedicalIncidents> data = JPA.em()
.createNativeQuery("SELECT * FROM MedicalIncident")
//.createQuery("Select m from MedicalIncident m")
.getResultList();
System.out.println(data);
AND I think it works, cause I have 2 entries in that table in database:
But System.out.println(data) return in plaay console:
[[Ljava.lang.Object;#70a0c9be, [Ljava.lang.Object;#4c1d12b6]
But it should return this object by model name like in example: computer-database-jpa:
[models.Computer#214c6fde, models.Computer#63728eb3, models.Computer#75f6bcc6, models.Computer#19e3a7ab, models.Computer#3114d8d4, models.Computer#4fa75f78, models.Computer#756ce822, models.Computer#40fc4c68, models.Computer#73fc612c, models.Computer#3e4fcb31]
So I think that there is something wrong with it. Please help
You mxied SQL queries with JPQL query. The method you used createQuery needs an JPQL query:
SELECT e FROM Entity e
Also please note in JPQL there is no SELECT *. If you want to write a SQL query, use the method em.createNtiveQuery().

How to remove multiple objects in batch call using their IDs?

How can I remove multiple objects in batch call using their IDs ?
I tried this
EntityManager em = ...
em.getTransaction().begin();
try
{
for (Visitor obj : map.keySet())
{
Visitor fake = em.getReference(Visitor.class, obj.getId());
em.remove(fake);
}
em.getTransaction().commit();
}
catch (Exception ex)
{
ex.printStackTrace();
}
I see DELETE statements in log file but them it throws
<openjpa-2.1.1-r422266:1148538 fatal store error> org.apache.openjpa.persistence.RollbackException: Optimistic locking errors were detected when flushing to the data store. The following objects may have been concurrently modified in another transaction: [com.reporting.data.Visitor-53043]
at org.apache.openjpa.persistence.EntityManagerImpl.commit(EntityManagerImpl.java:593)
at com.reporting.ui.DBUtil.saveAnswers(DBUtil.java:311)
I have single thread.
Update:
I also tried
for (Visitor obj : map.keySet())
em.remove(obj);
But it's slow because on every iteration it sends SELECT to a server. I assume OpenJPA does it to reattach object to context.
After multiple experiments I ended up doing hacky JPQL query. Here is code-snippet:
List<Long> lstExistingVisitors = ...
Query qDeleteVisitors = em.createQuery("delete from Visitor obj where obj.id in (?1)");
qDeleteVisitors.setParameter(1, lstExistingVisitors);
qDeleteVisitors.executeUpdate();
I tried list as big as 5000 IDs. It works fine with mysql 5.1 and H2DB.
Try to use JPQL
em.createQuery("delete from Visitor v where v.id in (:param)")
.setParameter("param", idsList).executeUpdate();
OpenJPA docs: http://openjpa.apache.org/builds/1.2.0/apache-openjpa-1.2.0/docs/manual/jpa_langref.html#jpa_langref_bulk_ops

Execute StoredProcedure in CodeFirst 4.1

I understand stored procedures mapping is not supported by my understanding is that I should be able to call stored procedures.
I have quite a few complex stored procedures and with the designer I could create a complex type and I was all good.
Now in code first let's suppose I have the following stored procedure, just put together something silly to give an idea. I want to return a student with 1 address.
In code I have A Student and Address Entity. But no StudentAddressEntity as it's a link table.
I have tried the following but I get an error
Incorrect syntax near '."}
System.Data.Common.DbException {System.Data.SqlClient.SqlException}
ALTER Procedure [dbo].[GetStudentById]
#StudentID int
AS
SELECT *
FROM Student S
left join StudentAddress SA on S.Studentid = sa.studentid
left join Address A on SA.AddressID = A.AddressID
where S.StudentID = #StudentID
C# code:
using (var ctx = new SchoolContext())
{
var student = ctx.Database.SqlQuery<Student>("GetStudentById,#StudentID",
new SqlParameter("StudentID", id));
}
Any examples out there how to call sp and fill a complexType in code first, using out parameters etc.. Can I hook into ADO.NET?
Trying just an SP that returns all students with no parameters I get this error
System.SystemException = Cannot create a value for property
'StudentAddress' of type
'CodeFirstPrototype.Dal.Address'. Only
properties with primitive types are
supported.
Is it because I have in a way ignore the link table?
Any suggestions?
I believe that your exception actually is:
Incorrect syntax near ','.
because this is invalid statement: "GetStudentById,#StudentID". It should be without comma: "GetStudentById #StudentID".
The problem with stored procedures in EF is that they don't support loading navigation properties. EF will materialize only the main entity and navigation properties will not be loaded. This is solved for example by EFExtensions. EFExtensions are for ObjectContext API so you will have to check if it is also usable for DbContext API.
Using EFExtentions it will look something like
using (var context = new SchoolContext())
{
var command = context.CreateStoreCommand("GetStudentById", CommandType.StoredProcedure,
new SqlParameter("StudentID", id));
using (command.Connection.CreateConnectionScope())
using (var reader = command.ExecuteReader())
{
// use the reader to read the data
// my recommendation is to create a Materializer using EFExtensions see
// http://blogs.msdn.com/b/meek/archive/2008/03/26/ado-entity-framework-stored-procedure-customization.aspx
// ex
var student = Student.Materializer.Materialize(reader).SingleOrDefault();
return student;
}
}