47

My problem is the same as described in [1] or [2]. I need to manually set a by default auto-generated value (why? importing old data). As described in [1] using Hibernate's entity = em.merge(entity) will do the trick.

Unfortunately for me it does not. I neither get an error nor any other warning. The entity is just not going to appear in the database. I'm using Spring and Hibernate EntityManager 3.5.3-Final.

Any ideas?

Community
  • 1
  • 1
Jan
  • 1,594
  • 1
  • 20
  • 30

8 Answers8

59

Another implementation, way simpler.

This one works with both annotation-based or xml-based configuration: it rely on hibernate meta-data to get the id value for the object. Replace SequenceGenerator by IdentityGenerator (or any other generator) depending on your configuration. (The creation of a decorator instead of subclassing, passing the decorated ID generator as a parameter to this generator, is left as an exercise to the reader).

public class UseExistingOrGenerateIdGenerator extends SequenceGenerator {
    @Override
    public Serializable generate(SessionImplementor session, Object object)
                        throws HibernateException {
        Serializable id = session.getEntityPersister(null, object)
                      .getClassMetadata().getIdentifier(object, session);
        return id != null ? id : super.generate(session, object);
    }
}

Answer to the exercise (using a decorator pattern, as requested), not really tested:

public class UseExistingOrGenerateIdGenerator implements IdentifierGenerator, Configurable {

    private IdentifierGenerator defaultGenerator;

    @Override
    public void configure(Type type, Properties params, Dialect d) 
                        throws MappingException;
        // For example: take a class name and create an instance
        this.defaultGenerator = buildGeneratorFromParams(
                params.getProperty("default"));
    }

    @Override
    public Serializable generate(SessionImplementor session, Object object)
                        throws HibernateException {
        Serializable id = session.getEntityPersister(null, object)
                      .getClassMetadata().getIdentifier(object, session);
        return id != null ? id : defaultGenerator.generate(session, object);
    }
}
Laurent Grégoire
  • 4,006
  • 29
  • 52
  • +1. Elegant solution. May I add that `@GeneratedValue` without defining any specific strategy seems to default to `SequenceGenerator` when using Hibernate. If I extend `IdentityGenerator` I got a null id. – Magnilex Sep 23 '14 at 08:47
  • 4
    How to use it with @GeneratedValue? – emeraldhieu Jan 20 '15 at 09:46
  • 1
    This solution works great except when it comes to JPA 2.1 schema generation for IdentityGenerator. When you make a custom class that extends IdentityGenerator as above, the JPA schema gen no longer considers it a identity column and therefore doesn't generate the correct table creation script. While this can be remedied by specifying the column creation script in annotations or hibernate config, it doesn't allow you to vary the database architecture (such as using HSQLDB during testing). – Yinzara Jan 09 '16 at 01:48
  • 2
    I added a defect into Hibernate's JIRA. Please vote: https://hibernate.atlassian.net/browse/HHH-10429 – Yinzara Jan 14 '16 at 16:40
  • Elegant solution working with other generators. In my case, I have tried with UUIDGenerator. – Turbut Alin Aug 31 '16 at 08:58
  • Please explain more about creating a decorator. The "exercise for the reader" comment is cute but I would like to understand more what the intent is here. Thx. – chrisinmtown Aug 15 '17 at 15:33
  • The use of a decorator pattern is just refactoring the code to make it cleaner, and is not really answering the initial answer. – Laurent Grégoire Aug 17 '17 at 07:47
  • 3
    Thanks for the help, it didn't quite work for me in 5.2 but the core of it is gold! I updated it for hibernate 5.2: https://stackoverflow.com/a/48819098/728602 – Cadell Christo Feb 16 '18 at 02:01
47

it works on my project with the following code:

@XmlAttribute
@Id
@Basic(optional = false)
@GeneratedValue(strategy=GenerationType.IDENTITY, generator="IdOrGenerated")
@GenericGenerator(name="IdOrGenerated",
                  strategy="....UseIdOrGenerate"
)
@Column(name = "ID", nullable = false)
private Integer id;

and

import org.hibernate.id.IdentityGenerator;
...
public class UseIdOrGenerate extends IdentityGenerator {
private static final Logger log = Logger.getLogger(UseIdOrGenerate.class.getName());

@Override
public Serializable generate(SessionImplementor session, Object obj) throws HibernateException {
    if (obj == null) throw new HibernateException(new NullPointerException()) ;

    if ((((EntityWithId) obj).getId()) == null) {
        Serializable id = super.generate(session, obj) ;
        return id;
    } else {
        return ((EntityWithId) obj).getId();

    }
}

where you basically define your own ID generator (based on the Identity strategy), and if the ID is not set, you delegate the generation to the default generator.

The main drawback is that it bounds you to Hibernate as JPA provider ... but it works perfectly with my MySQL project

Kevin
  • 4,618
  • 3
  • 38
  • 61
  • Nice thing. I played around with it and it works. Unfortunately I was not able to use a generic SeqenceGenerator, because my dialect (mssql) won't support it. Curiously the @SeqenceGenerator works fine. However, this was the solution. – Jan Jul 10 '10 at 15:52
  • 1
    @Jan, it's funny, it's exactly the opposite with MySQL, SequenceGenerator is not supported, but IdentityGenerator works fine, "vive la portabilité!" (~portability rocks!) – Kevin Jul 10 '10 at 19:42
  • @Kevin this soluttion also works if I need to use the generator or set manualy the id? Because I´m dealing with one problem about I have a superclass with ID and one of my application, the user enter manualy the key... – Diego Macario Apr 03 '14 at 09:55
  • 2
    I tried this but i keep getting : "field 'id' doesn't have a default value" – lior Apr 16 '14 at 09:54
  • @Id @GenericGenerator(name = "customGenerator", strategy = "xxx.yyy.UseIdOrGenerate") @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "customGenerator") private Integer eventId; Unfortunately it doesn't work for me – emeraldhieu Jan 20 '15 at 11:16
  • 2
    @lior, need use IncrementGenerator which inherit from IdentityGenerator. – Man Shen Mar 21 '17 at 05:53
  • @ManShen I get what lior is getting - when I use your suggestion it starts ignoring ids altogether – Michail Michailidis Oct 08 '17 at 16:08
  • 1
    @MichailMichailidis try remove the strategy=GenerationType.IDENTITY – Man Shen Oct 09 '17 at 12:03
  • 2
    Before I ask a new question, I'd like to offer the oppurtunity for more points by asking you for a solution for Hibernate 5 which has changed `IdentityGenerator`'s interface to `InsertGeneratedIdentifierDelegate getInsertGeneratedIdentifierDelegate(PostInsertIdentityPersister, Dialect, boolean)`. – Kalle Richter Feb 09 '18 at 10:48
34

Updating Laurent Grégoire's answer for hibernate 5.2 because it seems to have changed a bit.

public class UseExistingIdOtherwiseGenerateUsingIdentity extends IdentityGenerator {

    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
        Serializable id = session.getEntityPersister(null, object).getClassMetadata().getIdentifier(object, session);
        return id != null ? id : super.generate(session, object);
    }
}

and use it like this: (replace the package name)

@Id
@GenericGenerator(name = "UseExistingIdOtherwiseGenerateUsingIdentity", strategy = "{package}.UseExistingIdOtherwiseGenerateUsingIdentity")
@GeneratedValue(generator = "UseExistingIdOtherwiseGenerateUsingIdentity")
@Column(unique = true, nullable = false)
protected Integer id;
Cadell Christo
  • 3,105
  • 3
  • 21
  • 19
8

If you are using hibernate's org.hibernate.id.UUIDGenerator to generate a String id I suggest you use:

public class UseIdOrGenerate extends UUIDGenerator {


    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
        Serializable id = session.getEntityPersister(null, object).getClassMetadata().getIdentifier(object, session);
        return id != null ? id : super.generate(session, object);
    }
}
forhas
  • 11,551
  • 21
  • 77
  • 111
7

I`m giving a solution here that worked for me:
create your own identifiergenerator/sequencegenerator

public class FilterIdentifierGenerator extends IdentityGenerator implements IdentifierGenerator{

@Override
public Serializable generate(SessionImplementor session, Object object)
        throws HibernateException {
    // TODO Auto-generated method stub
    Serializable id = session.getEntityPersister(null, object)
            .getClassMetadata().getIdentifier(object, session);
    return id != null ? id : super.generate(session, object);
}

}

modify your entity as:

@Id
@GeneratedValue(generator="myGenerator")
@GenericGenerator(name="myGenerator", strategy="package.FilterIdentifierGenerator")
@Column(unique=true, nullable=false)
private int id;
...

and while saving instead of using persist() use merge() or update()

rakesh
  • 4,368
  • 1
  • 19
  • 13
2

According to the Selectively disable generation of a new ID thread on the Hibernate forums, merge() might not be the solution (at least not alone) and you might have to use a custom generator (that's the second link you posted).

I didn't test this myself so I can't confirm but I recommend reading the thread of the Hibernate's forums.

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
2

For anyone else looking to do this, above does work nicely. Just a recommendation to getting the identifier from the object rather than having inheritance for each Entity class (Just for the Id), you could do something like:

import org.hibernate.id.IdentityGenerator;

public class UseIdOrGenerate extends IdentityGenerator {

    private static final Logger log = Logger.getLogger(UseIdOrGenerate.class
            .getName());

    @Override
    public Serializable generate(SessionImplementor session, Object object)
            throws HibernateException {
        if (object == null)
            throw new HibernateException(new NullPointerException());

        for (Field field : object.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(Id.class)
                    && field.isAnnotationPresent(GeneratedValue.class)) {
                boolean isAccessible = field.isAccessible();
                try {
                    field.setAccessible(true);
                    Object obj = field.get(object);
                    field.setAccessible(isAccessible);
                    if (obj != null) {
                        if (Integer.class.isAssignableFrom(obj.getClass())) {
                            if (((Integer) obj) > 0) {
                                return (Serializable) obj;
                            }
                        }
                    }
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

        return super.generate(session, object);
    }
}
Jonathan
  • 21
  • 2
1

You need a running transaction.

In case your transaction are manually-managed:

entityManager.getTransaction().begin();

(of course don't forget to commit)

If you are using declarative transactions, use the appropriate declaration (via annotations, most likely)

Also, set the hibernate logging level to debug (log4j.logger.org.hibernate=debug) in your log4j.properties in order to trace what is happening in more details.

Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140