6

I have a JPA 2 web application (Struts 2, Hibernate 4 as JPA implementation only).

The current requirement is to add a (non-id) numeric sequential field, filled for certain rows only, to an existing entity. When inserting a new row, based on a certain condition, I need to set the new field to its highest value + 1 or to NULL.

For example:

ID     NEW_FIELD     DESCRIPTION
--------------------------------
1          1           bla bla
2                      bla bla       <--- unmatched: not needed here
3                      bla bla       <--- unmatched: not needed here
4          2           bla bla
5          3           bla bla
6          4           bla bla
7                      bla bla       <--- unmatched: not needed here
8          5           bla bla
9                      bla bla       <--- unmatched: not needed here
10         6           bla bla

In the good old SQL, it would be something like:

INSERT INTO myTable (
    id, 
    new_field, 
    description
) VALUES (
    myIdSequence.nextVal, 
    (CASE myCondition
        WHEN true 
        THEN myNewFieldSequence.nextVal
        ELSE NULL
    END),
    'Lorem Ipsum and so on....'
)

But I've no clue on how to achieve it with JPA 2.

I know I can define callbacks methods, but JSR-000317 Persistence Specification for Eval 2.0 Eval discourages some specific operations from inside it:

3.5 Entity Listeners and Callback Methods
- Lifecycle callbacks can invoke JNDI, JDBC, JMS, and enterprise beans.
- In general, the lifecycle method of a portable application should not invoke EntityManager or Query operations, access other entity instances, or modify relationships within the same persistence context.[43] A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.

[43] The semantics of such operations may be standardized in a future release of this specification.

Summarizing, yes to JDBC (!) and EJB, no to EntityManager and other Entities.


EDIT

I'm trying to achieve the solution described in the answer from @anttix, but I'm encoutering some problem, so please correct me where I'm wrong.

Table

MyTable
-------------------------
ID            number (PK)
NEW_FIELD     number
DESCRIPTION   text

Main Entity

@Entity
@Table(name="MyTable")
public class MyEntity implements Serializable {

    @Id
    @SequenceGenerator(name="seq_id", sequenceName="seq_id", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq_id")
    private Long id;

    @OneToOne(cascade= CascadeType.PERSIST) 
    private FooSequence newField;

    private String description

    /* Getters and Setters */
}

Sub entity

@Entity
public class FooSequence {

    @Id
    @SequenceGenerator(name="seq_foo", sequenceName="seq_foo", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq_foo")
    private Long value;

    /* Getter and Setter */
}

DAO

myEntity.setNewField(new FooSequence());
entityManager.persist(myEntity);

Exception

Caused by: javax.transaction.RollbackException: ARJUNA016053: Could not commit transaction.

[...]

Caused by: javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: ERROR: relation "new_field" does not exist

[...]

Caused by: org.hibernate.exception.SQLGrammarException: ERROR: relation "new_field" does not exist

[...]

Caused by: org.postgresql.util.PSQLException: ERROR: relation "new_field" does not exist

What am I doing wrong ? I'm pretty new to JPA 2 and I've never used an entity not associated to a physical table... this approach is totally new to me.

I guess I need to put the @Column definition somewhere: how could JPA possibly know that the newField column (mapped through ImprovedNamingStrategy to new_field on the database) is retrieved through the value property of the FooSequence entity ?

Some pieces of the puzzle are missing.


EDIT

As asked in comments, this is the persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" 
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
                     http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="MyService"  transaction-type="JTA">

        <jta-data-source>java:jboss/datasources/myDS</jta-data-source>      

        <properties>             

            <property name="hibernate.dialect" 
                     value="org.hibernate.dialect.PostgreSQLDialect" />

            <property name="hibernate.ejb.naming_strategy" 
                     value="org.hibernate.cfg.ImprovedNamingStrategy"/>

            <property name="hibernate.query.substitutions" 
                     value="true 'Y', false 'N'"/>           

         <property name="hibernate.show_sql" value="true" />
         <property name="format_sql"         value="true" />
         <property name="use_sql_comments"   value="true" />

        </properties>

    </persistence-unit>
</persistence>
Community
  • 1
  • 1
Andrea Ligios
  • 49,480
  • 26
  • 114
  • 243
  • @AndreiI Please, can you elaborate ? I don't get it. Which is the old column ? Table was ID - DESCRIPTION, entity was Id - description, now I added a non-id, non-pk field to both the table and the entity, and need to assign it a value only sometimes. – Andrea Ligios Mar 21 '14 at 16:24
  • Add `@JoinColumn(name="INCR_FIELD")` to your `private FooSequence newField;`, and add the `FooSequence` class to your persistence.xml (if needed). Afterwards, in order to get the value of NEW_FIELD use `getFooSequence().getId()` – V G Mar 21 '14 at 16:33
  • No heck, same error (I added `@JoinColumn(name="new_field")` to `private FooSequence newField;` , that was the only change I made). Please, post it as an answer with some more details, I'll assign the bounty as soon as it will work, no matter the ending period, and I'll upvote every helping answer immediately. – Andrea Ligios Mar 21 '14 at 16:56
  • please try using a different column than `new_field` (to be sure it is not caused by that column), that's why I used a new `INCR_FIELD` column. – V G Mar 21 '14 at 16:58
  • What should I expect ? I've `new_field` on the database table `MyTable`, if I put `INCR_FIELD`, it won't be found (I don't let hibernate create tables/columns for me) – Andrea Ligios Mar 21 '14 at 17:00
  • I do not want to pollute the answers with theories, so if it works, I'll post it as an answer for the bounty ;) It's a long shot, but PostgreSQL is case sensitive when table/field names are quoted. Try @JoinColumn(name="NEW_FIELD") http://binodsblog.blogspot.com/2011/02/postgresql-is-case-sensitive.html If that doesn't work, try to drop the column and re-create it with all lowercase. – anttix Mar 21 '14 at 23:38
  • Also I would enable query logging to see what is passed to database and then try to execute it from psql to figure out why it fails http://stackoverflow.com/questions/2536829/hibernate-show-real-sql – anttix Mar 21 '14 at 23:41
  • Looking at the Hibernate source, it seems it will convert backticks to double quotes when PostgreSQL dialect is used so I'd also try ``@JoinColumn(name="`NEW_FIELD`")`` where `NEW_FIELD` is a case sensitive spelling that you get directly from the database. Eg with \d MyTable from psql command line – anttix Mar 21 '14 at 23:55
  • @anttix: thanks, I'm already printing the SQL, the field was created all lowercase according to PLSQL specs, and I'm referencing it that way. – Andrea Ligios Mar 24 '14 at 10:00
  • The problem is that FooSequence is created, **but `FooSequence.value` is null** :////// – Andrea Ligios Mar 24 '14 at 10:01
  • Can you post the generated SQL statement that results in PSQL exception? – anttix Mar 24 '14 at 17:19
  • @AndreaLigios I tested anttixes code and it works perfectly (with a hsql DB). So you can be sure that you are doing something wrong, something that we do not see. – V G Mar 26 '14 at 17:02
  • I'm sure of that, because the original answer linked by anttix got 25 upvotes, so I guess it should run properly. Apart from the fact that, due to lack of time, I've used my real entities and database in the tests, instead of creating a minimal one like in this question. The blocking error I got was `@OneToOne or @ManyToOne on references an unknown entity: FooSequence`, there was no way to let the main entity recognize the second entity. What could cause that ? – Andrea Ligios Mar 26 '14 at 17:31
  • Well, the problem seems to be that `FooSequence` was not added to your persistence.xml file. Besides, could you post your persistence.xml in order to check what dialect you have? – V G Mar 26 '14 at 19:52
  • @AndreiI: my persistence.xml has no entities in it, the only thing there is the persistence-unit with a PostgreSQLDialect. Do I need to put that entity there because it is not bound to any table or something like that ? BTW since you tried anttix code and it worked, I would upvote its answer, especially after the SSCCE :) – Andrea Ligios Mar 26 '14 at 22:01
  • @AndreaLigios the `FooSequence` has a table and you do not need an entry in persistence.xml IF you have `false` in it. – V G Mar 27 '14 at 08:31
  • @AndreiI I've not set any exclude-unlisted-classes, and what do you mean by FooSequence has a table ? It represent a single field in the main table (of Main entity). There is only one table in database in this example, isn't it ? O_o I'll post my persistence.xml for completeness, btw. Thanks a lot for helping – Andrea Ligios Mar 27 '14 at 09:43
  • It must be also another table: in total you should have two tables. Every entity in JPA has at least one table (depending on relationships it can have additional related tables). I would try adding that entry in the persistence.xml file. – V G Mar 27 '14 at 11:09
  • @AndreiI Then the whole (mine) question (related to the accepted solution) makes no sense; look at the table I have in my question: it is ONE table, and I need to create a new row by generating one or two values. I expressed my doubts about having an entity (FooSequence) not related to a table in the first comment to anttix answer :/ So the accepted solution is working with TWO tables, each one with a single ID field ? – Andrea Ligios Mar 27 '14 at 11:21
  • 1
    @AndreaLigios exactly: it works with two tables, each with an ID field, but I doubt that is a problem for you. In order to take get the `NEW_FIELD` in your code, you should call `getFooSequence().getId()` (as I mentioned in one of my comments above). – V G Mar 27 '14 at 11:26

4 Answers4

4

One possible solution is to use a separate entity with its own table that will encapsulate only the new field and have an OneToOne mapping with that entity. You will then instantiate the new entity only when you encounter an object that needs the additional sequence number. You can then use any generator strategy to populate it.

@Entity
public class FooSequence {
    @Id
    @GeneratedValue(...)
    private Long value;
}

@Entity 
public class Whatever {
    @OneToOne(...)
    private FooSequnce newColumn;
}

See:

A gradle 1.11 runnable SSCCE (using Spring Boot):

src/main/java/JpaMultikeyDemo.java

import java.util.List;
import javax.persistence.*;
import lombok.Data;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;

@Configuration
@EnableTransactionManagement
@EnableAutoConfiguration
public class JpaMultikeyDemo {
    @Entity @Data
    public static class FooSequence {
        @Id @GeneratedValue private Long value;
    }

    @Entity @Data
    public static class FooEntity {
        @Id @GeneratedValue private Long id;
        @OneToOne           private FooSequence sequence;
    }

    @PersistenceContext
    EntityManager em;

    @Transactional
    public void runInserts() {
        // Create ten objects, half with a sequence value
        for(int i = 0; i < 10; i++) {
            FooEntity e1 = new FooEntity();
            if(i % 2 == 0) {
                FooSequence s1 = new FooSequence();
                em.persist(s1);
                e1.setSequence(s1);
            }
            em.persist(e1);
        }
    }

    public void showAll() {
        String q = "SELECT e FROM JpaMultikeyDemo$FooEntity e";
        for(FooEntity e: em.createQuery(q, FooEntity.class).getResultList())
            System.out.println(e);
    }

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(JpaMultikeyDemo.class);
        context.getBean(JpaMultikeyDemo.class).runInserts();
        context.getBean(JpaMultikeyDemo.class).showAll();
        context.close();
    }
}

build.gradle

apply plugin: 'java'
defaultTasks 'execute'

repositories {
    mavenCentral()
    maven { url "http://repo.spring.io/libs-milestone" }
}

dependencies {
    compile "org.springframework.boot:spring-boot-starter-data-jpa:1.0.0.RC5"
    compile "org.projectlombok:lombok:1.12.6"
    compile "com.h2database:h2:1.3.175"
}

task execute(type:JavaExec) {
    main = "JpaMultikeyDemo"
    classpath = sourceSets.main.runtimeClasspath
}

See also: http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-configure-datasource

Community
  • 1
  • 1
anttix
  • 7,709
  • 1
  • 24
  • 25
  • Thanks, I'd found that answer before asking my question, but wasn't sure if it was the best way to go. I've started trying today, but I'm encountering some errors, that I'll add to the question; meanwhile, If you have implemented (or seen) the solution you described, could you please expand its usage description ? Since I've always used Entities bound to DB tables, while this one should be a column of the table bound to the Parent entity (Whatever), I'm not sure if I'm doing it properly – Andrea Ligios Mar 21 '14 at 13:52
  • As said in a comment above, the problem is that **`FooSequence` is created, but `FooSequence.value` is null** – Andrea Ligios Mar 24 '14 at 10:05
  • New objects do not get their ID fields populated until they are persisted and flushed e.g. `em.persist(newObj); em.flush();` Hibernate will generate ID-s and order SQL statements before they are executed (flushed). Thus the fact that the value shows up as null initially will not stop the relation from being properly inserted/updated. – anttix Mar 24 '14 at 17:23
  • You are totally right, the null problem was a non problem, after writing that I found out the real issue was `@OneToOne or @ManyToOne on references an unknown entity: FooSequence`, then I've done a zillion tries and gave up using a workaround (I had few hours to experiment with this, unluckily). Can't post SQL (that was absolutely normal) right now, code is changed too much, even in the unit tests, and I've no time to clean it up to post it here; I still would like to see the above code running in an [SSCCE](http://www.sscce.org/), though :/ Thanks a lot for your time – Andrea Ligios Mar 24 '14 at 17:52
  • @andrea-ligios you do realize that a JPA SSCCE is not very small and not very self-contained ... That said, fortunately we have Spring boot ;) See the updated answer. – anttix Mar 26 '14 at 18:01
  • Never used Spring boot, but... woah, the SSCCE is perfect ;) I'll accept the answer and reward you with bounty right now (on faith, and I'll try it ASAP). Thanks a lot. – Andrea Ligios Mar 26 '14 at 18:29
  • could you specify if this solution is working with TWO tables with ONE field or with ONE table with TWO fields ? – Andrea Ligios Mar 27 '14 at 11:26
  • It has two tables and two fields. E(id, S_value) S(value). The extra table is part of a compromise this approach makes. In principle you can delete rows from the sequence table after insert if you do not have an FK constraint on the table. However it's probably not worth the effort. – anttix Mar 27 '14 at 17:54
  • Lol, this is what I was missing when reading [the original answer](http://stackoverflow.com/questions/277630/hibernate-jpa-sequence-non-id), and that is the reason why it didn't work for me... I had one table, as described in my answer. I thought this was a kind of hacky way to use an entity not bound to a table, but representing a column, something like that. Still thanks for your help :) – Andrea Ligios Mar 27 '14 at 18:04
  • As you probably know by now JPA only mandates the use of `@GeneratedValue` on an `@Id` column. So in order to have a non-id sequence on table you have to compromise either on the DB side (tolerate an extra table) or on the code side (use a complex mix of entity listeners and native queries). Both solutions are "hacky", which one is more "hacky" depends on if you see beauty in the DB schema or in the code :) – anttix Mar 27 '14 at 18:08
  • Exactly, I've gone with solution n.2 the day after opening this question, waiting for a better solution. I've not used any entity listener, I'll post my "manual" solution ASAP, please comment if you see big problems with that :) – Andrea Ligios Mar 28 '14 at 11:06
  • So if FooSequence gets its own table, but the owner of the sequence points specifically to it... doesn't that mean the sequence table only has one field? So how do we even deal with uniques in that table? Wouldn't it have to have an FK pointing to the parent? Big caveat: using GenerationType.SEQUENCE won't work in SQL Server 2008, if you're cursed with having to use that junk. – Alkanshel Jan 07 '16 at 20:49
  • Do we need to have separate table for the FooSequence to insert the sequence value.I am getting the insertion error for the FooSequence table.which i don't have in database – muthukumar Jul 31 '16 at 04:40
3

This looks like it could be a good case for some AOP. First start by creating a custom field annotation @CustomSequenceGeneratedValue, and then annotate the field on the entity with it:

public class MyEntity {
...
    @CustomSequenceGeneratedValue
    private Long generatedValue;

    public void setGeneratedValue(long generatedValue) {

    }
}

Then an aspect is created to increment generated values:

@Aspect
public class CustomSequenceGeneratedValueAspect {

    @PersistenceContext 
    private EntityManager em;

    @Before("execution(* com.yourpackage.dao.SomeDao.*.*(..))")
    public void beforeSaving(JoinPoint jp) throws Throwable {
        Object[] args = jp.getArgs();
        MethodSignature ms = (MethodSignature) jp.getSignature();
        Method m = ms.getMethod();

        Annotation[][] parameterAnnotations = m.getParameterAnnotations();

        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] annotations = parameterAnnotations[i];
            for (Annotation annotation : annotations) {
                if (annotation.annotationType() == CustomSequenceGeneratedEntity.class) {
                       ... find generated properties run query and call setter ...

                      ... Query query = em.createNativeQuery("select MY_SEQUENCE.NEXTVAL from dual");
                }
            }
        }
    } 
}

Then the aspect is scanned with <aop:aspectj-autoproxy />, and applied to any Spring DAO saving entities of this type. The aspect would populate the sequence generated values based on a sequence, in a transparent way for the user.

Angular University
  • 42,341
  • 15
  • 74
  • 81
  • Thanks, I know AOP, I've used it sometimes in the past, BTW it would be a little overkill in this case; I already have alternative ways to do my job, I'm just curious about the other-entity answer, since it seems to be the right JPA way to do it, but I can't make it work :/ Thanks the same, +1. – Andrea Ligios Mar 24 '14 at 10:04
  • the problem is that JPA says that @GeneratedValue cannot be used for non id fields, see in this PDF page 375, it says `[97] Portable applications should not use the GeneratedValue annotation on other persistent fields or properties.` http://code.google.com/p/jgk-spring-recipes/downloads/detail?name=persistence-2_0-final-spec.pdf – Angular University Mar 24 '14 at 14:41
  • so without an official solution, i think the only way is to either do some AOP, or create a hibernate custom type. one optimization is to use a hi-Lo sequence in the database (reserves for example 100 keys in one go, so that the sequence is not called 100 times, only one) – Angular University Mar 24 '14 at 14:43
  • The solution I was trying to set-up received 25 upvotes in the original question, and has been claimed to run properly, so that should be the official JPA "hack" (if not JPA "solution"), and someone must have been able tu make it work; I was trying that until one hour ago, when I dropped it and switched to an easier, 30min workaround, that I will post if noone will provide a solution. As said, your is a great solution to a more complex problem, and I'll use it in future, best fitting scenarios. I would like to avoid raw Hibernate solutions (while using Hibernate!), AspectJ is even beyond that – Andrea Ligios Mar 24 '14 at 14:58
1

You mentioned being open to using JDBC. Here is how you can you use Entity Callback with JdbcTemplate, the example uses Postgres's syntax for selecting next value in a sequence, just update it to use the right syntax for your DB.

Add this to your entity class:

@javax.persistence.EntityListeners(com.example.MyEntityListener.class)

And here is listener implementation (@Qualifier and required = true are necessary for it to work):

package com.example;

import javax.persistence.PostPersist;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class MyEntityListener {

    private static JdbcTemplate jdbcTemplate;

    @Autowired(required = true)
    @Qualifier("jdbcTemplate")
    public void setJdbcTemplate(JdbcTemplate bean) {
        jdbcTemplate = bean;
    }

    @PostPersist
    @Transactional
    public void postPersis(MyEntity entity) {
        if(isUpdateNeeded(entity)) { 
            entity.setMyField(jdbcTemplate.queryForObject("select nextval('not_hibernate_sequence')", Long.class));
        }
    }

    private boolean isUpdateNeeded(MyEntity entity) {
        // TODO - implement logic to determine whether to do an update
        return false;
    }
}
SergeyB
  • 9,478
  • 4
  • 33
  • 47
0

The hacky solution I used to keep it simple is the following:

MyEntity myEntity = new MyEntity();
myEntity.setDescription("blabla");
em.persist(myEntity);
em.flush(myEntity);
myEntity.setNewField(getFooSequence());

The complete code ("pseudo-code", I've written it directly on SO so it could have typos) with transaction handling would be like :

Entity

@Entity
@Table(name="MyTable")
public class MyEntity implements Serializable {

    @Id
    @SequenceGenerator(name="seq_id", sequenceName="seq_id", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq_id")
    private Long id;

    private Long newField; // the optional sequence
    private String description
    /* Getters and Setters */
}

Main EJB:

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER) // default
public class MainEjb implements MainEjbLocalInterface {

    @Inject 
    DaoEjbLocalInterface dao;

    // Create new session, no OSIV here
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) 
    public Long insertMyEntity(boolean myCondition) throws Exception {

        try {
            MyEntity myEntity = dao.insertMyEntity(); 
            // if this break, no FooSequence will be generated

            doOtherStuff();
            // Do other non-database stuff that can break here. 
            // If they break, no FooSequence will be generated, 
            // and no myEntity will be persisted.                                

            if (myCondition) {
                myEntity.setNewField(dao.getFooSequence());
                // This can't break (it would have break before). 
                // But even if it breaks, no FooSequence will be generated,
                // and no myEntity will be persisted.
            }
        } catch (Exception e){
            getContext().setRollbackOnly();
            log.error(e.getMessage(),e);
            throw new MyException(e);
        }    
    }
}

DAO EJB

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER) // default
public class DaoEjb implements DaoEjbLocalInterface {

    @PersistenceContext( unitName="myPersistenceUnit")
    EntityManager em;

    // default, use caller (MainEJB) session
    @TransactionAttribute(TransactionAttributeType.REQUIRED) 
    public MyEntity insertMyEntity() throws Exception{
        MyEntity myEntity = new MyEntity();
        myEntity.setDescription("blabla");
        em.persist(myEntity);
        em.flush(); // here it will break in case of database errors, 
                    // eg. description value too long for the column.
                    // Not yet committed, but already "tested".
        return myEntity;
    }

    // default, use caller (MainEJB) session
    @TransactionAttribute(TransactionAttributeType.REQUIRED) 
    public Long getFooSequence() throws Exception {
        Query query = em.createNativeQuery("SELECT nextval('seq_foo')");
        return ((BigInteger) query.getResultList().get(0)).longValue();
    }
}

This will guarantee there will be no gaps in the FooSequence generation.

The only drawback, that I don't care at all in my use case, is that FooSequence and the @Id sequence are not synchronized, so two concurrent inserts may have "inverted" FooSequence values, respecto to their order of arrive, eg.

ID  NEW FIELD
-------------
 1      2
 2      1
Andrea Ligios
  • 49,480
  • 26
  • 114
  • 243