JPA CriteriaQuery ManyToMany Predicate - spring-boot

A many-to-many relationship exists between songs and artists like this:
public class Song {
// ... other fields here
protected Collection<Artist> artists;
#JoinTable(
name = "song_artists",
schema = "playout",
joinColumns = #JoinColumn(name = "song"),
inverseJoinColumns = #JoinColumn(name = "artist"),
foreignKey = #ForeignKey(name = "fk_song_artists_song"),
inverseForeignKey = #ForeignKey(name = "fk_audio_artists_artist")
)
#ManyToMany
public Collection<Artist> getArtists(){
return artists;
}
}
The artist class is a basic Entity
public class Artist {
}
Given a Song x, show songs by any of the artists involved in Song x
, A raw SQL query would be something like this
SELECT * FROM songs WHERE id IN((SELECT song FROM song_artists x WHERE x.artist = ?));
Where '?' would be replaced by a comma separated string of artist IDs involved in the song in question
How can the same result be achieved with JPA (using Hibernate specifically)
Song song = null; // get desired song
Collection<Artist> artists = song.getArtists();
CriteriaBuilder builder = entityManager.getCriteriaBuilder()
CriteriaQuery criteria = builder.createQuery(Song.class);
Root<Song> root = criteria.from(Song.class);
Subquery<Artist> subquery = null; // how to create an appropriate subquery from the join
How can we filter the results here (get more songs by any of the artists in "artists" collection)?
Your feedback will be very much appreciated

Solved it!
In case you face something similar, here's what I did:
Subquery<Artist> subquery = criteria.subquery(Artist.class);
subquery.from(Artist.class);
Join<Audio, Artist> join = subquery.correlate(root.join("artists", JoinType.LEFT));
subquery.select(builder.nullLiteral(Artist.class));
subquery.where(join.in(value.getArtists()));
criteria.select(root).where(
builder.and(
builder.notEqual(root, value), builder.exists(subquery)
)
);

Related

Android Room Multimap issue for the same column names

As stated in official documentation, it's preferable to use the Multimap return type for the Android Room database.
With the next very simple example, it's not working correctly!
#Entity
data class User(#PrimaryKey(autoGenerate = true) val _id: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) val _id: Long = 0, val bookName: String, val userId: Long)
(I believe a loooot of the developers have the _id primary key in their tables)
Now, in the Dao class:
#Query(
"SELECT * FROM user " +
"JOIN book ON user._id = book.userId"
)
fun allUserBooks(): Flow<Map<User, List<Book>>>
The database tables:
Finally, when I run the above query, here is what I get:
While it should have 2 entries, as there are 2 users in the corresponding table.
PS. I'm using the latest Room version at this point, Version 2.4.0-beta02.
PPS. The issue is in how UserDao_Impl.java is being generated:
all the _id columns have the same index there.
Is there a chance to do something here? (instead of switching to the intermediate data classes).
all the _id columns have the same index there.
Is there a chance to do something here?
Yes, use unique column names e.g.
#Entity
data class User(#PrimaryKey(autoGenerate = true) val userid: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) valbookid: Long = 0, val bookName: String, val useridmap: Long)
as used in the example below.
or
#Entity
data class User(#PrimaryKey(autoGenerate = true) #ColumnInfo(name="userid")val _id: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) #ColumnInfo(name="bookid")val _id: Long = 0, val bookName: String, val #ColumnInfo(name="userid_map")userId: Long)
Otherwise, as you may have noticed, Room uses the value of the last found column with the duplicated name and the User's _id is the value of the Book's _id column.
Using the above and replicating your data using :-
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
var currentUserId = dao.insert(User(name = "Eugene"))
dao.insert(Book(bookName = "Eugene's book #1", useridmap = currentUserId))
dao.insert(Book(bookName = "Eugene's book #2", useridmap = currentUserId))
dao.insert(Book(bookName = "Eugene's book #3", useridmap = currentUserId))
currentUserId = dao.insert(User(name = "notEugene"))
dao.insert(Book(bookName = "not Eugene's book #4", useridmap = currentUserId))
dao.insert(Book(bookName = "not Eugene's book #5", useridmap = currentUserId))
var mapping = dao.allUserBooks() //<<<<<<<<<< BREAKPOINT HERE
for(m: Map.Entry<User,List<Book>> in mapping) {
}
for convenience and brevity a Flow hasn't been used and the above was run on the main thread.
Then the result is what I believe you are expecting :-
Additional
What if we already have the database structure with a lot of "_id" fields?
Then you have some decisions to make.
You could
do a migration to rename columns to avoid the ambiguous/duplicate column names.
use alternative POJO's in conjunction with changing the extract output column names accordingly
e.g. have :-
data class Alt_User(val userId: Long, val name: String)
and
data class Alt_Book (val bookId: Long, val bookName: String, val user_id: Long)
along with :-
#Query("SELECT user._id AS userId, user.name, book._id AS bookId, bookName, user_id " +
"FROM user JOIN book ON user._id = book.user_id")
fun allUserBooksAlt(): Map<Alt_User, List<Alt_Book>>
so user._id is output with the name as per the Alt_User POJO
other columns output specifically (although you could use * as per allUserBookAlt2)
:-
#Query("SELECT *, user._id AS userId, book._id AS bookId " +
"FROM user JOIN book ON user._id = book.user_id")
fun allUserBooksAlt2(): Map<Alt_User, List<Alt_Book>>
same as allUserBooksAlt but also has the extra columns
you would get a warning warning: The query returns some columns [_id, _id] which are not used by any of [a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_User, a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_Book]. You can use #ColumnInfo annotation on the fields to specify the mapping. You can annotate the method with #RewriteQueriesToDropUnusedColumns to direct Room to rewrite your query to avoid fetching unused columns. You can suppress this warning by annotating the method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns returned by the query: _id, name, _id, bookName, user_id, userId, bookId. public abstract java.util.Map<a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_User, java.util.List<a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_Book>> allUserBooksAlt2();
Due to Note that Room will not rewrite the query if it has multiple columns that have the same name as it does not yet have a way to distinguish which one is necessary. the #RewriteQueriesToDropUnusedColumns doesn't do away with the warning.
if using :-
var mapping = dao.allUserBooksAlt() //<<<<<<<<<< BREAKPOINT HERE
for(m: Map.Entry<Alt_User,List<Alt_Book>> in mapping) {
}
Would result in :-
possibly other options.
However, I'd suggest fixing the issue once and for all by using a migration to rename columns to all have unique names. e.g.

JPA Specification sort by native SQL field

I'm using Spring Boot v1.5.3
In my code, I have a search operation with a lot of conditions.
public Page<ParentObject> search(Pageable pageable) {
Specification<ParentObject> specification = (root, cq, cb) -> {
Predicate p = cb.and(
cb.equals(root.get("child").get("id"), "someValue"),
// a lot of predicates appended by conditions
);
return p;
};
Sort newSort = pageable.getSort().and(new Sort(Sort.Direction.ASC, "child.id"));
pageable = new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), newSort)
Page<ParentObject> result = parentObjectRepository.findAll(specification, pageable);
return result;
}
The problem is that my parent table contains child_id field with index. And I want SQL to be like:
SELECT .... FROM parent p INNER JOIN child c ON c.id = p.child_id WHERE ...
ORDER BY p.child_id ASC;
But in result I have:
SELECT .... FROM parent p INNER JOIN child c ON c.id = p.child_id WHERE ...
ORDER BY c.id ASC;
Pay attention to ORDER BY clause. If I have c.id index is not involved and the search is slow. If I have ORDER BY p.child_id it works much faster.
I tried to use
Sort newSort = pageable.getSort().and(new Sort(Sort.Direction.ASC, "child"));
but it does not work as expected.
Entity:
public class ParentObject {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "child_id", referencedColumnName = "id")
private ChildObject child;
}
I can't replace this by native SQL, because this search specification contains 30+ if/else statements and it will take a lot of time to rewrite the code.
How can I solve this problem? Thanks in advance for your answers.
Finally, I found a solution. As we know, ORM works with database thru entities.
All my entities have String (UUID) values as ids. So, I have added the following code into my ParentObject:
public class ParentObject {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "child_id", referencedColumnName = "id")
private ChildObject child;
#Column(name = "child_id", insertable = false, updatable = false)
private String childId;
}
As you see, here I have added simple field that contains id of child object. It's important to mark this field as insertable = false, updatable = false and we can't change this field in the code, this is read-only field. But this allows us to sort results by parent.child_id, not by child.id. If we need to replace child object, need just call setChild(newValue).
And finally, my search method now looks like:
public Page<ParentObject> search(Pageable pageable) {
Specification<ParentObject> specification = (root, cq, cb) -> {
Predicate p = cb.and(
cb.equals(root.get("child").get("id"), "someValue"),
// a lot of predicates appended by conditions
);
return p;
};
// IMPORTANT change in next line
Sort newSort = pageable.getSort().and(new Sort(Sort.Direction.ASC, "childId"));
pageable = new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), newSort)
Page<ParentObject> result = parentObjectRepository.findAll(specification, pageable);
return result;
}
And in result I have the following SQL query:
SELECT .... FROM parent p INNER JOIN child c ON c.id = p.child_id WHERE ...
ORDER BY p.child_id ASC;
Hope this would be useful for someone

Map new column from Spring Native query to entity

I have a case statement in my Native query where I am attempting to override a field in my entity.
SELECT i.id, i.ONE_TO_ONE_ID, i.ANOTHER, CASE(WHEN condition THEN 'YES' WHEN another_condition THEN 'NO' ELSE 'MAYBE' END) as word ....
I am using this with JpaRepository as a native query, with pagination.
When I run the native query against my db directly, the result set looks as though I expect.
| id_value | MAPPED_ENTITY_ID_value | another value | word_value (YES) |
When I run the native query from my JpaRepository, everything works there, except word is always null. I cant' seem to figure out how to map the additional String word result to a field in my Entity.
Is there a way to get this to map? Or will I have to create an entire #SqlResultSetMapping() for all of my fields coupled with a native query? (hoping not)
UPDATE: 1
I was generalizing above. Here is my Query.
#Query(
name = "listPagedMapping",
value = "SELECT DISTINCT i.ID, i.INSTANCE_ID, i.REGION, i.CNAME_STACK_ID, i.INSTANCE_STATE, i.IP_ADDRESS, i.EC2_ROLE_NAME, i.INSTANCE_OWNER, i.IS_MASTER, i.EC2_MASTER_ID, i.CNAME, i.EC2_START_TIMESTAMP, i.PRIVATE_DNS, i.INSTANCE_NAME, i.AUTO_TERMINATE, i.AUTO_TERMINATE_DATE, i.TERMINATION_ZONE, i.ADMIN_GROUP_AD_LDAP_ID, i.USER_GROUP_AD_LDAP_ID, (CASE WHEN i.INSTANCE_OWNER=:username THEN 'OWNER' WHEN i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'ADMIN' WHEN i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'USER' END) as PERMISSION FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))",
countQuery = "SELECT count(*) FROM (SELECT DISTINCT i.* FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))) as ug",
nativeQuery = true)
Page<Ec2Instance> findAllByPermissionUserAdminOrOwnerAndInstanceStateIn(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
}
Obviously a bit more complex.
I can get it to map to the entity field with using a named query, but then I loose all the default mappings:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(exclude={"masterNode", "workers", "associatedBuckets"})
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMapping(
name="listPagedMapping",
columns = {
#ColumnResult(name = "permission", type = String.class)
}
)
#NamedNativeQuery(
name = "listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultSetMapping = "listPagedMapping"
)
public class Ec2Instance {
....
private String permission;
#column(name = "INSTANCE_ID")
private String instanceId;
#ManyToOne
#JoinColumn(name = "EC2_MASTER_ID")
private Ec2Instance masterNode;
#Setter(AccessLevel.NONE)
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "WORKER_EC2_NODES", joinColumns = { #JoinColumn(name = "EC2_MASTER_ID") }, inverseJoinColumns = {
#JoinColumn(name = "ID") })
private Set<Ec2Instance> workers = new HashSet<>();
... More fields ..
}
I guess, I am hoping there is a way to provide a single mapping on-top of the default mapping that is done by ORM. The above code results in only a pageable of Content PERMISSION, rather than the whole entity + permission.
UPDATE: 2
Ok, so I am getting closer... Seems by removing the #ColumnResult I do get the default mapping, plus the PERMISSION field mapped over! Looks like this:
#SqlResultSetMapping(
name="listPagedMapping"
)
The last issue is it does not accept my CountQuery, and causes my tests to fail whenever a Pagination Query results with multiple pages. Looks like Spring try's to come up with its own CountQuery, which is not correct.
UPDATE: 3
To finish this off, looks like I can provide the Count Query as described here: Spring Data - Why it's not possible to have paging with native query
I will give this a go and update back.
I never got this to work quite how I wanted. I am sure I could by mapping my entire entity, but, that would have been painstaking. I ended up solving this by using NamedNativeQueries, with mapping for the additional Column as a result of my Case statement. My entity class is now annotated like:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(callSuper = false)
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMappings({
#SqlResultSetMapping(
name = "listPagedMapping",
entities = {
#EntityResult(
entityClass = Ec2Instance.class
)
},
columns = {#ColumnResult(name = "permission", type = String.class)}
),
#SqlResultSetMapping(name = "listPagedMapping.count", columns = #ColumnResult(name = "cnt"))
})
#NamedNativeQueries({
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultClass = Ec2Instance.class,
resultSetMapping = "listPagedMapping"
),
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances.count",
resultSetMapping = "listPagedMapping.count",
query = ACCESSIBLE_QUERY_COUNT
)
})
We also dont need the permission field in this entity anymore. I removed that.
Then in my Repository:
Page<Object[]> listAccessibleInstances(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
Thats it! Now the result of my case statement is returned with each entity.
Object[0] = original, default mapped entity.
Object[1] = permission

Why org.hibernate.TransientObjectException during join/select?

Trying to wrap my head around Criteria API (ouch). I have 3 classes: Devices, Offices, and SiteCodes. all joined
Devices.java
private Offices office;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "office_id")
public Offices getOffice() {
return office;
}
public void setOffice(Offices office) {
this.office = office;
}
Offices.java:
private List<SiteCodes> siteCodes;
#ManyToMany(cascade = CascadeType.PERSIST)
#JoinTable(name = "site_code_map", joinColumns = {
#JoinColumn(name = "office_id") }, inverseJoinColumns = {
#JoinColumn(name = "site_code_id") })
#OrderBy("siteCode ASC")
public List<SiteCodes> getSiteCodes() {
return this.siteCodes;
}
public void setSiteCodes(List<SiteCodes> siteCodes) {
this.siteCodes = siteCodes;
}
SiteCodes.java
private id;
private String siteCode;
<getters and setters>
I'm trying to find Devices.devId using the site code. The sql would look like this:
SELECT d.dev_id
FROM devices d, offices o, site_code_map s, site_codes ss
WHERE d.office_id=o.office_id
AND s.office_id=o.office_id
AND s.site_code_id=ss.site_code_id
AND ss.`site_code`='0S21'
I'm trying to use joins, but don't quite get how to do it. I got the following to compile:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Integer> cq = cb.createQuery(Integer.class);
Root<Devices> d = cq.from(Devices.class);
Join<Devices, Offices> join1 = d.join(Devices_.office);
ListJoin<Offices,SiteCodes> join2 = join1.join(Offices_.siteCodes);
I couldn't figure how to use join2 so I used join1 and a path like this
ParameterExpression<SiteCodes> p = cb.parameter(SiteCodes.class, "sitecode");
Predicate pr = cb.isMember(p, join1.get(Offices_.siteCodes));
cq.where(pr);
and eventually
TypedQuery<Integer> tq = em.createQuery(cq);
<set parameter here to a SiteCode object>
List<Integer> idList = tq.getResultList();
It produces sql like the following. That subquery doesn't belong - I want it to use a join and anyway it throws a TransientObjectException even though all I'm doing is selecting:
select
devices0_.dev_id as col_0_0_
from
devices devices0_
inner join
offices offices1_
on devices0_.office_id=offices1_.office_id
inner join
site_code_map sitecodes2_
on offices1_.office_id=sitecodes2_.office_id
inner join
site_codes sitecodes3_
on sitecodes2_.site_code_id=sitecodes3_.site_code_id
where
? in (
select
sitecodes4_.site_code_id
from
site_code_map sitecodes4_
where
offices1_.office_id=sitecodes4_.office_id
And anyway it also throws this error (not including whole stack trace unless sopmeone wants to see it):
Caused by: org.hibernate.TransientObjectException: object references an
unsaved transient instance - save the transient instance before
flushing: dne.nmst.dac.model.SiteCodes
This confused me because I wasn't doing any saving, just selecting.
How do I get this to work? I want the criteria constructed properly and of course to not have an error occur.
I got this worked out. Hope it helps someone else. Still don't know why I got the error it did, but I figured out the join's and that made the query work correctly
Joins:
Root<Devices> d = cq.from(Devices.class);
Join<Devices, Offices> join1 = d.join(Devices_.office);
ListJoin<Offices,SiteCodes> join2 = join1.join(Offices_.siteCodes);
Parameter expression and Predicate:
ParameterExpression<String> p = cb.parameter(String.class, "sitecode");
Predicate pr = cb.equal(join2.get(SiteCodes_.siteCode),p);
Produced SQL like this:
select
devices0_.dev_i a col_0_0
from
devices devices0
inner join
office offices1
o devices0_.office_id=offices1_.office_i
inner join
site_code_map sitecodes2
o offices1_.office_id=sitecodes2_.office_i
inner join
site_code sitecodes3
o sitecodes2_.site_code_id=sitecodes3_.site_code_i
where
sitecodes3_.site_code=?

org.hibernate.hql.internal.ast.QuerySyntaxException: writes is not mapped

I set up a Database Application with Spring an Hibernate and I'm using a Many-To-Many-Relationship.
Here's the code:
Author.java
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name = "writes", joinColumns = {#JoinColumn(name = "authorId")}, inverseJoinColumns = {#JoinColumn(name = "publicationId")})
private Set<Publication> publications = new HashSet<Publication>();
Publication.java
#ManyToMany(mappedBy = "publications")
private Set<Author> authors = new HashSet<Author>();
these lines of code generate a connection-table named writes, but when I try to run a query over all Tables int gives me the error, named above.
this is the method, that schould run the query:
#Transactional
public List<Author> findAuthorByLastname(String lastName) {
String hql = "from Author a, Publication p, writes w where a.id = w.authorId and p.id = w.publicationId and a.lastname = :lastName";
Query q = sessionFactory.getCurrentSession().createQuery(hql);
q.setParameter("lastName", lastName);
List<Author> result = q.list();
return result;
}
You don't need to mention connection table explicitly in HQL queries (moreover, you even cannot mention them - only mapped entities). You need to use join on mapped relationships instead.
Also, it's not quite clear what you want to achieve.
If you want to fetch Authors with eagerly filled collections of their Publications in one query, use join fetch:
select distinct(a) from Author a join fetch a.publications where a.lastName = :lastName
If you want to fetch a list of pairs (Author, Publication), use regular join:
select a, p from Author a join a.publications p where a.lastName = :lastName

Resources