how to get tuple in jpql using params - jpa

Now I am using JPA to access database. I want to get the comments in the specific courses and specific lessons, so the sql would like this:
select * from comment where (commentType, commentId) in (("course", "1"), ("lesson", 2))
I use annotation #Query like this:
#Query("select c from Comment c where (c.commentType, c.commentId) in :attaches")
Page<Comment> findByAttachIn(#Param("attaches") List<String[]> attaches, Pageable pageable);
But finally I got the sql like this:
select * from comment where (commentType, commentId) in (?)
JPA can not translate the array from jql to sql. What shall I do?

An IN expression allows only a single-valued path expressions, so you can't do (commentType, commentId) in in a JPQL query.

JPQL BNF documentation is very clear for the IN part
in_expression ::= {state_field_path_expression | type_discriminator} [NOT] IN { ( in_item {, in_item}* ) | (subquery) | collection_valued_input_parameter }
in_item ::= literal | single_valued_input_parameter
Consequently it can be concluded that you can't do that in JPQL. What you can do is do it manually in the WHERE clause ...
... WHERE (commentType = :type1 AND commentId = :id1) OR
(commentType = :type2 AND commentId = :id2) OR
...
long winded, but gets the answer, and complies with JPQL BNF notation. How you convert standard JPQL into some Spring syntax is left as an exercise to those familiar with that non-standard part

Related

Criteria query containing a collection valued property

Is it possible (and if so how) to create a criteria query that results in a tuple or array of which some elements are collections from a collection valued property?
Given an entity Dummy which has a List<SubEntities> with name subs
class Dummy {
String name;
List<SubEntity> subs;
}
class SubEntity {
// some attributes
}
I want a criteria API query which results in something with the structure
Tuple>
An Array instead of a Tuple would be fine, same for an Array or similar for List.
I tried the following:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> q = cb.createQuery(Object[].class);
Root<DummyEntityWithCollection> root = q.from(DummyEntityWithCollection.class);
Join<Object, Object> subs = root.join("subs");
q.select(cb.array(root.get("name"), subs));
List<Object[]> list = em.createQuery(q).getResultList();
But the Object[]s contained in list have as the second element SubEntitys instead of List<SubEntity>.
This one fails in the same way:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> q = cb.createTupleQuery();
Root<DummyEntityWithCollection> root = q.from(DummyEntityWithCollection.class);
Join<Object, Object> subs = root.join("subs");
q.multiselect(root.get("name"), subs);
List<Tuple> list = em.createQuery(q).getResultList();
This variant
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> q = cb.createQuery(Object[].class);
Root<DummyEntityWithCollection> root = q.from(DummyEntityWithCollection.class);
q.select(cb.array(root.get("name"), root.get("subs")));
List<Object[]> list = em.createQuery(q).getResultList();
Doesn't work at all and results in an invalid SQL statement with a single . as one select column (at least for Hibernate and HSQLDB).
The question How can I retrieve a collection property using criteria Api seems to indicate that it is not possible but it is based on Hibernate and I would like to get a JPA based answer. Especially an answer pointing out the section of the JPA spec that makes it clear that this is not possible would be appreciated.
JPQL defines the select clause in section 4.8 as
select_clause ::= SELECT [DISTINCT] select_item {, select_item}*
select_item ::= select_expression [[AS] result_variable]
select_expression ::= single_valued_path_expression | scalar_expression | aggregate_expression | identification_variable | OBJECT(identification_variable) | constructor_expression
so you can see that multi-valued expressions are not selectable in JPQL.
Criteria is simply a way to create a query using an API and objects. See JPA spec chapter 6.1:
The semantics of criteria queries are designed to reflect those of Java Persistence query
language queries.
So it's reasonable to assume the same constraint applies to the Criteria API.

Passing a null parameter to a native query using #Query JPA annotation

In a Spring Boot application, I have a SQL query that is executed on a postgresql server as follows :
#Query(value = "select count(*) from servers where brand= coalesce(?1, brand) " +
"and flavour= coalesce(?2, flavour) ; ",
nativeQuery = true)
Integer icecreamStockCount(String country, String category);
However,
I get the following error when I execute the method :
ERROR: COALESCE types bytea and character varying in PostgreSQL
How do I pass String value = null to the query?
**NOTE : ** I found that my question varied from JPA Query to handle NULL parameter value
You need not coalesce, try this
#Query("select count(*) from servers where (brand = ?1 or ?1 is null)" +
" and (flavour = ?2 or ?2 is null)")
Integer icecreamStockCount(String country, String category);
When I encounted this error, I ended up using a combination of OR and CAST to solve the issue.
SELECT COUNT(*)
FROM servers
WHERE (?1 IS NULL OR brand = CAST(?1 AS CHARACTER VARYING))
AND (?2 IS NULL OR flavour = CAST(?2 AS CHARACTER VARYING))
This works even if ?1, ?2, brand and flavor are all nullable fields.
Note that passing null for ?1 means "all servers regardless of brand" rather than "all servers without a brand". For the latter, you could use IS DISTINCT FROM as follows.
SELECT COUNT(*)
FROM servers
WHERE (CAST(?1 AS CHARACTER VARYING) IS NOT DISTINCT FROM brand)
AND (CAST(?2 AS CHARACTER VARYING) IS NOT DISTINCT FROM flavour)
Finally, certain parameter types such as Boolean cannot be cast in SQL from BYTEA to BOOLEAN, for those cases you need a double cast:
SELECT COUNT(*)
FROM servers
WHERE (?1 IS NULL OR is_black = CAST(CAST(?1 AS CHARACTER VARYING) AS BOOLEAN))
In my eyes this is a problem in Hibernate which could be solved by passing Java null parameters as plain SQL NULLs rather than interpreting null as a value of type BYTEA.
If you really need to use native query, there is a problem because it's an improvement not implemented yet, see hibernate. If you don't need to use native you can do (where ?1 is null or field like ?1). Assuming you do need native,
you may treat the String before by setting this empty and then calling the repository and this one would be like:
#Query(value = "select count(*) from servers where (?1 like '' or brand like ?1) " +
"and (?2 like '' or flavour like ?2)",
nativeQuery = true)
Integer icecreamStockCount(String country, String category);
There is always javax.persistence.EntityManager bean as option for native query situations and I recommend it instead of previous approach. Here you can append to your query the way you want, as follows:
String queryString = "select count(*) from servers ";
if (!isNull(country)) queryString += "where brand like :country";
Query query = entityManager.createNativeQuery(queryString);
if (!isNull(country)) query.setParameter("country", country);
return query.getResultList();
Observations:
Newer versions have improved this '+' concatenation Strings. But you can build your queryString the way you want with StringBuilder or String Format, it doesn't matter.
Be careful with SQL injection, the setParameter method avoid this kind of problem, for more information see this Sql Injection Baeldung
So this is not the exact answer to the question above, but I was facing a similar issue, I figured I would add it here, for those that come across this question.
I was using a native query, in my case, it was not a singular value like above, but I was passing in a list to match this part of the query:
WHERE (cm.first_name in (:firstNames) OR :firstNames is NULL)
I was getting the bytea error, in the end I was able to send an empty list.
(null == entity.getFirstName()? Collections.emptyList() : entity.getFirstName())
In this case, sending the empty list to the resolver worked, where as null did not.
hope this saves you some time.
null parameters are not allowed before Hibernate 5.0.2.
See https://hibernate.atlassian.net/browse/HHH-9165
and the replies to https://www.postgresql.org/message-id/6ekbd7dm4d6su5b9i4hsf92ibv4j76n51f#4ax.com

Search database using Entity Framework. LIKE operator

I want to search one of my tables using Entity Framework 5. I don't know how many words there are in the query, but I want to match all of them.
query = hello
SELECT * FROM [table] WHERE [column] LIKE '%hello%'
query = hello world
SELECT * FROM [table] WHERE [column] LIKE '%hello%' AND [column] LIKE '%world%'
I know the function PATINDEX , but it doesn't work good enough. Why? I'll show you:
SELECT * FROM person WHERE PATINDEX('%test%.com%', email)>0
will match "test#email.com", but if the search word are ordered the other way, it will not find this person:
SELECT * FROM person WHERE PATINDEX('%.com%test%', email)>0
What is the most efficient way to create this query using EF?
using linq to entities you can use .Contains to do the equivilant in SQL
table(x => x.column).Where(y => y.ColumnName).Contains("hello");
Sorry forgot the where clause that should work.
You can build the query in a foreach loop:
var words = new[] {"com", "test"};
var table = <your initial DbSet or ObjectSet>
foreach (var word in words)
{
string word1 = word; // prevent modified closure.
table = table.Where(x => x.column.Contains(word1));
}
var result = table.ToList(); // Enumerate the composed linq query.
The Contains function translates to LIKE with the search term enclosed in % characters.

Escaping formula for Grails derived properties

Grails offers derived properties to generate a field from a SQL expression using the formula mapping parameter:
static mapping = {
myfield formula: "field1 + field2"
}
I'm trying to use the formula parameter with a PostgreSQL database to make a concatenated field. The syntax is a little strange since PostgreSQL 8.4 doesn't yet support concat_ws:
static mapping = {
myfield formula: "array_to_string(array[field1, field2],' ')"
}
The produced SQL shown with loggingSql = true in the DataSource config has the table prefix inserted into some strange places:
select table0_.field1 as field1_19_0_,
table0_.field2 as field2_19_0_,=
array_to_string(table0_.array[field1, table0_.field2], ' ') as formula0_0_
from test_table table0_ where table0_.id=?
The table prefix errantly appears before array but not before field1 in the derived formula. Is there a way to escape the prefix or correct this behavior more explicitly?
This is just an issue with parsing the formula syntax. GORM tries to insert the table prefix for unquoted expressions not followed by parens, so the ARRAY[] notation trips it up.
My solution was to define the concat_ws function:
CREATE OR REPLACE FUNCTION concat_ws(separator text, variadic str text[])
RETURNS text as $$
SELECT array_to_string($2, $1);
$$ LANGUAGE sql;
The GORM formula parameter can now avoid the ARRAY[] syntax, and works as expected.
myfield formula: "concat_ws(' ', field1, field2)"
I had a very similar problem and solved it by adding single-quotes around the things that GORM was trying to prefix:
static mapping =
{
dayOfYear formula: " EXTRACT('DOY' FROM observed) "
}
GORM then produced this, which worked:
select
EXTRACT('DOY' FROM observed) as y1_
This may not work in all cases, but I hope it helps somebody.

JPA date literal

How represent a date in a JPA query, without using (typed) parameters?
If the date is really fixed (for example, 1 mar 1980), the code:
TypedQuery<MyEntity> q = em.createQuery("select myent from db.MyEntity myent where myent.theDate=?1", db.MyEntity.class).setParameter(1, d);
having set:
Date d = new Date(80, Calendar.MARCH, 1);
is quite verbose, isn't it? I would like to embed 1980/1/3 into my query.
UPDATE:
I modified the sample date to 1980/1/3, because 1980/1/1 as it was, was ambiguous.
IIRC you can use date literals in JPQL queries just like you do it in JDBC, so something like:
// d at the beginning means 'date'
{d 'yyyy-mm-dd'} i.e. {d '2009-11-05'}
// t at the beginning means 'time'
{t 'hh-mm-ss'} i.e. {t '12-45-52'}
// ts at the beginning means 'timestamp'; the part after dot is optional
{ts 'yyyy-mm-dd hh-mm-ss.f'} i.e. {ts '2009-11-05 12-45-52.325'}
should do the work (the curly braces and apostrophes are required).
I spent a couple days digging around on this. Seems the root of the problem is that the Hibernate generated grammar does not include support for temporal literals.
The JPA specification does include support for temporal literals but does not require persistence providers to translate from from the JPA syntax to the native syntax of the JDBC driver. From the JPA2 Spec 4.6.1:
"The JDBC escape syntax may be used for the specification of date, time, and timestamp literals. For example:
SELECT o
FROM Customer c JOIN c.orders o
WHERE c.name = 'Smith'
AND o.submissionDate < {d '2008-12-31'}
The portability of this syntax for date, time, and timestamp literals is dependent upon the JDBC driver in use. Persistence providers are not required to translate from this syntax into the native syntax of the database or driver."
It would be nice if Hibernate did provide support for date literals, but it seems the implementation for this is a little more involved that I'd suspected.
The functionality lacking here as far as my needs are concerned is that you cannot do a select coalesce(somePath, someDateLiteral) query. You can still do a where somePath=someDate. As long as they're mapped entities you can throw whatever you want in a where clause.
I used criteriaQuery for my querys, its just great. It looks like:
#Override
public List<Member> findAllByDimensionAtTime(Dimension selectedDimension,
Date selectedDate) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Member> criteriaQuery = criteriaBuilder
.createQuery(Member.class);
Root<Member> member = criteriaQuery.from(Member.class);
criteriaQuery
.select(member)
.where(criteriaBuilder.lessThanOrEqualTo(
member.get(Member_.validFrom), selectedDate),
criteriaBuilder.greaterThanOrEqualTo(
member.get(Member_.validTo), selectedDate),
criteriaBuilder.equal(
member.get(Member_.myDimensionId),
selectedDimension.getId())).distinct(true);
return em.createQuery(criteriaQuery).getResultList();
validFrom and validTo are date fields!
Edit: shorter Example (according to yours):
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<MyEntity> criteriaQuery = criteriaBuilder
.createQuery(MyEntity.class);
Root<MyEntity> entity= criteriaQuery.from(MyEntity.class);
criteriaQuery
.select(member)
.where(criteriaBuilder.equal(
entity.get(MyEntity_.theDate),
new Date(80, Calendar.MARCH, 1);)).distinct(true);
return em.createQuery(criteriaQuery).getResultList();