1

I've been successful in creating the following projection in combination with the @Query annotation:

Approach 1

interface Deployment{
  val app: String
  val platform: String
  val org: String
  val space: String
  val instances: Int
  val state: String
}

@Query(value="select a.name as app, p.name as platform, o.name as org, s.name as space, a.instances as instances, a.state as state " +
    "from apps a " +
    "inner join spaces s on a.space_id = s.id " +
    "inner join orgs o on s.org_id = o.id " +
    "inner join platforms p on o.platform_id = p.id " +
    "where o.name = :eai " +
    "and lower(a.name) = lower(:app) " +
    "and lower(a.state) = lower(:state) " +
    "order by p.name, s.name",
    nativeQuery = true)
fun getDeploymentsByEaiAndAppAndState(eai: String, app: String?, state: String?): List<Deployment>

Approach 2

However, I need to dynamically create the SQL statement based on information supplied in a filter:

data class DeploymentFilter(
    val eai: String
    )
{
  var app: String? = null
  var state: String? = null
}

override fun getByFilter(filter: DeploymentFilter): List<Deployment> {
  var sql =
    "select a.name as app, p.name as platform, o.name as org, s.name as space, a.instances as instances, a.state as state " +
    "from apps a " +
    "inner join spaces s on a.space_id = s.id " +
    "inner join orgs o on s.org_id = o.id " +
    "inner join platforms p on o.platform_id = p.id " +
    "where o.name = :eai"
  if (!filter.app.isNullOrBlank()) { sql += " and lower(a.name) = lower(:app)" }
  if (!filter.state.isNullOrBlank()) { sql += " and lower(a.state) = lower(:state)" }
  sql += " order by p.name, s.name"
  val query = em.createNativeQuery(sql)
  query.setParameter("eai", filter.eai)
  if (!filter.app.isNullOrBlank()) { query.setParameter("app", filter.app) }
  if (!filter.state.isNullOrBlank()) { query.setParameter("state", filter.state) }
  @Suppress("UNCHECKED_CAST")
  return query.resultList as List<Deployment>
}

Although the code runs and executes, it's as if Spring (or Kotlin) is no longer automatically converting the Java List<Object[]> into a Kotlin List.

When I convert the List<Deployment> to JSON using Approach 1, I correctly see:

{
  "eai": "6949",
  "deployments": [
    {
      "app": "CisBidLine",
      "platform": "edcbo1",
      "instances": 3,
      "org": "6949",
      "state": "STARTED",
      "space": "production"
    },
    ...

However, with Approach 2, the Deployment data is treated as an array instead of an Object:

{
  "eai": "6949",
  "deployments": [
    [
      "CisPicture",
      "edcbo1",
      "6949",
      "production",
      1,
      "STARTED"
    ],
    ...

There's no difference in the environment or logic between both approaches, other than one explicitly using @Query and the other em.createNativeQuery.

Any help would be greatly appreciated!

UPDATE

If I understand this correctly, Kotlin can effectively transform a Map to the Interface, as long as the keys of the Map match the properties of the Interface. Because query.resultList returns a List<Object[]>, the trick might be to transform that to a Map using the aliases as the keys. As I'm using Hibernate under the hood, I found the following worked:

query
.unwrap(org.hibernate.query.NativeQuery::class.java)
.setResultTransformer(org.hibernate.transform.Transformers.ALIAS_TO_ENTITY_MAP)

Now I can simply convert the Map to the Interface using:

@Suppress("UNCHECKED_CAST")
return query.resultList as List<Deployment>

For those not using Hibernate, I'm assuming one could manually convert the List to a Map first, then transform using as List<Deployment>.

I'm open to other approaches, but this seems to make reasonable sense!

Michael Cronk
  • 63
  • 2
  • 6
  • Use the `NEW` operator – crizzis Aug 11 '20 at 16:30
  • Thank you for your feedback. I believe I can't leverage this for two reasons. The first being this is a **native query** versus **jpql**. The second is the DTO is an interface, not a class that typically defines a constructor. – Michael Cronk Aug 11 '20 at 17:00
  • Ahh, OK, I totally missed the fact it was a native query. You need to use a [result set mapping](https://stackoverflow.com/questions/13012584/jpa-how-to-convert-a-native-query-result-set-to-pojo-class-collection) – crizzis Aug 11 '20 at 17:10
  • Also, 'one explicitly using @Query and the other em.createNativeQuery' makes a world of difference, because it means the former uses Spring Data, while the latter uses vanilla JPA – crizzis Aug 11 '20 at 17:12
  • My understanding is that most of the approaches, including result set mapping, rely on the DTO/POJO to be a class with a constructor, not an interface. For example, when using Hibernates aliasToBean transformer, the exception ***org.hibernate.HibernateException: Could not instantiate resultclass*** was thrown. Transforming the result to a Map first allowed me to leverage Kotlin to then transform it effectively into the Interface (see ***UPDATE*** above). – Michael Cronk Aug 11 '20 at 17:32
  • 1
    Yes, your understanding is correct, because the feature that makes projections work with interfaces is a feature of Spring Data JPA and **not** JPA. You simply need to provide an implementation for the interface and use it whenever you're using result set mappings (or use Kotlin magic, of course, but that was not what the question was about) – crizzis Aug 11 '20 at 17:34
  • Thanks @crizzis. I'm relatively new to the Kotlin + Spring Data + JPA landscape. With so many layers of abstraction, it's been a challenge for sure. – Michael Cronk Aug 11 '20 at 17:56
  • Hi , Is it possible that you cna post a full working example . I am trying to use the approach one but i am just not able to do it , also the documentation is not helpful. – Gagan Singh Jan 03 '21 at 20:29

0 Answers0