Assign custom identifier to an @id property

后端 未结 2 1452
一个人的身影
一个人的身影 2020-12-30 11:11

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 ov

相关标签:
2条回答
  • 2020-12-30 11:31

    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.

    0 讨论(0)
  • 2020-12-30 11:32

    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

    0 讨论(0)
提交回复
热议问题