caseballs caseballs - 7 months ago 41
Java Question

How to handle an extra field in a named native query in Hibernate

I have a named native query:

@NamedNativeQueries({
@NamedNativeQuery(
name = "WeightLog.getAll",
query = "select weightLogId, dateOfLog, weight, weight - lag(weight) over (ORDER BY dateOfLog) as change, userId from weightLog order by dateOfLog desc",
resultClass = WeightLog.class)})


Where I have "change" showing the difference from the last entry in this column using the lag command.

What is the correct way to handle this in the Entity so that when I do a get I can return it to the UI but when I do a create/update/delete it will be ignored?

What I have done is annotated it like so:

@Column(insertable = false, updateable=false)
private Double change;


This means that during my getAll it will showing in the JSON being returned from my REST endpoint and will be ignored when doing a create/update. However with this I am not able to delete from by DB through the corresponding REST endpoint as in by delete method in my DAO I use a get, which will return the "change".

Delete methods in DAO:

public WeightLog getById(final Long weightLogId){
return get(weightLogId);
}

public Long deleteById(final Long weightLogId){
final WeightLog weightLogToDelete = this.getById(weightLogId);
this.delete(weightLogToDelete);
return weightLogToDelete.getWeightLogId();
}

private void delete(final WeightLog weightLog){
currentSession().delete(weightLog);
}


Exception is:

ERROR [2016-04-18 21:06:32,365] io.dropwizard.jersey.errors.LoggingExceptionMapper: Error handling a request: 1f50d70af5fc5606
! org.postgresql.util.PSQLException: ERROR: column weightlog0_.change does not exist
! Position: 51

@Transient is no good and it seems this gets ignored both ways then and won't show up for any of the CRUD operations.

I presume that what I have above is completely the wrong way to do this and is just a "hacky" way to get as far as I have so feel free to tell me what I have done is completely wrong and point me in the right direction of how to handle this case of using lag. I would prefer to use annotations if possible.

EDIT FOR OPTION ONE:

Following the first option from Dimitri below how the json data being returned to the UI is of this format:

[
{
"weightLogId": 15,
"dateOfLog": "2016-04-30",
"weight": 55,
"userId": 1
},
-31.7]


With the change on its own with no name attached. This makes it difficult to actually get this attribute printed to the screen no? I'm using Backbone on the front end to can't call model.attribute or model.get('attribte'). With my old cold the format would have been something like:

{
"weightLogId": 5,
"dateOfLog": "2016-03-13",
"weight": 87.5,
"userId": 1,
"change": 0
}


Anyway I can get the "name" given in the ResultSetMapping to show up here?

Its actually messes up the JSON format completely as now the attributes are in an object one layer down and now dont match up with the Backbone model format on the UI, which is:

defaults: {
weight: 0,
dateOfLog: "2016-01-01",
userId: 1,
change: "0"
},


For reference the ResultMapping used was:

@SqlResultSetMapping(
name = "WeightLogWithChange",
entities = {
@EntityResult(
entityClass = WeightLog.class,
fields = {
@FieldResult(name = "weightLogId", column = "weightLogId"),
@FieldResult(name = "userId", column = "userId"),
@FieldResult(name = "dateOfLog", column = "dateOfLog"),
@FieldResult(name = "weight", column = "weight")
}),
},
columns = @ColumnResult(name = "change", type = Double.class)


)

EDIT FOR OPTION 2

For option two how would the named query change?

@NamedNativeQueries({
@NamedNativeQuery(
name = "WeightLog.getAll",
query = "select weightLogId, dateOfLog, weight, weight - lag(weight) over (ORDER BY dateOfLog) as change, userId from weightLog order by dateOfLog desc",
resultSetMapping = "WeightLogWithChange"
)


})

Here it contains the lag SQL. If that Lag part is moved to a Formula above the attribute then how does this named query change? I presume the lag part here will need to be removed?

Answer

If I understand correctly you'd like the change attribute to be visible when the query is performed but invisible in delete/update/insert operations.

Solution 1

One way is to treat this parameter separately and not as an entity attribute but still keep it close to the entity employing a JPA feature, the @SqlResultSetMapping (documentation).

Your entity will look like this with the new mapping annotation (ignoring all other annotations):

@SqlResultSetMapping(
   name = "WeightLogWithChange",
   entityClass = WeightLog.class,
   fields = {
      @FieldResult(name = "id", column = "weightLogId"),
      @FieldResult(name = "userId", column = "userId"),
      @FieldResult(name = "someDate", column = "dateOfLog"),
      @FieldResult(name = "weight", column = "weight")}),
   columns = @ColumnResult(name = "change", type = Double.class))
class WeightLog {
  //..
}

The invocation of the Native query would then be:

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

List<Object[]> results = em.createNativeQuery("select weightLogId, dateOfLog, weight, weight - lag(weight) over (ORDER BY dateOfLog) as change, userId from weightLog order by dateOfLog desc", "WeightLogWithChange").getResultList();

results.stream().forEach((result) ->  {
   log.info("This is my weightLog object:" + result[0] + " and this is my computed change stored in a separate var: " + result[1]);
});

em.getTransaction().commit();
em.close();

Note in the above example: 1) how we map the returned column values of the SQL to entity attributes and the change variable to a particular type we want and 2) how we get the result from the native query as an array where the first element is the returned entity object and second the variable change we mapped.

Solution 2

You could keep the change within the entity and mark it as a formula which is a Hibernate specific feature.

From the documentation:

A powerful feature is derived properties. These properties are by definition read-only. The property value is computed at load time. You declare the computation as an SQL expression. This then translates to a SELECT clause subquery in the SQL query that loads an instance.

In this case the attribute would look like:

//you have to check the validity of the SQL
@Formula("(select weight - lag(weight) over (ORDER BY dateOfLog) from weightLog wl where wl.id = id)")
private Double change;
Comments