I'm trying to implement a simple text search. I would like to allow a user to type any number of words, and check all of those words against the title of an article. A result would be returned if the title matched all of the words in the search (in any order).
Some example database queries would be something like this:
SELECT * FROM Article WHERE
lower(title) like lower(concat('%', '<term1>', '%'));
SELECT * FROM Article WHERE
lower(title) like lower(concat('%', '<term1>', '%'))
AND lower(title) like lower(concat('%', '<term2>', '%'));
SELECT * FROM Article WHERE
lower(title) like lower(concat('%', '<term1>', '%'))
AND lower(title) like lower(concat('%', '<term2>', '%'));
AND lower(title) like lower(concat('%', '<term3>', '%'));
Notice how the title column is searched in each case, and will be compared with either 1, 2, or 3 (or more) terms.
Can I create a single JPQL query to do this?
Something like this:
#Query("SELECT a FROM Article a WHERE lower(a.title) in %:terms%")
Iterable<Article> findAllContainingTitle(String... terms)
It appears Java's varargs work, but more in a sense of specific values within a set.
UPDATE 1
Thanks to this question, and this link I learned how to do this directly in a Postgres query:
SELECT * FROM Article WHERE ~~* ALL(ARRAY['%term1%', '%term2%', '%term3%']);
The challenge now has been to try and convert this to a Spring Data JPA query.
Note: the ~~* can also be exchanged with ILIKE (case insensitive LIKE statement).
This does not answer my question, but it at least provides a workaround, should anyone else need something similar. Part of the requirement was to also use JOIN FETCH to pull back all data in a single query, and not run into the N+1 performance problem.
The workaround is to use CriteriaBuilder and CriteriaQuery to generate the query dynamically. I feel this creates a less readable query, but it certainly is better than nothing.
#Autowired
private EntityManager entityManager;
...
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder()
CriteriaQuery<Content> criteriaQuery = criteriaBuilder.createQuery(Article.class);
Root<Article> rootArticle = crirteriaQuery.from(Article.class);
// Provide any number of joins here
rootArticle.fetch("author", JoinType.INNER);
// This is where we dynamically add all of the 'like' statements
List<Predicate> predicates = new ArrayList<>();
// Assume that 'searchQuery' is plain text from the user, that will be split on spaces to get individual search terms.
for (String searchTerm : searchQuery.split(" ")) {
predicates.add(criteriaBuilder.like(
criteriaBuilder.lower(rootArticle.get("title")),
"%"+searchTerm.toLowerCase()+"%"));
}
criteriaQuery.where(predicates.toArray(new Predicate[0]));
TypedQuery<Article> query = entityManager.createQuery(criteriaQuery);
List<Article> articles = query.getResultList();
Special thanks to this answer which linked to this page which explained how to add the JOIN statements to a CriteriaQuery.
Related
I am experiencing various things while studying JPA, but I am too unfamiliar with it, so I would like to get some advice.
The parts I got stuck in during my study were grouped into three main categories. Could you please take a look at the code below?
#Repository
public interface TestRepository extends JpaRepository<TestEntity,Long> {
#Query(" SELECT
, A.test1
, A.test2
, B.test1
, B.test2
FROM TEST_TABLE1 A
LEFT JOIN TEST_TABLE2 B
ON A.test_no = B.test_no
WHERE A.test3 = ?1 # Here's the first question
if(VO.test4 is not null) AND B.test4 = ?2") # Here's the second question
List<Object[] # Here's the third question> getTestList(VO);
}
First, is it possible to extract test3 from the VO received when using native sql?
Usually, String test1 is used like this, but I wonder if there is any other way other than this.
Second, if extracting is possible in VO, can you add a query in #QUERY depending on whether Test4 is valued or not?
Thirdly, if I use List<Object[]>, can the result of executing a query that is not in the already created entity (eg, test1 in TEST_TABLE2, which is not in the entity of TEST_TABLE1) can be included?,
First, is it possible to extract test3 from the VO received when using native sql? Usually, String test1 is used like this, but I wonder if there is any other way other than this.
Yes, it is possible.
You must use, eg where :#{[0].test3} is equals vo.test3
[0] is position the first param, past for method annotated with #Query
#Query(value = "SELECT a.test1, a.test2, b.test1, b.test2
FROM test_table1 a
LEFT JOIN test_table2 b ON a.test_no = b.test_no
WHERE a.test3 = :#{[0].test3}", nativeQuery = true)
List<Object[]> getList(VO);
Second, if extracting is possible in VO, can you add a query in #QUERY depending on whether Test4 is valued or not?
You can use a trick eg:
SELECT ... FROM table a
LEFT JOIN table b ON a.id = b.id
WHERE a.test3 = :#{[0].test3}
AND (:#{[0].test4} IS NOT NULL AND b.test4 = :#{[0].test4})
Thirdly, if I use List<Object[]>, can the result of executing a query that is not in the already created entity (eg, test1 in TEST_TABLE2, which is not in the entity of TEST_TABLE1) can be included?
Sorry, but I not understand the third question.
Maybe this tutorial will help you: https://www.baeldung.com/jpa-queries-custom-result-with-aggregation-functions
A sample of my data looks something like this:
{"city": "NY",
"skills": [
{"soft_skills": "Analysis"},
{"soft_skills": "Procrastination"},
{"soft_skills": "Presentation"}
],
"areas_of_training": [
{"areas of training": "Visio"},
{"areas of training": "Office"},
{"areas of training": "Risk Assesment"}
]}
I would like to run a query to find users with soft_skills Analysis and maybe run another one to find users whose area of training is Visio and Risk Assesment
My column type is jsonb. How can I implement a search query on these deeply nested objects? A query on level one for city works using SELECT * FROM mydata WHERE content::json->>'city'='NY';
How can I also run a match using the LIKE keyword or string matching for deeply nested values?
1)
SELECT * FROM mydata
WHERE content->'skills' #> '[{"soft_skills": "Analysis"}]';
2)
SELECT * FROM mydata
WHERE content->'areas_of_training' #> '[{"areas of training": "Visio"},{"areas of training": "Risk Assesment"}]';
About JSON(B) operators
PS: And be ready for extremely slow queries. I highly recommend to think about data normalization.
Update for LIKE
For your example data it could be:
SELECT * FROM mydata
WHERE EXISTS (
SELECT *
FROM jsonb_array_elements(content->'areas_of_training') as a
WHERE a->>'areas of training' ilike '%vi%');
But query highly depending on the actual JSON structure.
Use json_array_elements() to get values of nested elements, examples:
select d.*
from mydata d,
json_array_elements(content->'skills')
where value->>'soft_skills' ilike '%analysis%';
select d.*
from mydata d,
json_array_elements(content->'areas_of_training')
where value->>'areas of training' ~* 'visio|office';
It is possible that the query yields duplicate rows, so it is reasonable to use select distinct on (id), where id is a primary key.
Note that the function json_array_elements() is costly and you cannot use indexes in contrary to Abelisto's solution. However you have to use it if you want to have an access to values of nested json elements.
i try use "like" method from Criteriabuilder for get all record based on pattern " 10% ".
I want get record where ID is - 101, 10002, 1003,1000 etc...
I've use this code:
Predicate p = cb.like(r.<String>get("ID").as(String.class), "10%")
but i got Exception where i see what postgres can't execute query like this:
SELECT ID, NAME, SOMETHING FROM TABLE WHERE ID LIKE 10%
That is JPA (Glassfish 4.x) generate wrond query.
Right query must like that :
SELECT ID, NAME, SOMETHING FROM TABLE WHERE CAST (ID as TEXT) LIKE '10%'
How to build query via Criteria API that i got a right query for postgres ?
Updated:
I try write a CAST function :
Expression<String> postgresqlCastFunction = cb.function("CAST", String.class, r.<String>get("ID").as(String.class));
Predicate p = cb.like(postgresqlCastFunction, "10%");
but got a query like this :
FROM TABLE WHERE (CAST(ID) LIKE ?)
, so, how to add need expression in function for this right result -
FROM TABLE WHERE (CAST(ID as TEXT) LIKE ?) ..
An example implementation using PostgreSQL native TO_CHAR function may look as follows:
JPQL
SELECT r FROM Records r
WHERE FUNCTION('TO_CHAR', r.ID, 'FM9999999999') LIKE :pattern
Criteria API
Path<String> id = r.get("ID");
Expression<String> format = cb.literal("FM9999999999");
Expression<String> function= cb.function("TO_CHAR", String.class, id, format);
ParameterExpression<String> pattern = cb.parameter(String.class, "pattern");
Predicate like = cb.like(function, pattern);
cq.where(like);
also you can build the query as an one-liner:
cq.where(cb.like(cb.function("TO_CHAR", String.class, r.get("ID"), cb.literal("FM9999999999")), cb.parameter(String.class, "pattern")));
Execute the above query:
Query q = em.createQuery(cq).setParameter("pattern", "10%");
I am trying to use the Criteria API instead of constructing queries as JPQL Strings as the Criteria API seems much better suited for this. However, I am having a few problems understanding how to construct the two following statements.
SELECT e
FROM Subject e
WHERE e.company = :c1
OR e.company = :c2
OR e.company = :c3
In this case I need to iterate over an unknown number of values (c1, c2, c3 etc.) to match to the same attribute.
SELECT e
FROM Subject e
WHERE e.company
LIKE :c1
OR e.account
LIKE :c1
OR e.email
LIKE :c1
In this case I need to pass in a single value (c1) and have a 'LIKE' comparison done on a specific range of attributes.
My current pattern looks something like this:
// Criteria
CriteriaBuilder builder = subjectDAO.criteriaBuilder();
CriteriaQuery<Subject> query = builder.createQuery(Subject.class);
// Root
Root<Subject> subject = query.from(Subject.class);
List<Predicate> predicates = new ArrayList();
for (String property : map.keySet()) {
String value = (String) coreFilter.get(map);
predicates.add(????? This is where I come unstuck ?????);
}
// pass all the predicates into the query
query.where(predicates.toArray(new Predicate[predicates.size()]));
NB. I don't have any problems constructing the Query object or specifying the Root or Joins. I am just having problems with the specificity of the above queries. For the sake of clarity just assume that all the attributes are String and don't require any joins.
The expression CriteriaQuery<T> where(Predicate... restrictions), as you can see in the javadoc,
Modify the query to restrict the query result according to the conjunction of the specified restriction predicates.
So, it makes the conjunction of the predicates in the list, i.e. it concatenates them with AND expressions. In order to concatenate them with OR expression, simply use CriteriaBuilder#or(Predicate... restrictions)
query.where(builder.or(predicates.toArray(new Predicate[] {})));
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.