Bypass GeneratedValue in Hibernate (merge data not in db?)
Asked Answered
W

8

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?

Weaks answered 7/7, 2010 at 12:40 Comment(0)
H
48

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

Hilar answered 10/7, 2010 at 9:27 Comment(9)
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.Weaks
@Jan, it's funny, it's exactly the opposite with MySQL, SequenceGenerator is not supported, but IdentityGenerator works fine, "vive la portabilité!" (~portability rocks!)Hilar
@Hilar 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...Cornwall
I tried this but i keep getting : "field 'id' doesn't have a default value"Puisne
@Id @GenericGenerator(name = "customGenerator", strategy = "xxx.yyy.UseIdOrGenerate") @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "customGenerator") private Integer eventId; Unfortunately it doesn't work for meUphemia
@lior, need use IncrementGenerator which inherit from IdentityGenerator.Tolan
@ManShen I get what lior is getting - when I use your suggestion it starts ignoring ids altogetherNyeman
@MichailMichailidis try remove the strategy=GenerationType.IDENTITYTolan
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).Haemolysis
P
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);
    }
}
Procumbent answered 16/12, 2011 at 13:41 Comment(8)
+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.Navicular
How to use it with @GeneratedValue?Uphemia
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).Lailaibach
I added a defect into Hibernate's JIRA. Please vote: hibernate.atlassian.net/browse/HHH-10429Lailaibach
Elegant solution working with other generators. In my case, I have tried with UUIDGenerator.Sciomachy
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.Benuecongo
The use of a decorator pattern is just refactoring the code to make it cleaner, and is not really answering the initial answer.Strophic
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://mcmap.net/q/183231/-bypass-generatedvalue-in-hibernate-merge-data-not-in-dbJobye
H
48

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

Hilar answered 10/7, 2010 at 9:27 Comment(9)
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.Weaks
@Jan, it's funny, it's exactly the opposite with MySQL, SequenceGenerator is not supported, but IdentityGenerator works fine, "vive la portabilité!" (~portability rocks!)Hilar
@Hilar 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...Cornwall
I tried this but i keep getting : "field 'id' doesn't have a default value"Puisne
@Id @GenericGenerator(name = "customGenerator", strategy = "xxx.yyy.UseIdOrGenerate") @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "customGenerator") private Integer eventId; Unfortunately it doesn't work for meUphemia
@lior, need use IncrementGenerator which inherit from IdentityGenerator.Tolan
@ManShen I get what lior is getting - when I use your suggestion it starts ignoring ids altogetherNyeman
@MichailMichailidis try remove the strategy=GenerationType.IDENTITYTolan
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).Haemolysis
J
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;
Jobye answered 16/2, 2018 at 2:0 Comment(0)
K
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);
    }
}
Klingensmith answered 13/8, 2018 at 10:39 Comment(0)
C
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()

Chopper answered 20/4, 2017 at 12:35 Comment(0)
D
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.

Dauphine answered 7/7, 2010 at 15:19 Comment(0)
B
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);
    }
}
Beeswax answered 28/10, 2010 at 20:27 Comment(0)
L
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.

Lm answered 7/7, 2010 at 13:20 Comment(4)
Spring is doing that for me using @Transactional.Weaks
Then make sure the transaction is really started.Lm
@downvoter - while this might not be the issue with this situation, it is often the problem when data is not inserted. So please explain your downvote.Lm
I think the point was good, even thought it was not the problem.Weaks

© 2022 - 2024 — McMap. All rights reserved.