How do I do "SELECT something IN (...)" with jooq? - java

I'm trying to do the following with Jooq and can't for the life of me figure out how to do it properly:
select name, id in (
select capability_id
from a.capabilities_users
where user_id = ?)
from a.capabilities;
Basically I want to get all items (capabilities) and know whether each one applies to a particular user. It seems that all the condition type operators (like greater than or in) can only be used in the where and not the select. And I can't think of how else to express this.
Worst case, I can do a select count and then do the boolean logic in Java, but I was hoping to use fetchMap.

Depending on your database and schema meta data, a LEFT JOIN might be a better choice than a predicate in the projection. You should of course verify this in the execution plan.
Solving this with a LEFT JOIN:
-- NVL2 is Oracle syntax.
-- jOOQ will emulate NVL2 using CASE, if it's not available in your database
SELECT c.name, NVL2(cu.capability_id, 1, 0)
FROM a.capabilities c
LEFT OUTER JOIN a.capabilities_users cu
ON (c.id = cu.capability_id
AND cu.user_id = ?)
The above assumes, of course, that there is a unqiue constraint on cu(user_id, capability_id). This would then translate into jOOQ as such:
Capabilities c = CAPABILITIES.as("c");
CapabilitiesUsers cu = CAPABILITIES_USERS.as("cu");
Field<String> key = c.NAME.as("key");
Field<Boolean> value = nvl2(
CAPABILITIES_USER.CAPABILITY_ID, true, false
).as("value");
Map<String, Boolean> map =
DSL.using(configuration)
.select(key, value)
.from(c)
.leftOuterJoin(cu)
.on(c.ID.eq(cu.CAPABILITY_ID))
.and(cu.USER_ID.eq(...))
.fetchMap(key, value);
Solving this with a predicate in the projection:
If you really prefer a predicate in the projection, you might try DSL.field(Condition), which allows for precisely this:
Field<String> key = CAPABILITIES.NAME.as("key");
Field<Boolean> value = field(
CAPABILITIES.ID.in(
select(CAPABILITY_ID)
.from(CAPABILITIES_USERS)
.where(CAPABILITIES_USERS.USER_ID.eq(...))
)
).as("value");
Map<String, Boolean> map =
DSL.using(configuration)
.select(key, value)
.from(CAPABILITIES)
.fetchMap(key, value);
Note that if you're using a standards-compliant database, which doesn't allow for predicates to be treated as columns, DSL.field(Condition) will render an equivalent CASE statement for you.

Related

QueryDSL filtering with a label table

I have a table which is queried for a grid view using labels as filters.
Schema:
project:
id, col_a
label:
id, name, type
label_project:
id, label_id, project_id
The problem I have is that I want to get all project records with the labels the user is using but for some labels an OR needs to be done,
Here is a working example of what the query needs to do:
SELECT DISTINCT gd.*
FROM project p
JOIN label_project lp1 ON lp1.label_id=306
JOIN label_project lp2 ON lp2.label_id=135
JOIN label_project lp3 ON lp3.label_id=285
JOIN label_project lp4 ON lp4.label_id=173
WHERE ( lp1.project_id=p.id
OR lp2.project_id=p.id
) -- labels of lp1 and lp2 have the same type
AND lp3.project_id=p.id
AND lp4.project_id=p.id;
-- labels of (lp1, lp2), lp3 and lp4 have different types
Lets say there are 6 label "types" and for labels of the same type an OR needs to be done between them(see first where clause in query) for the rest use AND (see rest of where clause)
The problem with the example query is that it is extremely show in QueryDSL ~10 seconds for a single query. I read this is mainly because the query uses distinct.
Would anyone know a way to write this query in QueryDSL with better performance? Or in SQL for that matter
Query before label filtering is added:
query.distinct().from(PROJECT)
.leftJoin(FAVORITE_PROJECT)
.on(PROJECT.eq(FAVORITE_PROJECT.project).and(FAVORITE_PROJECT.employee.eq(employee)))
.where(ProjectService.restrictedProjectWhereClause(context.getEmployee()));
}
/**
* Returns a predicate that filters out results of restricted projects where the employee has no rights for
* #param employee The logged in employee
* #return The predicate
*/
public static Predicate restrictedProjectWhereClause(Employee employee) {
return PROJECT.restricted.isFalse()
.or(PROJECT.restricted.isTrue()
.and(PROJECT.employee.eq(employee)
.or(PROJECT.leaderEmployee.eq(employee)
.or(PROJECT.managerEmployee.eq(employee)
.or(hasRestrictedRoleAccess(employee).exists())))));
}
private static JPQLQuery<Integer> hasRestrictedRoleAccess(Employee employee) {
return JPAExpressions.selectFrom(USER_SECURITY_ROLE)
.join(USER)
.on(USER_SECURITY_ROLE.user.eq(USER))
.join(EMPLOYEE)
.on(USER_SECURITY_ROLE.user.eq(EMPLOYEE.user))
.where(USER_SECURITY_ROLE.securityRole.in(ESecurityRole.RESTRICTED_SECURITY_ROLES)
.and(EMPLOYEE.eq(employee)))
.select(USER_SECURITY_ROLE.id);
}
How I add the label filtering to the query in QueryDSL:
// First add necessary joins
for (int i = 0; i < labels.size(); i++) {
QLabelProject lp = new QLabelProject(String.format("lp%d", i));
labelMap.computeIfAbsent(labels.get(i).getSystemLabelType(), k -> new HashMap<>());
labelMap.get(labels.get(i).getSystemLabelType()).put(labels.get(i), lp);
query = query.join(lp)
.on(lp.project.eq(qProject));
}
// Decide where clause
BooleanExpression expression = null;
for (Map.Entry<ESystemLabelType, Map<Label, QLabelProject>> entry : labelMap.entrySet()) {
BooleanExpression subExpression = null;
for (Map.Entry<Label, QLabelProject> lp : entry.getValue().entrySet()) {
if (entry.getKey() == null) {
subExpression = subExpression == null ? lp.getValue().label.id.eq(lp.getKey().getId()) :
subExpression.and(lp.getValue().label.id.eq(lp.getKey().getId()));
} else {
subExpression = subExpression == null ? lp.getValue().label.id.eq(lp.getKey().getId()) :
subExpression.or(lp.getValue().label.id.eq(lp.getKey().getId()));
}
}
expression = expression == null ? (BooleanExpression)new BooleanBuilder().and(subExpression).getValue() :
expression.and(subExpression);
}
I don't really understand what you are trying to achieve, but see if something like this would work:
SELECT gd.*
FROM grid_data gd
JOIN label_grid_data AS lgd ON lgd.grid_data_id = gd.id
WHERE lgd.label_id IN (285, 173, 306, 135)
There WHERE clause may need to be more complex, but I suspect you don't really need all those subqueries.
Another approach:
( SELECT grid_data_id FROM label_grid_data
WHERE label_id IN (285, 173) -- "OR"
)
UNION ALL
( SELECT grid_data_id FROM label_grid_data
WHERE label_id IN (306, 135)
HAVING COUNT(*) = 2 -- kludge to achieve "AND"
)
Then
SELECT gd.*
FROM ( the above union ) AS lgd
JOIN grid_data gd ON gd.id = lgd.grid_data_id
That will give you the rows that have either 285 or 277 or both 406 and 135.
Then, please provide SHOW CREATE TABLE so we can advise on the optimal INDEXes to have.

Union query with multiple selects post java 8

Here is a query that I want to try out in MySQL
SELECT A.x
FROM A
WHERE A.y = 'P'
UNION
SELECT A.x
FROM A
WHERE A.y = 'Q'
The above is a cut-down, much simpler version of the original query that I am trying. In my original query, each SELECT statement involves multiple tables with INNER JOIN
If the possible number of values in 'y' column of table 'A' that I need to query upon is 'n', then my query will involve doing 'n-1' unions on 'n' SELECT statements
I know that JOOQ can do union of multiple SELECT statements. But is there a good way to do this post Java 8 style? maybe using Steam.collect()?
This is what I have but wondering if I could do better
String firstValueToQuery = valuesToQuery.get(0);
Select<Record5<UUID, UUID, String, Integer, String>> selectQuery = getSelectQueryForValue(firstValueToQuery);
valuesToQuery.stream()
.skip(1)
.forEach(valueToQuery -> selectQuery.unionAll(getSelectQueryForValue(valueToQuery)));
selectQuery.fetchStream();
Here is how I implement getSelectQueryForValue
private Select<Record5<UUID, UUID, String, Integer, String>> getSelectQueryForValue(String valueToQuery) {
return jooq.select(
A.P,
A.Q,
A.R,
A.S,
A.T)
.from(A)
.where(A.Y.eq(valueToQuery));
}
PS: I understand that I could rather use the 'IN' clause like below
SELECT A.x
FROM A
WHERE A.y IN ('P','Q',...)
But with my current data distribution in the database, MySQL is using a sub-optimal query plan. Thus using UNION so that the database implicitly prefers a faster query plan by making use of the right index
The idiomatic approach here would be as follows (using JDK 9 API):
try (Stream<Record5<UUID, UUID, String, Integer, String>> stream = valuesToQuery
.stream()
.map(this::getSelectQueryForValue)
.reduce(Select::union)
.stream() // JDK 9 method
.flatMap(Select::fetchStream)) {
...
}
It uses the useful Optional.stream() method, which was added in JDK 9. In JDK 8, you could do this instead:
valuesToQuery
.stream()
.map(this::getSelectQueryForValue)
.reduce(Select::union)
.ifPresent(s -> {
try (Stream<Record5<UUID, UUID, String, Integer, String>> stream =
s.fetchStream()) {
...
}
})
I blogged about this in more detail here.

OrmLite: Grouped Where Clauses with Joins

I am using ORMLite within my Android Application which uses a Database to Create/Update/Delete Objects.
I have the following table structure:
Table Wine
Country (own table / dao)
Winery (own table / dao)
Comment
PurchasePlace
some more columns, which are not relevant for this
What i am doing (only the crucial part):
In Words:
I want to show all Wines, where certain fields match a user input, but respecting the wine-type, which relates to a constant.
Preparing the Join Query Builder:
queryBuilderWinery.where().like("name", "%Value%");
queryBuilderCountry.where().like("name", "%Value%");
queryBuilderWine
.join(queryBuilderWinery)
.join(queryBuilderCountry)
.where().like("name", "%Value%")
.or().like("purchasePlace", "%Value%").and().eq("wine_type_id", 1)
Producing:
FROM wine
INNER JOIN winery
ON wine.winery_id = winery.id
INNER JOIN country
ON wine.country_id = country.id
WHERE ((wine.name LIKE '%in%' OR wine.comment2 LIKE '%in%') AND wine.wine_type_id = 1)
OR (winery.winery_name LIKE '%in%')
OR (country.country_name LIKE '%in%')
Where clause, which would be needed:
WHERE (wine.comment like '%Value%' OR
wine.purchasePlace like '%Value%' OR
winery.name like '%Value%' OR
country.name like '%Value%')
AND wine.wine_type_id = 1
or
WHERE wine.wine_type_id = 1
AND (wine.comment like '%Value%' OR
wine.purchasePlace like '%Value%' OR
winery.name like '%Value%' OR
country.name like '%Value%')
My question is, can this be done? I have read the documentation on this, i know a could use RPN (Reverse Polish Notation) or create the where clause with groups, but i am not sure on how to group the where
clause correctly, since all joins will have an OR Clause as well, and i need the AND Clause grouped seperatly.
If i would use Where Grouping, would this have effect on the Join-Conditions as well?
Any help appreciated.
Update:
I tried a raw Where Clause, but same Problem here, i cannot imagine a way, where i can group the WhereClause over more than one Table...
WineDAO wineDAO = new DatabaseHelperProvider().get(this).getWineDAO();
CountryDAO countryDAO = new DatabaseHelperProvider().get(this).getCountryDAO();
WineryDAO wineryDAO = new DatabaseHelperProvider().get(this).getWineryDAO();
QueryBuilder<Wine, Long> wineQueryBuilder = wineDAO.queryBuilder();
wineQueryBuilder = wineQueryBuilder.join(countryDAO.queryBuilder()).join(wineryDAO.queryBuilder());
Where<Wine, Long> where = wineQueryBuilder.where().raw("(winery.name = ? OR country.name = ?) AND wine_type_id = ?",
new SelectArg("winery.name", "in"),
new SelectArg("country.name", "in"),
new SelectArg("wine_type_id", 1));
String prepStatement = wineQueryBuilder.prepareStatementString();
List<Wine> query = where.query();
int i = query.size();
This will not work, because the column winery.name obviously is not contained within the table wine...

JDBC Template - One-To-Many

I have a class that looks like this. I need to populate it from two database tables, which are also shown below. Is there any preferred way to do this?
My thought is to have a service class to select a List<> via a ResultSetExtractor from a DAO. Then do a foreach on that list, and select a List<> of emails for the individual person via another ResultSetExtractor, and attach it from with the foreach loop.
Is there a better way, or is this as good as it gets?
public class Person {
private String personId;
private String Name;
private ArrayList<String> emails;
}
create table Person (
person_id varchar2(10),
name varchar2(30)
);
create table email (
person_id varchar2(10),
email varchar2(30)
);
This is best solved by an ORM. With JDBC, you have to do by hand what an ORM would do for you. Executing N + 1 queries is very inefficient. You should execute a single query, and build your objects manually. Cumbersome, but not hard:
select person.id, person.name, email.email from person person
left join email on person.id = email.person_id
...
Map<Long, Person> personsById = new HashMap<>();
while (rs.next()) {
Long id = rs.getLong("id");
String name = rs.getString("name");
String email = rs.getString("email");
Person person = personsById.get(id);
if (person == null) {
person = new Person(id, name);
personsById.put(person.getId(), person);
}
person.addEmail(email);
}
Collection<Person> persons = personsById.values();
I was looking for something similar, and although the answer is perfectly valid I went with this nice library instead https://simpleflatmapper.org/0203-joins.html
It also integrates perfectly with Spring boot.
main advantage is that you have a clean repository layer, it uses your pojo and makes refactoring much easier, and like hibernate you can still map deep nested and complex one to many and still be in control of what is executed.
It also has a nice jdbctemplate CRUD and Java 13 finally brings support for multi-line string literals which is very good for sql statements readability. hope this helps someone :)
In my case, I had to use the LinkedHashMap to keep the query result ordered by the position field.
From JavaDoc:
LinkedHashMap: "This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map."
HashMap: "This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time".
TIP: using the getOrDefault method eliminates the extra check for nullable object.
public List<BucketDto> findAll() {
var sql = """
SELECT
b.uuid bucket_uuid, b.position bucket_position, b.name bucket_name,
c.uuid card_uuid, c.position card_position, c.name card_name
FROM bucket AS b
LEFT JOIN card AS c ON c.bucket_id = b.id
ORDER BY b.position ASC, c.position ASC
""";
return jdbcTemplate.query(sql, rs -> {
Map<Double, BucketDto> resultMap = new LinkedHashMap<>();
while (rs.next()) {
var position = rs.getDouble("bucket_position");
var bucketDto = resultMap.getOrDefault(position, new BucketDto(
UUID.fromString(rs.getString("bucket_uuid")),
position,
rs.getString("bucket_name")));
if (Optional.ofNullable(rs.getString("card_uuid")).isPresent()) {
bucketDto.addCard(new CardDto(
UUID.fromString(rs.getString("card_uuid")),
rs.getDouble("card_position"),
rs.getString("card_name")));
}
resultMap.put(position, bucketDto);
}
return new ArrayList<>(resultMap.values());
});
}

How do I find a value in a column that just have unique values with EclipseLink?

You have the EntityManager.find(Class entityClass, Object primaryKey) method to find a specific row with a primary key.
But how do I find a value in a column that just have unique values and is not a primary key?
You can use appropriate JPQL with TypedQuery.
try {
TypedQuery<Bean> tq = em.createQuery("from Bean WHERE column=?", Bean.class);
Bean result = tq.setParameter(1, "uniqueKey").getSingleResult();
} catch(NoResultException noresult) {
// if there is no result
} catch(NonUniqueResultException notUnique) {
// if more than one result
}
For example, like this:
List<T> results = em.createQuery("SELECT t FROM TABLE t", T.class)
.getResultList();
With parameters:
List<T> results = em.createQuery("SELECT t FROM TABLE t where t.value = :value1")
.setParameter("value1", "some value").getResultList();
For single result replace getResultList() with getSingleResult():
T entity = em.createQuery("SELECT t FROM TABLE t where t.uniqueKey = :value1")
.setParameter("value1", "KEY1").getSingleResult();
One other way is to use Criteria API.
You can use a Query, either JPQL, Criteria, or SQL.
Not sure if your concern is in obtaining cache hits similar to find(). In EclipseLink 2.4 cache indexes were added to allow you to index non-primary key fields and obtain cache hits from JPQL or Criteria.
See,
http://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Caching/Indexes
Prior to 2.4 you could use in-memory queries to query the cache on non-id fields.
TL;DR
With in DSL level - JPA no practice mentioned in previous answers
How do I find a value in a column that just have unique values and is not a primary key?
There isn't specification for query with custom field with in root interface of javax.persistence.EntityManager, you need to have criteria base query.
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<R> criteriaQuery = criteriaBuilder.createQuery(EntityType.class)
Root<R> root = criteriaQuery.from(type);
criteriaBuilder.and(criteriaBuilder.equal(root.get(your_field), value));
You can also group your predicates together and pass them all together.
andPredicates.add(criteriaBuilder.and(root.get(field).in(child)));
criteriaBuilder.and(andPredicates.toArray(new Predicate[]{});
And calling result(rather single entity or a list of entities) with
entityManager.createQuery(suitable_criteria_query).getSingleResult();
entityManager.createQuery(suitable_criteria_query).getResultList();

Categories