I'm not a JPA persistence criteria API guru and sometimes I get very terrible headaches using it.
Yesterday I noticed a new very weird behaviour. The code I will post is an adaptation of existing functioning code, so don't focus on trivial errors. I'm using glassfish 3.1.1 and the corresponding eclipse persistence plugin and Mysql DB.
I have written a criteriaQuery which filters data from different tables. If this criteriaquery is executed twice the second time it generates wrong SQL query. I cannot figure out why.
public CriteriaQuery createQuery4Count(EntityManager em) {
Calendar lastDate4Search = GregorianCalendar.getInstance();
javax.persistence.criteria.CriteriaBuilder cb = em.getCriteriaBuilder();
javax.persistence.criteria.CriteriaQuery cq = cb.createQuery();
javax.persistence.criteria.Root<Permessimercepath> checkPointRt = cq.from(Permessimercepath.class);
javax.persistence.criteria.Path<Permessimerce> permessimerceClass = checkPointRt.get(Permessimercepath_.permessimerce);
Predicate checkPointDatePredicate = cb.isNull(checkPointRt.get(Permessimercepath_.dataTransito));
Predicate checkPointAreaPredicate = cb.equal(checkPointRt.get(Permessimercepath_.iDArea), area);
Predicate datePredicate = cb.greaterThanOrEqualTo(permessimerceClass.get(Permessimerce_.datafine), lastDate4Search.getTime());
Predicate isValidPredicate = cb.lt(permessimerceClass.get(Permessimerce_.statopermesso), Permessimerce.COMPLETED);
cq.where(cb.and(checkPointAreaPredicate, checkPointDatePredicate, datePredicate, isValidPredicate));
cq.select(cb.countDistinct(checkPointRt));
return cq;
}
CriteriaQuery myCriteriaQuery = createQuery4Count(getEntityManager())
javax.persistence.Query q = getEntityManager().createQuery(myCriteriaQuery );
Long Result = ((Long) q.getSingleResult()).intValue();
// second query created with the same criteriaQuery
q = getEntityManager().createQuery(myCriteriaQuery );
Long Result2 = ((Long) q.getSingleResult()).intValue();
The generated sql is
// First and correct one
SELECT COUNT(t0.ID_permesso) FROM permessimercepath t0 WHERE EXISTS (SELECT t1.ID_permesso FROM permessimerce t2, permessimercepath t1 WHERE ((((t0.ID_permesso = t1.ID_permesso) AND (t0.CheckPointIndex = t1.CheckPointIndex)) AND ((((t1.ID_Area = ?) AND (t1.DataTransito IS NULL)) AND (t2.Data_fine >= ?)) AND (t2.Stato_permesso < ?))) AND (t2.ID_permesso = t1.ID_permesso)))
bind => [3 parameters bound]
// Second and wrong one
SELECT COUNT(t0.ID_permesso) FROM permessimercepath t0, permessimerce t2, permessimercepath t1 WHERE (((((t1.ID_Area = ?) AND (t1.DataTransito IS NULL)) AND (t2.Data_fine >= ?)) AND (t2.Stato_permesso < ?)) AND (t2.ID_permesso = t1.ID_permesso))
If nobody has an idea on why it happens I can try to reproduce it in a simpler way.
Thanks
Filippo
The problem seems to related with EntityManager.createQuery(CriteriaQuery) method. I checked the documentation, but nothing is mentioned about whether this method modifies CriteriaQuery or not. Normally one expects this method to not modify the passed parameter. However, what you are getting suggests that EntityManager.createQuery(CriteriaQuery) method modifies the passed parameter.
If this is the case you will need to call createQuery4Count(getEntityManager()) before calling EntityManager.createQuery(CriteriaQuery) in your code each time.
It looks likely that you spotted bug in implementation. Slightly similar (or so to say more complex but different) case works fine with Hibernate. Also, section "6.8 Query Modification" from JPA Specification encourages reuse of CriteriaQuery:
A CriteriaQuery object may be modified, either before or after
TypedQuery objects have been created and executed from it. For
example, such modification may entail replacement of the where
predicate or the select list. Modifications may thus result in the
same CriteriaQuery “base” being reused for several query instances.
Related
I've got a few tables, Deployment, Deployment_Report and Workflow. In the event that the deployment is being reviewed they join together so you can see all details in the report. If a revision is going out, the new workflow doesn't exist yet new workflow is going into place so I'd like the values to return null as the revision doesn't exist yet.
Complications aside, this is a sample of the SQL that I'd like to have run:
DECLARE #WorkflowID int
SET #WorkflowID = 399 -- Set to -1 if new
SELECT *
FROM Deployment d
LEFT JOIN Deployment_Report r
ON d.FSJ_Deployment_ID = r.FSJ_Deployment_ID
AND r.Workflow_ID = #WorkflowID
WHERE d.FSJ_Deployment_ID = 339
The above in SQL works great and returns the full record if viewing an active workflow, or the left side of the record with empty fields for revision details which haven't been supplied in the event that a new report is being generated.
Using various samples around S.O. I've produced some Entity to SQL based on a few multiple on statements but I feel like I'm missing something fundamental to make this work:
int Workflow_ID = 399 // or -1 if new, just like the above example
from d in context.Deployments
join r in context.Deployment_Reports.DefaultIfEmpty()
on
new { d.Deployment_ID, Workflow_ID }
equals
new { r.Deployment_ID, r.Workflow_ID }
where d.FSJ_Deployment_ID == fsj_deployment_id
select new
{
...
}
Is the SQL query above possible to create using LINQ to Entities without employing Entity SQL? This is the first time I've needed to create such a join since it's very confusing to look at but in the report it's the only way to do it right since it should only return one record at all times.
The workflow ID is a value passed in to the call to retrieve the data source so in the outgoing query it would be considered a static value (for lack of better terminology on my part)
First of all don't kill yourself on learning the intricacies of EF as there are a LOT of things to learn about it. Unfortunately our deadlines don't like the learning curve!
Here's examples to learn over time:
http://msdn.microsoft.com/en-us/library/bb397895.aspx
In the mean time I've found this very nice workaround using EF for this kind of thing:
var query = "SELECT * Deployment d JOIN Deployment_Report r d.FSJ_Deployment_ID = r.Workflow_ID = #WorkflowID d.FSJ_Deployment_ID = 339"
var parm = new SqlParameter(parameterName="WorkFlowID" value = myvalue);
using (var db = new MyEntities()){
db.Database.SqlQuery<MyReturnType>(query, parm.ToArray());
}
All you have to do is create a model for what you want SQL to return and it will fill in all the values you want. The values you are after are all the fields that are returned by the "Select *"...
There's even a really cool way to get EF to help you. First find the table with the most fields, and get EF to generated the model for you. Then you can write another class that inherits from that class adding in the other fields you want. SQL is able to find all fields added regardless of class hierarchy. It makes your job simple.
Warning, make sure your filed names in the class are exactly the same (case sensitive) as those in the database. The goal is to make a super class model that contains all the fields of all the join activity. SQL just knows how to put them into that resultant class giving you strong typing ability and even more important use-ability with LINQ
You can even use dataannotations in the Super Class Model for displaying other names you prefer to the User, this is a super nice way to keep the table field names but show the user something more user friendly.
In the Apache Cayenne documentation, they provide an example of how to create a parameterized query using the Expression class' fromString() function:
// create a qualifier with two named parameters: "pname" and "aname"
Expression qual = Expression.fromString("paintingTitle = $pname or toArtist.artistName = $aname");
// build a query prototype of a query - simply another select query
SelectQuery proto = new SelectQuery(Painting.class, qual);
The making of such a query is pretty straightforward, except for one problem: the documentation does not explain what $pname and $aname are or how to set them to the values you want to query for!
Can anyone explain how to set these parameters??? Please advise...
You are probably checking older documentation. Check out "Named Parameter Expressions" here if you are on Cayenne 3.0, or "Creating Expressions from Strings" here for 3.1. But in any event, this is fairly simple - you put your parameters in a Map, and then use "expWithParameters" method. To follow your example:
Expression qual =
Expression.fromString("paintingTitle = $pname or toArtist.artistName = $aname");
Map<String, Object> params = new HashMap<>();
params.put("pname", "A");
params.put("aname", "B");
qual = qual.expWithParameters(params);
Note that in the last line I am reassigning the Expression, as 'expWithParameters' creates a clone.
my problem is about this kind of query :
select * from SOMETABLE where SOMEFIELD in ('STRING1','STRING2');
the previous code works fine within Sql Developer.
The same static query also works fine and returns me a few results;
Query nativeQuery = em.createNativeQuery(thePreviousQuery,new someResultSet());
return nativeQuery.getResultList();
But when I try to parameterize this, I encounter a problem.
final String parameterizedQuery = "select * from SOMETABLE where SOMEFIELD in (?selectedValues)";
Query nativeQuery = em.createNativeQuery(parameterizedQuery ,new someResultSet());
nativeQuery.setParameter("selectedValues","'STRING1','STRING2'");
return nativeQuery.getResultList();
I got no result (but no error in console).
And when I look at the log, I see such a thing :
select * from SOMETABLE where SOMEFIELD in (?)
bind => [STRING1,STRING2]
I also tried to use no quotes (with similar result), or non ordered parameter (:selectedValues), which leads to such an error :
SQL Error: Missing IN or OUT parameter at index:: 1
I enventually tried to had the parentheses set directly in the parameter, instead of the query, but this didn't work either...
I could build my query at runtime, to match the first (working) case, but I'd rather do it the proper way; thus, if anyone has an idea, I'll read them with great interest!
FYI :
JPA version 1.0
Oracle 11G
JPA support the use of a collection as a list literal parameter only in JPQL queries, not in native queries. Some JPA providers support it as a proprietary feature, but it's not part of the JPA specification (see https://stackoverflow.com/a/3145275/1285097).
Named parameters in native queries also aren't part of the JPA specification. Their behavior depends on the persistence provider and/or the JDBC driver.
Hibernate with the JDBC driver for Oracle support both of these features.
List<String> selectedValues = Arrays.asList("STRING1", "STRING2");
final String parameterizedQuery = "select * from SOMETABLE where SOMEFIELD in (:selectedValues)";
return em.createNativeQuery(parameterizedQuery)
.setParameter("selectedValues", selectedValues)
.getResultList();
Instead of:
nativeQuery.setParameter("selectedValues", params);
I had to use:
nativeQuery.setParameterList("selectedValues", params);
This worked for me in derby. parameter without "()".
List<String> selectedValues = Arrays.asList("STRING1", "STRING2");
final String parameterizedQuery = "select * from SOMETABLE where SOMEFIELD in
:selectedValues";
return em.createNativeQuery(parameterizedQuery)
.setParameter("selectedValues", selectedValues)
.getResultList();
Replace this:
nativeQuery.setParameter("selectedValues","'STRING1','STRING2'");
with
List<String> params;
nativeQuery.setParameter("selectedValues",params);
I also faced the same issue.
This is what I did:
List<String> sample = new ArrayList<String>();
sample.add("sample1");
sample.add("sample2");
And now you, can set the sample in params.
I am trying to generate and execute the following sql through CActiveRecord:
SELECT * FROM `bucket` `t`
WHERE bkt_user = unhex('A4FF2131E00C4696837689FCAAAC7DD2');
I came up with this:
$uuid = 'A4FF2131E00C4696837689FCAAAC7DD2';
$criteria = new CDbCriteria();
$expression = new CDbExpression(
'unhex(:value)',
array(':value'=>$uuid,));
$criteria->addCondition("bkt_user = :exp");
$criteria->params = array(':exp' => $expression);
$buckets = Bucket::model()->findAll($criteria);
The code executes without error or exception but does not return results as expected. A little debugging revealed that the snippet above generates the following sql:
SELECT * FROM `xpg_bucket` `t` WHERE bkt_user = :exp
and AFAIK only one CDbCommandBuilder::bindValue where :exp is bound to unhex(:value). There is no parameter binding done for :value. This is happening in CDbCommandBuilder::createFindCommand. CDbCommandBuilder::createInsertCommand, on the other hand seems to take care of a value itself being a CDbExpression and generated the appropriate bindValue's.
CDbExpression's documentation contains:
* CDbExpression is mainly used in {#link CActiveRecord} as attribute values.
* When inserting or updating a {#link CActiveRecord}, attribute values of
* type CDbExpression will be directly put into the corresponding SQL statement
* without escaping.
Does this mean CDbExpression is not meant to be used with CDbCriteria in findAll()? If yes, what is the alternative?
Actually, it turns out all that circus is not required. The following works with findAll:
$criteria->addCondition('bkt_user = unhex(:value)');
$criteria->params = array(':value'=>'665730BDEDA7489383E2519DB5DE6D60');
$buckets = Bucket::model()->findAll($criteria);
Be aware that the same will not work with add or update operations.
I'm somewhat new to Entity Framework (4). I've been tracking down a bug in our software, and I've nailed it down the follow quirk. I'm curious if anyone can help me explain why these two Counts (dataCount and data2Count) would be different depending on the way I've invoked them. data2Count is correct and actually matches up with what I have in SQL.
using (var context = new Entities(ConnectionString))
{
var startDateTime = DateTime.Parse("10/1/2011");
var endDateTime = DateTime.Parse("12/31/2011 23:59");
var query = from data in context.vDATA
where data.ParentId == parentId &&
data.TimeStamp >= startDateTime &&
data.TimeStamp <= endDateTime
select data;
var data = query.ToList();
var dataCount = data.Where(x => x.TestType == 20).Count();
//dataCount is 162
var data2 = query.Where(x => x.TestType == 20);
var data2Count = data2.Count();
//data2Count is 198
}
Thanks.
Alright, I think I found and fixed what was happening - although I'm not sure I can explain internally how EF handles it... I found an article here: Entity framework result discrepancy for a database views that sounded like a similar issue. My data in SQL had several records who's fields were roughly identical. The only distinguishing field was TestType. But it was NOT marked as part of the key. Simply extending the key to include this field caused the Counts to match up correctly.
How is parentId getting set? Is it a variable declared outside of a loop this code fragment is running in? If so, this might be acting funky because of closure issues. This other question gives you a lot of information on how to address the closure issues.