Bulk update over spring data jpa with join - jpa

I'm having troubles doing a bulk update (JPA 2.1) with spring-data-jpa.
According to docs:
5.3.8. Modifying queries
All the sections above describe how to declare queries to access a given entity or collection of entities. Of course you can add custom modifying behaviour by using facilities described in Custom implementations for Spring Data repositories. As this approach is feasible for comprehensive custom functionality, you can achieve the execution of modifying queries that actually only need parameter binding by annotating the query method with #Modifying:
#Modifying
#Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
But the entity I need update has a #ManyToOne relationship with another entity.
#Entity
#Table(name = "usuarios", indexes = {
#Index(name = "idx_usuarios_rut", columnList = "rut")
,#Index(name = "idx_usuarios_email", columnList = "email")})
public class User extends BaseEntity implements UserDetails {
...
#NotNull(message = "Debe seleccionar un nivel")
#ManyToOne(fetch = FetchType.LAZY)
public Role getRole() {
return role;
}
}
So my UPDATE is:
#Modifying
#Transactional
#Query("update User u set u.password = ?2 where u.company = ?1 and u.role.name not in ('ROLE_ADMIN', 'ROLE_ROOT')")
int updatePasswordForAll(Company company, String password);
The resulting native query is update usuarios cross join set password=? where company_id=? and (nombre not in ('ROLE_ADMIN' , 'ROLE_ROOT')) so I get com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual ...
What am I doing wrong ?
I tried with
#Query("update User u set u.password = ?2 join u.role r where u.company = ?1 and r.name not in ('ROLE_ADMIN', 'ROLE_ROOT')")
But this one is a bad formed update sentence org.hibernate.QueryException: node to traverse cannot be null! [update cl.arvisoft.mantenimiento.jpa.User u set u.password = ?2 join u.role r where u.company = ?1 and r.name not in ('ROLE_ADMIN', 'ROLE_ROOT')].

In JPQL you cannot join entities when doing update operation (see reference).
To workaround this situation try to use a sub-select, something like this (not tested):
update
User u
set
u.password = ?2
where
u.company = ?1 and
u.role in (select r from Role r where r.name not in ('ROLE_ADMIN', 'ROLE_ROOT'))

Related

How to filter one-to-many mapping based on entity fields in JPA?

I can represent this query:
SELECT * FROM group g JOIN user u ON user.group_id = group.id
via the following in JPA:
#EntityGraph(attributePaths = {"users.posts.comments"})
Optional<Group> findEagerlyFetchedById(UUID id);
But how do I filter out some users based on a field? I want to get the group with the given groupId, with user rows (and children of those) but only for users that are authenticated. As in, how do I represent the following SQL query in JPA?
SELECT * FROM group g JOIN user u ON user.group_id = group.id WHERE user.isAuthenticated = true
I currently have the query below but it takes an all-or-nothing approach. If a single user has matching isAuthenticated field then it returns the group along with all users regardless of whether that field is true for that user. Also, if no users are authenticated, then the group isn't returned at all.
#EntityGraph(attributePaths = {"users.posts.comments"})
#Query("SELECT g FROM Group g JOIN g.users gu WHERE gu.isAuthenticated = :isAuthenticated AND g.id = :groupId")
Optional<Group> findEagerlyFetchedByUserAuthed(UUID groupId, boolean isAuthenticated);
For reference these are the entity definitions:
Group:
#Entity
public class Group {
private UUID id;
#OneToMany(
mappedBy = "groups",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<User> users = Sets.newHashSet();
}
User:
#Entity
public class User {
private UUID id;
private Boolean isAuthenticated;
#ManyToOne( fetch = FetchType.LAZY )
private Group group;
}
From what i am understanding is that you want to select all groups that have authenticated users.
As I understand your problem with "then it returns the group along with all users regardless of whether" is that the database loads all the users, into the java/context even if this is not needed. The Problem for this is probably that even though the users are fetched lazy
#OneToMany(
mappedBy = "groups",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<User> users = Sets.newHashSet();
java has to evaluate weather or not or not the entry in the set is unique. Depending on your implementation of equals for Group or User (not shown in your example) it might be possible that the value of all fields is called, therefore requiring the Set to be fully loaded. A solution for this could be replacing the Set with a List.
private Set<User> users = new ArrayList<>();
Depending on your toString() implementation of the classes it could also just be a problem with debugging since most debuggers call the toString() implementation when trying to display an Object inside the debugger.
The second problem I understand you are approaching is "Also, if no users are authenticated, then the group isn't returned at all." I dont know how to help with that since your SQL clearly states
" ....g.users gu WHERE gu.isAuthenticated = :isAuthenticated ..."
this will always just return groups with authenticated users. Here i cant understand what your problem is. That is what i thought was your goal.
A practical approach that might help you could be selecting the Users and then accessing the groups (in Java via streams).
#Query("SELECT u FROM Users u WHERE u.isAuthenticated = :isAuthenticated)
List<Users> findEagerlyFetchedByUserAuthed(boolean isAuthenticated);
or trying to do a sub select of users first and then joining with something like this:
#Query(
"SELECT group
FROM from group
where groupid IN (SELECT u.groupId
FROM Users u
WHERE u.isAuthenticated = :isAuthenticated))
Optional<Group> findEagerlyFetchedByUserAuthed(UUID groupId, boolean isAuthenticated);
My syntax here is probably not 100% correct but i hope you got the idea.
Lastly it might be better to use
List<Group> findEagerlyFetc...
instead of
Optional<Group> findEagerlyFetc....
#EntityGraph with #Query not working properly.
Use JPA method naming query with #EntityGraph
#EntityGraph(attributePaths = {"users.posts.comments"})
Optional<Group> findByIdAndUsers_IsAuthenticated(UUID groupId, boolean isAuthenticated);
Note: To resolve ambiguity we can use _ inside your method name to manually define traversal points.

How to join fetch an attribute of element in collection with QueryDSL JPA

Entities as below:
class A {
Long id;
#ManyToMany
#JoinTable(name = "rel_a_b", joinColumns = #JoinColumn(name = "a_id"), inverseJoinColumns = #JoinColumn(name = "b_id"))
Set<B> bSet;
}
class B {
Long id;
#ManyToMany(mappedBy = "bSet")
Set<A> aSet;
#ManyToMany
#JoinTable(name = "rel_b_c", joinColumns = #JoinColumn(name = "b_id"), inverseJoinColumns = #JoinColumn(name = "c_id"))
Set<C> cSet;
}
class C {
Long id;
#ManyToMany(mappedBy = "cSet")
Set<B> bSet;
}
I need to select A entities and join fetch bSet and cSet in B entity. Using JPA Criteria, codes as below:
final Fetch<A, B> bSetFetch = rootA.fetch("bSet", JoinType.LEFT);
bSetFetch.fetch("cSet", JoinType.LEFT);
are working perfectly, but I can't achieve this with QueryDSL. I tried
final QA a = QA.a;
jpaQuery
.from(a)
.leftJoin(a.bSet, QB.b).fetchJoin()
.leftJoin(QB.b.cSet).fetchJoin()
.select(a)
but it throws exception that
query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=b,role=A.b,tableName=`b`,tableAlias=b4_,origin=a a2_,columns={a2_.id ,className=B}}] [select a
from A a
left join fetch a.bSet as b
left join fetch b.cSet]]
, if without fetchJoin(), the results don't include bSet and cSet. Could anyone solve this?
The fetch joins are applied correctly from a QueryDSL point of view. We can also observe this from the fact that the produced JPQL query looks correct.
The limitation here is that Hibernate only allows FETCH JOINS if the owner of the fetch association is projected in the select clause. cSet is an association on B, so you would need to project your b's or omit the fetch join for cSet. For example:
jpaQuery
.from(a)
.leftJoin(a.bSet, QB.b).fetchJoin()
.leftJoin(QB.b.cSet).fetchJoin()
.select(a, b)
Now this will result in duplicate results for a due to the cardinality of bSet. That is just a limitation of fetch joins in Hibernate.
Alternatively, you could consider specifying a fetch graph for the query:
EntityGraph postGraph = em.getEntityGraph("post");
query.setHint("javax.persistence.fetchgraph", postGraph);
For more information on using EntityGraphs see https://www.baeldung.com/jpa-entity-graph

How can I write an EXISTS predicate on a collection attribute in Criteria API?

I have these classes:
#Entity
public class Customer {
#Id long id;
String name;
#OneToMany List<Customer> related;
}
and I'm using this JPQL query:
select c from Customer c where c.name = 'ACME'
or exists( select 1 from c.related r where r.name = 'ACME' )
How can I write the same query with the Criteria API? I need to use exists with a subquery, like the JPQL, but I don't know how to create a subquery from a collection attribute in the Criteria API.
Something like this would give EXISTS (subquery)
Subquery<Long> sq = cq.subquery(Long.class);
Root<Customer> customerSub = sq.correlate(customer);
Join<Customer,Customer> related = customerSub.join(Customer_.related);
... extra config of subquery
Predicate existsCustomer = cb.exists(sq);
where cq is the CriteriaQuery, and cb is CriteriaBuilder. This comes from an example in the JPA 2.1 spec p323 Example 4

How replace native order by clause on JPA equivalent?

I use JPA 2.0 criteria builder. I need get data from one table and sort them by column from other. This tables have relations OneToMany:
class Club{
#OneToMany(mappedBy = "club")
private List<Address> addresses;
...
}
class Address{
#JoinColumn(name = "club_id", referencedColumnName = "id")
#ManyToOne(fetch = FetchType.LAZY)
private Club club;
#Column(name = "type")
private Long type;
#Column(name = "full_address")
private String full_address;
...
}
May be several address of some type but I need only one row of this specific address.
I write native queries with subquery, but it's has problem because subquery doesn't use in order clause and in select clause in JPA 2.0.
select c.full_name from club c
ORDER BY (select a.full_address from address a WHERE c.id= a.club_id and a.type=1 LIMIT 1)
select c.full_name, (select a.full_address from address a WHERE a.type=1 AND c.id=a.club_id LIMIT 1) as full_address FROM club c
ORDER BY fullAddress;
How I can replace native order by clause on JPA equivalent?
Thanks!
This native query also resolve problem and it can replace by JPA query
select c.full_name, min(a.full_address) FROM club c LEFT JOIN address a on c.id = a.club_id
where a.id is null or a.type=1 or not exists(SELECT 1 from address aSub WHERE aSub .club_id=c.id AND aSub.type=1)
GROUP BY c.id, c.full_name ORDER BY min(a.full_address);
JPA equivalent
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<ClubItem> query = builder.createQuery(ClubItem.class);
Root<Club> root = query.from(Club.class);
Join<Club, Address> addressJoin = root.join(Club_.address, JoinType.LEFT);
query.select(builder.construct(ClubItem.class, root.get(Club_.id), root.get(Club_.fullName), builder.function("min", String.class, addressJoin.get(Address_.fullAddress))));
Subquery<Address> subquery = query.subquery(Address.class);
Root<Address> addressRoot = subquery.from(Address.class);
subquery.select(addressRoot);
subquery.where(
builder.and(
builder.equal(addressRoot.get(Address_.type), 1),
builder.equal(addressRoot.get(Address_.clubId), root.get(Club_.id))));
query.where(builder.or(builder.isNull(addressJoin), builder.equal(addressJoin.get(Address_.type), builder.literal(new Long(1))),
builder.not(builder.exists(subquery))));
query.groupBy(root.get(Club_.id), root.get(Club_.fullName))
Order order = builder.asc(builder.function("min", String.class, addressJoin.get(Address_.fullAddress)));
query.orderBy(order);
TypedQuery<ClubItem> contentQuery = em.createQuery(query);
It's not terribly elegant, but it gets the job done...
Make your "Club" class implement Comparable. Put the order-by logic into the Comparable. Then use Collections.sort(unsortedList) to get the list into sorted form. There's also a Collections.sort(unsortedList, Comparable) method which could be useful, especially if you are doing a bunch of similar methods that just vary on order-by.

eclipselink JPQL Cascade query probleam

I have a problem in JPQL cascade query with eclipselink2.5
please see code
Entity code
public class Category{
...
#JoinColumn(name = "parent_category", referencedColumnName = "id")
#ManyToOne(cascade = CascadeType.REFRESH, fetch = FetchType.EAGER,optional = true)
private Category parentCategory;
...
}
JPQL code
String jpql = "select o from Category o order by o.parentCategory.sort ASC";
problem
the problem is this JPQL return list does not include 'o' if 'o.parentCategory' is null.
please see this table http://i.stack.imgur.com/xsXvk.jpg
the return list only rows id is 2,3,4 .
because the column parent_category is null, I lost rows 1,5,6
the correct result should be return all rows
Looking forward to your help!
Using o.parentCategory.sort in the order by clause forces an inner join which filters nulls. If you want nulls included, you will need to use an explicit outer join in the query:
"select o from Category o outer join o.parentCategory parentCategory order by parentCategory.sort ASC"