Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assign custom identifier to an @id property

I'm migrating a legacy system over to use Hibernate 3. It currently generates its own identifiers. To keep with what the system currently does before I try and move it over to something a little better, how would I go about specifying (using annotations) my own class that will return the custom generated identifiers when an insert occurs?

Something like:

@Id
@CustomIdGenerator(Foo.class) // obviously this is not a real annotation
public String getId() { ... }

Where the Foo class has one method that generates the identifier.

Currently I'm just calling the setId(String id) method manually but was hoping for a better way to deal with this situation.

like image 557
digiarnie Avatar asked Nov 24 '10 01:11

digiarnie


2 Answers

I don't think there is out-of-box support for generating custom Ids using custom annotations using pure JPA-2 API. But if you want to use provider specific API, then the job is pretty simple. Sample Example

To be provider independent try any of following tricks....

IdGeneratorHolder

public abstract class IdGeneratorHolder {
    /* PersistentEntity is a marker interface */
    public static IdGenerator getIdGenerator(Class<? extends PersistentEntity> entityType) {
             /* sample impelementation */
        if(Product.class.isAssignableFrom(entityType)) {
            return new ProductIdGenerator();

        }
        return null;
    }
}

General IdGenerator interface

public interface IdGenerator {
    String generate();
}

Specific IdGenerator - Product Id Generator

public class ProductIdGenerator implements IdGenerator {
    public String generate() {
            /* some complicated logic goes here */
        return ${generatedId};
    }
}

Now set the generated id either in no-arg constructor OR in @PrePersist method.

Product.java

public class Product implements PersistentEntity {

    private String id;

    public Product() {
        id = IdGeneratorHolder.getIdGenerator(getClass()).generate();
    }

    @PrePersist
    public void generateId() {
        id = IdGeneratorHolder.getIdGenerator(getClass()).generate();
    }

}

In above example all the ids are of the same type i.e. java.lang.String. If the persistent entities have ids of different types.....

IdGenerator.java

public interface IdGenerator {
    CustomId generate();
}

CustomId.java

   public class CustomId {

    private Object id;

    public CustomId(Object id) {
        this.id = id;
    }

    public String  toString() {
        return id.toString();
    }
    public Long  toLong() {
        return Long.valueOf(id.toString());
    }
}

Item.java

@PrePersist
    public void generateId() {
        id = IdGeneratorHolder.getIdGenerator(getClass()).generate().toLong();
    }

You can also use your custom annotation...

CustomIdGenerator.java

public @interface CustomIdGenerator {
    IdStrategy strategy();
}

IdStrategy.java

  enum IdStrategy {
        uuid, humanReadable,    
    }

IdGeneratorHolder.java

public abstract class IdGeneratorHolder {
    public static IdGenerator getIdGenerator(Class<? extends PersistentEntity> entityType) {
        try { // again sample implementation
            Method method = entityType.getMethod("idMethod");
            CustomIdGenerator gen = method.getAnnotation(CustomIdGenerator.class);
            IdStrategy strategy = gen.strategy();
            return new ProductIdGenerator(strategy);
        }

One more thing.... If we set id in @PrePersist method, the equals() method cannot rely on id field (i.e. surrogate key), we have to use business/natural key to implement equals() method. But if we set id field to some unique value (uuid or "app-uid" unique within application) in no-arg constructor, it helps us to implement the equals() method.

public boolean equals(Object obj) {
        if(obj instanceof Product) {
            Product that = (Product) obj;
            return this.id ==that.id;
        }
        return false;
    }

If we or someone else call (intentionally or by mistake) the @PrePersist annotated method more than one times, the "unique id will be changed!!!" So setting id in no-arg constructor is preferable. OR to address this issue put a not null check...

  @PrePersist
    public void generateId() {
        if(id != null)
            id = IdGeneratorHolder.getIdGenerator(getClass()).generate();
    }
}

UPDATE

If we put the id generation in a no-arg constructor, wouldn't that cause a problem when loading entities from the database? because hibernate will call the no-arg constructor causing existing ids to be re-generated

Yeah you are right, I missed that part. :( Actually, I wanted to tell you that:- in my application every Entity object is associated with an Organization Entity; so I've created an abstract super class with two constructors, and every Entity (except Organization) extends this class.

    protected PersistentEntityImpl() {
    }

    protected PersistentEntityImpl(Organization organization) {
        String entityId = UUIDGenerator.generate();
        String organizationId = organization.getEntityId();
        identifier = new EntityIdentifier(entityId, organizationId);
    }

The no-arg constructor is for JPA provider, we never invoke no-arg constructor, but the other organization based constructor. As you can see. id is assigned in Organization based constructor. (I really missed this point while writing the answer, sorry for that).

See if you can implement this or similar strategy in your application.

The second option was using the @PrePersist annotation. I put that in and the method never got hit and gave me an exception stating that I needed to set the id manually. Is there something else I should be doing?

Ideally, JPA provider should invoke @PrePersist methods (one declared in class and also all the other methods that are declared in super-classes) before persisting the entity object. Can't tell you what is wrong, unless you show some code and console.

like image 52
dira Avatar answered Oct 23 '22 18:10

dira


You can.

First, implement org.hibernate.id.IdentifierGenerator

Then you'd have to map it in a mapping xml file. I couldn't find a way to do this with annotations:

<!--
    <identifier-generator.../> allows customized short-naming 
         of IdentifierGenerator implementations.
-->
<!ELEMENT identifier-generator EMPTY>
    <!ATTLIST identifier-generator name CDATA #REQUIRED>
    <!ATTLIST identifier-generator class CDATA #REQUIRED>

Finally, use @GeneratedValue(generator="identifier-name")

Note that this is hibernate-specific (not JPA)

Update: I took a look at the sources of Hibernate, and it seems at one place, after failing to resolve the short name, hibernates attempts to call Class.forName(..). The parameter there is called strategy. So Here's what you try:

  • try setting the class fully-qualified name as string in the generator attribute
  • try setting the class fqn as string in the @GenericGenerator strategy attribute (with some arbitrary name)

Let me know which (if any) worked

like image 1
Bozho Avatar answered Oct 23 '22 17:10

Bozho