How to use a #ConstructorResult with a Set<SomeEnum> field - jpa

I'm trying to create a #NamedNativeQuery with a #ConstructorResult for a class that has a field with a Set of enum values.
VeterinarianJPA.java:
#Entity
#Table(name = "veterinarians")
#Setter
#Getter
#NoArgsConstructor
#NamedNativeQueries({
#NamedNativeQuery(
name = VeterinarianJPA.FIND_ALL_VETS,
query = "SELECT v.id, v.name, vs.specialisations " +
"FROM veterinarians v " +
"JOIN veterinarian_specialisations vs ON v.id = vs.vet_id",
resultSetMapping = VeterinarianJPA.VETERINARIAN_RESULT_MAPPER
)})
#SqlResultSetMappings({
#SqlResultSetMapping(
name = VeterinarianJPA.VETERINARIAN_RESULT_MAPPER,
classes = #ConstructorResult(
targetClass = Veterinarian.class,
columns = {
#ColumnResult(name = "id", type = Long.class),
#ColumnResult(name = "name"),
#ColumnResult(name = "specialisations", type = Set.class)
}
)
)})
class VeterinarianJPA {
static final String FIND_ALL_VETS = "net.kemitix.naolo.gateway.data.jpa.findAllVets";
static final String VETERINARIAN_RESULT_MAPPER = "net.kemitix.naolo.gateway.data.jpa.Veterinarian";
#Id
#GeneratedValue
private Long id;
private String name;
#ElementCollection
#Enumerated(EnumType.STRING)
#CollectionTable(
name = "veterinarian_specialisations",
joinColumns = #JoinColumn(name = "vet_id")
)
private final Set<VetSpecialisation> specialisations = new HashSet<>();
}
Veterinarian.java:
public final class Veterinarian {
private Long id;
private String name;
private Set<VetSpecialisation> specialisations;
public Veterinarian() {
}
public Veterinarian(final long id,
final String name,
final Set<VetSpecialisation> specialisations) {
this.id = id;
this.name = name;
this.specialisations = new HashSet<>(specialisations);
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public Set<VetSpecialisation> getSpecialisations() {
return new HashSet<>(specialisations);
}
}
VetSpecialisation.java:
public enum VetSpecialisation {
RADIOLOGY,
DENTISTRY,
SURGERY
}
When I attempt to execute the named query:
entityManager.createNamedQuery(VeterinarianJPA.FIND_ALL_VETS, Veterinarian.class)
.getResultStream()
I get the following exception:
java.lang.IllegalArgumentException: Could not locate appropriate constructor on class : net.kemitix.naolo.entities.Veterinarian
at org.hibernate.loader.custom.ConstructorResultColumnProcessor.resolveConstructor(ConstructorResultColumnProcessor.java:92)
at org.hibernate.loader.custom.ConstructorResultColumnProcessor.performDiscovery(ConstructorResultColumnProcessor.java:45)
at org.hibernate.loader.custom.CustomLoader.autoDiscoverTypes(CustomLoader.java:494)
at org.hibernate.loader.Loader.processResultSet(Loader.java:2213)
at org.hibernate.loader.Loader.getResultSet(Loader.java:2169)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:1930)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:1892)
at org.hibernate.loader.Loader.scroll(Loader.java:2765)
at org.hibernate.loader.custom.CustomLoader.scroll(CustomLoader.java:383)
at org.hibernate.internal.SessionImpl.scrollCustomQuery(SessionImpl.java:2198)
at org.hibernate.internal.AbstractSharedSessionContract.scroll(AbstractSharedSessionContract.java:1058)
at org.hibernate.query.internal.NativeQueryImpl.doScroll(NativeQueryImpl.java:217)
at org.hibernate.query.internal.AbstractProducedQuery.scroll(AbstractProducedQuery.java:1462)
at org.hibernate.query.internal.AbstractProducedQuery.stream(AbstractProducedQuery.java:1486)
at org.hibernate.query.Query.getResultStream(Query.java:1110)
I expect that the SQL is returning multiple rows for a multi-valued Set rather than a single value, which is causing the constructor not to match. How do I change the SQL to produce the correct input to the constructor, or is there another configuration change I need to make?

Well, I'm not sure if that's even possible in the way you want to to this. But you can use LISTAGG function on specialisations table to inline the specialisations with veterinarians by using some kind of separator.
So the query should look like this:
SELECT v.id, v.name
(SELECT LISTAGG(vs.type, ';')
WITHIN GROUP (ORDER BY vs.type)
FROM veterinarian_specialisations vs
WHERE vs.vet_id = v.id) specialisations
FROM veterinarians v;
The query will return veterinarian and his semicolon separated specialisations:
1 NAME DENTISTRY;RADIOLOGY
And then in your Veterinarian class constructor you must remap String result back to Set of VetSpecialisation. I used Java 8 stream api just for convenience.
public final class Veterinarian {
private Long id;
private String name;
private Set<VetSpecialisation> specialisations;
public Veterinarian() {
}
public Veterinarian(final long id,
final String name,
final String specialisations) {
this.id = id;
this.name = name;
this.specialisations = Arrays.asList(specialisations.split(";"))
.stream()
.map(VetSpecialisation::valueOf) //Map string to VetSpecialisation enum.
.collect(Collectors.toSet());
}

Related

Keycloack does not detect #SqlResultSetMapping in custom provider jar

I am trying to create a custom query in Keycloak in my custom provider Jar. The annotation below is not detected. I had to resort to use the call that returns a list of objects from which I can extract the columns from the result set. However I would like to use the SqlResultSetMapping interface. Here is the code that I am using.
#MappedSuperclass
#SqlResultSetMapping( name = "GroupTabularDataMapping",
classes = {
#ConstructorResult(
targetClass = GroupTabularData.class,
columns = {
#ColumnResult(name = "id"),
#ColumnResult(name = "name"),
#ColumnResult(name = "updated_by"),
#ColumnResult(name = "updated_by_email"),
#ColumnResult(name = "last_updated"),
#ColumnResult(name = "count",type = Long.class)
})
})
public class GroupTabularData {
private String id;
private String name;
private String updatedBy;
private String updatedByEmail;
private String lastUpdated;
private BigInteger count;
public GroupTabularData(String id, String name, String updatedBy, String updatedByEmail,
String lastUpdated, BigInteger count) {
this.id = id;
this.name = name;
this.updatedBy = updatedBy;
this.updatedByEmail = updatedByEmail;
this.lastUpdated = lastUpdated;
this.count = count;
}
public GroupTabularData(Object objArr) {
Object[] arr = (Object[]) objArr;
this.id = arr[0].toString();
this.name = arr[1].toString();
this.updatedBy = arr[2].toString();
this.updatedByEmail = arr[3].toString();
this.lastUpdated = arr[4].toString();
this.count = (BigInteger)arr[5];
}
}
This call works:
Object resultData = q.getResultList().stream().map(
objArr -> new GroupTabularData(objArr)).collect(Collectors.toList());
But this one does not:
Query q = em.createNativeQuery(GROUP_TABULAR_QUERY, "GroupTabularDataMapping");
List<GroupTabularData> resultData = q.getResultList();
I imagine that the reason is because I need to add my class to the scanning list of JPA but I don't know which file I need to update or if it is some other configuration. I appreciate any leads.

How to convert Integer param which can be 'null' to '0' when selecting data with Spring Data JPA

Assume we have entity Animal. There are animals in DB with 'amount' = null, it's a valid case to save animal without the 'amount'.
Is there a way to convert field 'amount' to 0 in case it's null in query?
The simplest workaround seems to convert amount null to '0' earlier
when saving, but it's not allowed.
As another workaround we can do this mapping to '0' after fetching
it from the repository. When sorting by amount in asc order, null values will be at the beginning, in desc order they will be at the end. And after
converting to '0' everything will be at the right place. But it seems that can cause problems with pagination in future
What is the proper way to do it in Query?
Spring Data Jpa 2.2.9.RELEASE, Postgresql 42.2.16.
#Repository
public interface AnimalRepository extends JpaRepository<AnimalEntity, Long> {
#Query(value = "SELECT animal FROM AnimalEntity animal" +
" WHERE animal.ownerId = :ownerId" +
" and function('replace', upper(animal.name), '.', ' ') like function('replace', upper(concat('%', :name,'%')), '.', ' ') "
)
Page<AnimalEntity> findAllLikeNameAndOwnerSorted(String ownerId, String name, Pageable pageable);
}
#Entity
#Table(name = "animal")
public class AnimalEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer amount;
private String name;
private String ownerId;
}
UPDATE
Also important to mention. The solution I suggested with replacing nulls with zero is incorrect, because of the different null ordering in Postgresql and HSQLDB.
But it will work in tests, if you're using HSQLDB.
Animal entities in DB test sample: [
Animal(name=Cat, amount=599999.99),
Animal(name=Dog, amount=null),
Animal(name=John, amount=5000)
]
Hsqldb amount desc query result:
[
Animal(name=Cat, amount=599999.99),
Animal(name=John, amount=5000),
Animal(name=Dog, amount=null)
]
Postgresql amount desc query result:
[
Animal(name=Dog, amount=null)
Animal(name=Cat, amount=599999.99),
Animal(name=John, amount=5000)
]
The JPA supports the COALESCE function. Thus you can set up the desired value via this function.
SELECT COALESCE(amount,0) AS desiredAmount FROM AnimalEntity animal
The code should look like this:
#Entity
#Table(name = "animal")
public class AnimalEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer amount;
public AnimalEntity() {
}
public AnimalEntity(Integer amount, String name) {
this.amount = amount;
this.name = name;
}
public Long getId() {
return id;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And the repository:
#Repository
public interface AnimalRepository extends JpaRepository<AnimalEntity, Long> {
#Query(
value = "SELECT animal.id AS id, COALESCE(animal.amount,0) AS amount, UPPER(animal.name) AS name FROM animal animal WHERE animal.name = :name",
nativeQuery = true)
Page<AnimalEntity> findAllLikeNameAndOwnerSorted(String name, Pageable pageable);
}
Also I have prepared the test:
#SpringBootTest
class AnimalRepositoryTest {
#Autowired
private AnimalRepository animalRepository;
#Test
void findAllLikeNameAndOwnerSorted() {
AnimalEntity animalEntity = new AnimalEntity(null, "dog");
animalRepository.save(animalEntity);
AnimalEntity animalEntity2 = new AnimalEntity(1, "CAT");
animalRepository.save(animalEntity2);
System.out.println(animalEntity2.getId());
Pageable sortedByName = PageRequest.of(0, 3, Sort.by("id"));
Page<AnimalEntity> animals = animalRepository.findAllLikeNameAndOwnerSorted("dog", sortedByName);
animals.forEach(System.out::println);
}
}
You can check the commit: https://gitlab.com/chlupnoha/meth/-/commit/76abbc67c33b2369231ee89e0946cffda0460ec9 - it is experiment project.

JPA with hibernate implementation is generating wrong named query

I configured JPA with spring. I am using spring 4.
I have an entity
#Entity
#NamedQueries({
#NamedQuery(name="PartnerCourseMapping.findByPartnerCourseIdAndHandlerName", query="select pm from PartnerCourseMapping pm where pm.partnerCourseId=:partnerCourseId and pm.handlerName=:handlerName")
})
#Table(name="PARTNER_COURSE_MAPPING")
public class PartnerCourseMapping implements Serializable {
private static final long serialVersionUID = 1L;
#Id
protected Long id;
#Column(name="COURSE_ID")
protected Long courseId;
#Column(name="PARTNER_COURSE_ID")
protected String partnerCourseId;
#Column(name="PARTNER_ID")
protected Integer partnerId;
#Column(name="PRODUCT_TYPE")
protected String productType;
#Column(name="HANDLER_NAME")
protected String handlerName;
//getters and setters
}
I have another entity which i defined like below
#Entity
#NamedNativeQueries({
#NamedNativeQuery(
name="ExternalCourse.findExternalCourseMappingByLearningSessionGuid",
query="SELECT PCM.*, LE.id AS LearnerEnrollmentId, LE.LEARNER_ID AS LearnerId "
+ "FROM LEARNINGSESSION LS "
+ "INNER JOIN LEARNERENROLLMENT LE ON LE.ID = LS.ENROLLMENT_ID "
+ "INNER JOIN PARTNER_COURSE_MAPPING PCM ON PCM.COURSE_ID = LE.COURSE_ID "
+ "WHERE LS.LEARNINGSESSIONGUID = :learningSessionGuid",
resultSetMapping="externalCourseMapping"
)
})
#SqlResultSetMappings({
#SqlResultSetMapping(
name="externalCourseMapping",
classes = {
#ConstructorResult(targetClass = ExternalCourse.class,
columns={
#ColumnResult(name = "ID", type=Long.class ),
// remaing ColumnResult
}
)
}
)
})
public class ExternalCourse extends PartnerCourseMapping /*implements Serializable*/ {
private Long learnerEnrollmentId;
private Long learnerId;
//default constructor
public ExternalCourse(Long id, Long courseId, String partnerCourseId, Integer partnerId, String productType,
String handlerName, Long learnerEnrollmentId, Long learnerId) {
this.id = id;
// remaing values
}
//getters and setters for learnerEnrollmentId and learnerId
}
Now I query PartnerCourseMapping.findByPartnerCourseIdAndHandlerName
TypedQuery<PartnerCourseMapping> query = entityManager.createNamedQuery("PartnerCourseMapping.findByPartnerCourseIdAndHandlerName", PartnerCourseMapping.class);
query.setParameter("partnerCourseId", paernerCourseId);
query.setParameter("handlerName", handlerName);
return getResult(query);
protected T getResult(TypedQuery<T> query) {
List<T> list = query.getResultList();
return CollectionUtils.isEmpty(list) ? null : list.get(0);
}
Hibernate is generating exception that
javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: could not extract ResultSet
...
Caused by: com.microsoft.sqlserver.jdbc.SQLServerException: Invalid column name 'learnerEnrollmentId'.
When I debug then I saw hibernate is generating query like below
select partnercou0_.id as id2_13_,
partnercou0_.COURSE_ID as COURSE_I3_13_,
partnercou0_.HANDLER_NAME as HANDLER_4_13_,
partnercou0_.PARTNER_COURSE_ID as PARTNER_5_13_,
partnercou0_.PARTNER_ID as PARTNER_6_13_,
partnercou0_.PRODUCT_TYPE as PRODUCT_7_13_,
partnercou0_.learnerEnrollmentId as learnerE8_13_,
partnercou0_.learnerId as learnerI9_13_,
partnercou0_.DTYPE as DTYPE1_13_
from PARTNER_COURSE_MAPPING partnercou0_ where partnercou0_.PARTNER_COURSE_ID=? and partnercou0_.HANDLER_NAME=?
I want to ask that why hibernate is including learnerEnrollmentId and learnerId column? I am passing the query name and query. If I refactor my code like below then I get the correct result
#Entity
#NamedNativeQueries({
#NamedNativeQuery(
name="ExternalCourse.findExternalCourseMappingByLearningSessionGuid",
...
resultSetMapping="externalCourseMapping"
)
})
#SqlResultSetMappings({
#SqlResultSetMapping(
name="externalCourseMapping",
classes = {
..
}
)
})
public class ExternalCourse implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private Long id;
private Long courseId;
private String partnerCourseId;
private Integer partnerId;
private String productType;
private String handlerName;
private Long learnerEnrollmentId;
private Long learnerId;
//default constructor
//constructor with all parameters
//getters and setters
}
Why I am getting exception when I am extending class? I am passing the query name. Why ?
Thanks

Using COUNT in JPQL Query

I have the following JPQL query:
List<DestinationInfo> destinations = em.createQuery("SELECT NEW com.realdolmen.patuva.dto.DestinationInfo(d.name, d.continent, MIN(t.departureDate), MIN(t.pricePerDay), COUNT(t.id))" +
" FROM Destination d, Trip t" +
" WHERE d.continent = :continent " +
" GROUP BY d.name, d.continent").setParameter("continent", searchedContinent).getResultList();
If I run this I get the error:
javax.ejb.EJBTransactionRolledbackException: org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate appropriate constructor on class [com.realdolmen.patuva.dto.DestinationsList]
If I leave out the COUNT(t.id) and remove that parameter from my DestinationInfo constructor it works fine. Why can't I map the COUNT(t.id) to my DestinationInfo DTO.
This is my DestinationInfo class:
public class DestinationInfo {
private String name;
private Continent continent;
private Date earliestDeparture;
private Integer totalNumTrips;
private BigDecimal lowestPrice;
public DestinationInfo(String name, Continent continent, Date earliestDeparture, BigDecimal lowestPrice, Integer totalNumTrips) {
this.name = name;
this.continent = continent;
this.earliestDeparture = earliestDeparture;
this.totalNumTrips = totalNumTrips;
this.lowestPrice = lowestPrice;
}
// getters and setters
}
Apparently COUNT(t.id) returns a number of type long. Changing the DestinationInfo class to the following makes it work:
public class DestinationInfo {
private String name;
private Continent continent;
private Date earliestDeparture;
private long totalNumTrips;
private BigDecimal lowestPrice;
public DestinationInfo(String name, Continent continent, Date earliestDeparture, BigDecimal lowestPrice, long totalNumTrips) {
this.name = name;
this.continent = continent;
this.earliestDeparture = earliestDeparture;
this.totalNumTrips = totalNumTrips;
this.lowestPrice = lowestPrice;
}
// getters and setters
}

Wrong parameter binding when using named queries with optional parameters in EclipseLink

I'm trying to do a database lookup using JPA with EclipseLink. My database is Oracle 11.2.0. I have the following entity classes defined:
#Entity
#Table(name = "BS_PROVIDERS")
public class BsProvider {
#Id
#Column(name = "GUID")
private String guid;
public String getGuid() { return guid; }
public void setGuid(String guid) { this.guid = guid; }
#Column(name = "NAME")
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
#Entity
#Table(name = "CP_PROVIDERS")
public class CpProvider{
#Id
#Column(name = "GUID")
private String guid;
public String getGuid() { return guid; }
public void setGuid(String guid) { this.guid = guid; }
#Column(name = "NAME")
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
#NamedQueries({
#NamedQuery(
name = Catalog.FIND_BY_CPP_AND_BSP,
query = "select c from Catalog c where (c.cpProvider = :cpProvider) and ( (:bsProvider IS NULL) or (c.bsProvider = :bsProvider))"
)
})
#Entity
#Table(name = "CATALOGS")
public class Catalog {
public static final String FIND_BY_CPP_AND_BSP = "Catalog.findByCppAndBsp";
public static final String CP_PROVIDER_PARAM = "cpProvider";
public static final String BS_PROVIDER_PARAM = "bsProvider";
#Id
#Column(name = "GUID")
private String guid;
public String getGuid() { return guid; }
public void setGuid(String guid) { this.guid = guid; }
#Column(name = "NAME")
private String name;
public String getName() { return name; }
public void setName() { this.name = name; }
#ManyToOne
#JoinColumn(name = "CPP_GUID")
private CpProvider cpProvider;
public CpProvider getCpProvider() { return cpProvider; }
public void setCpProvider(CpProvider cpProvider) { this.cpProvider = cpProvider; }
#ManyToOne()
#JoinColumn(name = "BSP_GUIG")
private BsProvider bsProvider;
public BsProvider getBsProvider() { return bsProvider; }
public void setBsProvider(BsProvider bsProvider) { this.bsProvider = bsProvider; }
}
Code that creates the query and sets the parameters:
TypedQuery<Catalog> catalogQuery = em.createNamedQuery(Catalog.FIND_BY_CPP_AND_BSP, Catalog.class);
catalogQuery.setParameter(Catalog.CP_PROVIDER_PARAM, cpProvider);
catalogQuery.setParameter(Catalog.BS_PROVIDER_PARAM, bsProvider);
List<Catalog> catalogList = catalogQuery.getResultList();
When the variable bsProvider is NULL, all parameters are registerd are registerd correctly according to the EclipseLink log:
SELECT GUID, NAME, BSP_GUIG, CPP_GUID FROM CATALOGS WHERE ((CPP_GUID = ?) AND ((? IS NULL) OR (BSP_GUIG = ?)))
bind => [18EC0EDB-21A4-4845-960A-D5D2BDAC7B87, null, null]
Otherwise when the variable bsProvider refers to an existing entity I get the following exception:
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Invalid column type
Error Code: 17004
Call: SELECT GUID, NAME, BSP_GUIG, CPP_GUID FROM CATALOGS WHERE ((CPP_GUID = ?) AND ((? IS NULL) OR (BSP_GUIG = ?)))
bind => [18EC0EDB-21A4-4845-960A-D5D2BDAC7B87, com.bssys.ebpp.dbaccess.model.BsProvider#5dbbe8df, 44E8F4BF-CFDC-49DB-AB0B-718C72D6B4EF]
As you can see the first and the third parameters are bound correctly (they are replaced by the primary key values), but the second is not. What's the reason of such a strange behavior?