JPA Criteria Query: how to automatically create LEFT JOIN instead of WHERE conditions - jpa

I have two entity:
public class public class Person implements Serializable {
private static final long serialVersionUID = -8729624892493146858L;
#Column(name="name")
private String name;
...
#JoinColumn(name = "idcity",referencedColumnName = "id",nullable = true)
#ManyToOne(targetEntity = City.class, fetch = FetchType.EAGER)
private City city
...
}
and the related entity (extract):
public class City{
Long id;
String name;
...
}
Now i'm creating a criteria query in a standard way, querying the Person class:
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(selectClass);
Root<T> root = query.from(this.entityClass);
Selection selezioni[] = new Selection[selections.length];
for(int i=0; i< selections.length; i++){
selezioni[i] = CriteriaHelper.getField(selections[i], cb, root);
}
query.select(cb.construct(selectClass, selezioni));
where entityClass is Person and selection and selectClass are used to compile the SELECT clause. In the select i've person.city.name field.
This system create a query with where clause:
select person.name, ..., city.name from person, city WHERE person.idcity = city.id...
but city is not required, so the records without city are not fetched.
Without changing all my automatic system, does exists a simpler way to force the use on LEFT JOIN for the relationship than adding a system to create root.join("field",LEFT)?
Note: the method CriteriaHelper.getField() return a Path starting from the root object

Related

Sorting on embedded entity name in hibernate search

I have the following Entity definitions.
public class Order {
#Id
#DocumentId
private Long id;
#Field
#IndexedEmbedded(includePaths = {"name"})
#OneToOne
#JoinColumn(name = "ACCOUNT_ID")
private Account account;
// the rest are omitted for brevity purpose
}
public class Account {
#Id
#DocumentId
private Long id;
#SortableField(forField = "name_Sort")
#Field(name = "name_Sort", store = Store.YES, normalizer= #Normalizer(definition = SearchConstants.LOWER_CASE_NORMALIZER))
#Column(name = "NAME")
private String name;
}
If I search on Order and want to have the search results sorted by account name, is there good way of doing so possibly using the embedded indexed annotation? I know we can do it by adding an extra string field in Order that is called accountName, then just add sorting annotation on top of that. Is it possible to achieve this without specifying the sorting annotation in Order but just use the sorting annotation that is already defined in Account?
Change this:
#IndexedEmbedded(includePaths = {"name"})
To this:
#IndexedEmbedded(includePaths = {"name", "name_Sort"})
Then you can use the field account.name_Sort for sorts on orders:
QueryBuilder builder = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity( Order.class ).get();
Query luceneQuery = /* ... */;
FullTextQuery query = s.createFullTextQuery( luceneQuery, Order.class );
query.setSort(
builder.sort().byField("account.name_Sort").createSort()
);
List results = query.list();

Crieria API query using criteriabuilder.construct with a non existing relationship

Given this very simple DTO:
#Entity
public class Employee implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
#OneToOne
private Employee boss;
}
I'd like to make a query that gathers all employee names and their boss' id, put in a nice clean POJO:
public class EmployeeInfo {
private String name;
private Long bossId;
public EmployeeInfo(String name, Long bossId) {
this.name = name;
this.bossId = bossId;
}
}
This query should be of use:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<EmployeeInfo> query = cb.createQuery(EmployeeInfo.class);
Root<Employee> root = query.from(Employee.class);
query.select(
cb.construct(EmployeeInfo.class,
root.get("name").as(String.class),
root.get("boss").get("id").as(Long.class)));
result = em.createQuery(query).getResultList();
When a bossId is present in the employee column this works just fine. But when no boss id is set the record will be completly ignored. So how do i treat this non existing boss relation as null or 0 for the construct/multiselect?
In pure SQL it is easy:
SELECT name, COALESCE(boss_id, 0) FROM EMPLOYEE;
But for the love of god i cannot make the criteria api do this.
cb.construct(EmployeeInfo.class,
root.get("name").as(String.class),
cb.coalesce(root.get("boss").get("id").as(Long.class), 0L)));
The problem is that root.get("boss") generate query with cross join like this from Employee employee, Employee boss where employee.boss.id=boss.id. So records where employee.boss.id is null are ignored.
To solve the problem you should use root.join("boss", JoinType.LEFT) instead of root.get("boss")

Spring projections select collection

I am attempting to have a station projection include a list of associated logos. Below is my domain:
#Table(name = "Station")
public class Station implements Serializable {
#Id
#Column(name = "Id")
private int id;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "station")
private Set<Logo> logos;
}
The #OneToMany associated logos:
#Table(name = "Logo")
public class Logo {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Transient
private String fullUrl; // calculated
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "StationId", nullable = false)
private Station station;
}
My repository and query is as follows:
#Query(value = "SELECT s.id AS Id, s.logos As Logos FROM Station s JOIN s.users su WHERE su.username = ?1")
Collection<StationListViewProjection> findStationsByUsername(String username);
My station projection expects the Id and a list of logoProjections
#Projection(name = "StationListViewProjection", types = Station.class)
public interface StationListViewProjection {
int getId();
Set<LogoProjection> getLogos();
}
The logoProjection only needs the url
#Projection(name = "LogoProjection", types = Logo.class)
public interface LogoProjection {
String getFullUrl();
}
When i execute my query I get a strange response:
MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'as col_5_0_, . as col_6_0_, stationlog3_.id as id1_20_0_'
If I understand this
#Transient
private String fullUrl; // calculated
correct, your fullUrl gets calculated inside your java code and more important, it doesn't have a matching column in the database. You can't use such field in projections directly. You might be able to use an Open Projection and specify the calculation to obtain the fullUrl using a #Value annotation.

JPQL: How to join via #ElementCollection with #MapKeyJoinColumn

I have problems creating the correct JPQL query for joining through the following tables:
While between GROUPS and USERS there is a conventional #ManyToMany mapping table, DOCUMENTS_GROUPS is what causes the trouble. As you can see in the following entity, I want the relationship between DOCUMENTS and GROUPS to be mapped as a Map containing the access_mode (which works just fine except for the query):
#Entity
#Table(name = "DOCUMENTS")
#NamedQueries({
#NamedQuery(
name = "Documents.findAccessibleByUser",
query = "SELECT d FROM Document d INNER JOIN d.groups g INNER JOIN KEY(g).members m WHERE m.id = :userId"
)
})
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#ElementCollection
#CollectionTable(name = "DOCUMENTS_GROUPS", joinColumns = {#JoinColumn(name = "document_id")})
#MapKeyJoinColumn(name = "group_id")
#Column(name = "access_mode")
#Enumerated(EnumType.STRING)
private Map<Group, AccessMode> groups = new HashMap<>();
/* ... */
}
With Group being rather normal:
#Entity
#Table(name = "GROUPS")
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(length = 255)
private String name;
#ManyToMany
#JoinTable(name = "USERS_GROUPS", //
joinColumns = {#JoinColumn(name = "group_id")}, //
inverseJoinColumns = {#JoinColumn(name = "user_id")} //
)
private Set<User> members = new HashSet<>();
/* ... */
}
My question is now: How do I need to modify the second JOIN in my JPQL query?
SELECT d FROM Document d
INNER JOIN d.groups g
INNER JOIN KEY(g).members m
WHERE m.id = :userId
is syntactically wrong (unexpected KEY after INNER JOIN).
Of course, I have already tried a plain INNER JOIN g.members m, but since we're dealing with a Map<Group, AccessMode>, this fails with cannot dereference scalar collection element: members.
I was facing the same problem with a simple key-value Map<String, String> like:
#Entity Item.java
#ElementCollection
#MapKeyColumn(name = "name")
#Column(name = "value")
#CollectionTable(indexes = #Index(columnList = "value"))
private Map<String, String> attributes = new HashMap<>();
Joining the attributes was possible:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr");
but not querying fields:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr.value = 'something'");
I debugged the Hibernate internals and found out that the alias attr is already resolved to the value (e.attributes.value), so the only thing you can do here is:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr = 'something'");
But I did not find any documentation or JPQL examples pointing that out. The behaviour is is useless in my case, because I want to have conditions for both key and value. Thats why I migrated to a foreign entity collection with key mapping and composite primary key. Its way more complicated but works as expected.
The composite key entity to prevent single primary keys
#Embeddable
public class ItemAttributeName implements Serializable {
private String name;
#ManyToOne
#JoinColumn(nullable = false)
private Item item;
// Empty default constructor is important
public ItemAttributeName() {
}
public ItemAttributeName(Item item, String name) {
this.item = article;
this.name = name;
}
}
The real attribute entity
#Entity
public class ItemAttribute {
#EmbeddedId
private ItemAttributeName id;
private String value;
// Empty default constructor is important
public ItemAttribute() {
}
public ItemAttribute(Item item, String name) {
this.id = new ItemAttributeName (item, name);
}
public String getValue() {
return value;
}
}
#Entity Item.java
#OneToMany(mappedBy = "id.item",cascade = CascadeType.PERSIST)
#MapKeyColumn(name = "name")
public Map<String, ItemAttribute> attributes = new HashMap<>();
Creating entities:
Item item = new Item ();
ItemAttribute fooAttribute = new ItemAttribute(item, "foo");
fooAttribute.setValue("356");
item.attributes.put("foo", fooAttribute);
Querying entities:
Query query = em.createQuery("SELECT i FROM Item i JOIN i.attributes attr WHERE attr.id.name = 'foo' AND attr.value='bar'");
List<Item> resultList = query.getResultList();
System.out.println(resultList.get(0).attributes.get("foo").getValue());
Prints out: bar

JPA - Finding nested entities using combined criterias

I have an entity named FileEntity which contains a list of reports of the type ReportEntity.
FileEntity has an field which determines, which user has created the file containing a number of reports.
#Entity
public class FileEntity {
#Id
private Long id;
#JoinColumn(name = "user")
#ManyToOne(optional = true)
#NotNull
private User user;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
orphanRemoval = true)
#NotNull
private List<Report> reports = new ArrayList<>(5);
...
}
#Entity
public class Report {
#Id
private Long id;
...
}
I am currently trying to fetch a single report with a given report ID and the ID of the person who issued the file containing the report. The combination is unique, so it should only return one report for a certain combination of report and user ID. But I am unable to retrieve a single result using the following criteria:
public Report findReportByUserAndReportId(Long reportId, Long userId) {
Objects.nonNull(reportId);
Objects.nonNull(userId);
try {
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<Report> cq = cb.createQuery(Report.class);
final Root<FileEntity> fileEntity = cq.from(FileEntity.class);
final Root<Report> report = cq.from(Report.class);
final Join<FileEntity, Report> join = fileEntity.join(FileEntity_.reports);
final Predicate[] predicates = new Predicate[]{
cb.equal(join.get("id"),
userId),
cb.equal(join.get(Report_.id),
reportId)};
cq.select(report).where(predicates);
return entityManager.createQuery(cq).getSingleResult();
} catch (NoResultException |
NonUniqueResultException ne) {
LOG.log(Level.WARNING,
"Could not find report: {0}",
ne.getMessage());
}
return null;
}
Has someone an idea what I am doing wrong?
First, don't use multiple roots here, because they generate a carthesian product of all the elements, without joining them. Use joins or Paths instead. Second, in the first predicate. join object denotes a Path of reports, not a user, therefore it doesnt' make sense to look for the userid there.
Root<FileEntity> fileEntity = cq.from(FileEntity.class);
Path<User> user = fileEntity.get(FileEntity_.user);
Join<FileEntity, Report> reports = fileEntity.join(FileEntity_.reports);
Predicate[] predicates = new Predicate[]{
cb.equal(user.get(User_.id),
userId),
cb.equal(reports.get(Report_.id),
reportId)};