I have two JPA Entities:
@Entity
@Table(name = "registry_group")
public class RegistryGroupEntity {
@Id
@SequenceGenerator(name="registry_groups_gen", sequenceName="registry_groups_id_seq", allocationSize = 1)
@GeneratedValue(generator="registry_groups_gen")
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "registryGroup")
private Collection<ServiceEntity> services;
}
and
@Entity
@Table(name = "services")
public class ServiceEntity {
@Id
@SequenceGenerator(name="service_gen", sequenceName="services_id_seq", allocationSize = 1)
@GeneratedValue(generator="service_gen")
private Integer id;
@Column(nullable = false, length = 100)
private String name;
}
I'd like to represent the selection as RegistryGroupRow
class object:
public class RegistryGroupRow {
private Long id;
private String name;
private String serviceNames;
public RegistryGroupRow(Long id, String name, List<String> serviceNames) {
this.id = id;
this.name = name;
this.serviceNames = serviceNames;
}
}
I have a function like that:
public List<RegistryGroupRow> getRegistryGroupRows(RegistryGroupFilter filter, Integer offset, Integer limit) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<RegistryGroupRow> c = cb.createQuery(RegistryGroupRow.class);
Root<RegistryGroupEntity> registryGroup = c.from(RegistryGroupEntity.class);
c.multiselect(registryGroup.get(RegistryGroupEntity_.id),
registryGroup.get(RegistryGroupEntity_.name),
registryGroup.get(RegistryGroupEntity_.services) //problem is here
);
It wouldn't work, as RegistryGroupEntity_.services
is a collection of ServiceEntity
, but I need a concatenation on ServiceEntity._name
here. How I can call something like cb.concat(registryGroup.get(RegistryGroupEntity_.services).get(ServiceEntity._name))
for the last parameter of the RegistryRowGroup
constructor? How to make CriteriaBuilder
to run through the collection's objects specific field and collect/concat/sum its values?
For concat I'd like to concat with separator to String
. For collect I'd like to collect into List<String>
.
Approach
One way to solve this issue is to use a native sql function to do the aggregation of the service names.
E.g. in H2 database, there is a function named LISTAGG
which takes 2 parameters, first the field to aggregate and second the delimiter. I am certain whichever database you are using have something similar to aggregate data; e.g. in postgres there is string_agg(expression, delimiter)
.
Prerequisites
The function from the database needs to be registered and mapped to a name in hibernate. A custom dialect can be created or a MetadataBuilderContributor
can used to achieve the same. The MetadataBuilderContributor
is neaty so here we are..
// Registering the function LISTAGG under the name 'listagg'
public class H2SqlFunctionContributor implements MetadataBuilderContributor {
@Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction(
"listagg",
new StandardSQLFunction("LISTAGG", StandardBasicTypes.STRING)
);
}
}
Update the spring properties file to include the MetadataBuilderContributor
usig the fully qualified name of the class as such;
spring.jpa.properties.hibernate.metadata_builder_contributor = com.stackoverflow.q75989598.H2SqlFunctionContributor
Update the class RegistryGroupRow
to use this constructor;
public RegistryGroupRow(Long id, String name, String serviceNames) {
this.id = id;
this.name = name;
this.serviceNames = serviceNames;
}
Querying part
Assuming RegistryGroupEntity
looks as such:
public class RegistryGroupEntity {
@Id
private long id;
// using 'groupName' to makes it distinctive in the queries to come
@Column
private String groupName;
@OneToMany(mappedBy = "registryGroupEntity")
List<ServiceEntity> serviceEntities;
}
And ServiceEntity
looks as such:
public class ServiceEntity {
@Id
private long id;
@Column
private String name;
@JoinColumn(name = "registry_group_id")
@ManyToOne
private RegistryGroupEntity registryGroupEntity;
}
The quering will be as follows;
// the usual stuffs
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<RegistryGroupRow> criteriaQuery = criteriaBuilder.createQuery(RegistryGroupRow.class);
Root<RegistryGroupEntity> registryGroup = criteriaQuery.from(RegistryGroupEntity.class);
// Create a join on the entity ServiceEntity using the defined relationship of 'serviceEntities'
Join<RegistryGroupEntity, ServiceEntity> services = registryGroup.join("serviceEntities");
criteriaQuery.multiselect(
registryGroup.get("id"),
registryGroup.get("groupName"),
// use the criteria builder to call the function named 'listagg' and pass parameters in it
criteriaBuilder.function("listagg", String.class, services.get("name"), criteriaBuilder.literal(",")).alias("serviceNames")
);
// using an aggregate function usually requires a group by to be done
criteriaQuery.groupBy(registryGroup.get("id"), registryGroup.get("groupName"));
RegistryGroupRow registryGroupRow = entityManager.createQuery(criteriaQuery).getSingleResult();
Result Query generated by hibernate:
select
registrygr0_.id as col_0_0_,
registrygr0_.group_name as col_1_0_,
LISTAGG(serviceent1_.name, ',') as col_2_0_
from
registry_group registrygr0_
inner join services serviceent1_ on
registrygr0_.id = serviceent1_.registry_group_id
group by
registrygr0_.id ,
registrygr0_.group_name
Result of @ToString
for the RegistryGroupRow
retrieved.
RegistryGroupRow(id=1, name=RegistryGroup1, serviceNames=ServiceName1,ServiceName2)