Enterprise Java Development@TOPIC@

Chapter 27. JPA Enum Mapping

27.1. Mapping Enum Ordinal Values
27.2. Mapping Enum Name Values
27.3. Mapping Enum Alternate Values
27.4. Summary

This chapter will take you through the steps to map an enumerated class to the database. Enums are a specialized cross between a typed collection and an inheritance hiearchy. They are very convenient for expressing well known, type-safe values for a property. Enumerated classes can be mapped to the database by

With what we know about PROPERTY mapping, we can also leverage some object-based tricks to map more customized value types.

Note

For simplicity, the exercise will only deal with annotated classes from this point forward. As you should have realized -- anything you can do with an annotation has a parallel construct within the ORM descriptor. It should be a trivial exercise to locate the orm.xml equivalent for the annotation should you need to use the concepts discussed from here on in a mapped entity.

We will first look at mapping an ordinal enum value using a Dog entity. The ordinal is stored in a efficient integer column type but can be more cryptic to read in a raw query result.

  1. Put the following entity class in place in your src/main tree.

    package myorg.entityex.annotated;
    
    
    import javax.persistence.Access;
    import javax.persistence.AccessType;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.Table;
    @Entity
    @Table(name="ENTITYEX_DOG")
    @Access(AccessType.FIELD)
    public class Dog {
        public enum Sex {
                MALE, FEMALE
        }
        
        @Id @GeneratedValue
        private int id;
        private Sex gender;
        
        public int getId() { return id; }
        public void setId(int id) {
            this.id = id;
        }
        public Sex getGender() { return gender; }
        public Dog setGender(Sex gender) {
            this.gender = gender;
            return this;
        }
    }
  2. Add the new entity to your persistence unit in src/test

    
    # src/test/resources/META-INF/persistence.xml

            <class>myorg.entityex.annotated.Dog</class>
  3. Build the module with the new entity and observe the generated schema for the Dog. The property, by default is being mapped by its ordinal property. That means the value stored will be a 0, 1, 2, ... value that represents the values position within the defined enum.

        create table ENTITYEX_DOG (
            id integer generated by default as identity,
            gender integer,
            primary key (id)
        );
  4. Optionally add an explicit specification to use ordinal mapping.

        @Enumerated(EnumType.ORDINAL)
    
        private Sex gender;
  5. Add the following test method to your AnimalTest.java JUnit test case. It will persist an instance of a Dog, poke into the raw database to verify what is being stored, and obtain a new Dog instance to verify the enum was materialized properly.

        @Test
    
        public void testEnums() {
            logger.info("testEnums");
            Dog dog = new Dog()
                .setGender(Dog.Sex.FEMALE);
            em.persist(dog);
            em.flush();
            
            //check the raw value stored in the database
            Object[] o = (Object[])em.createNativeQuery("select GENDER from ENTITYEX_DOG where id=?")
                    .setParameter(1, dog.getId())
                    .getSingleResult();
            logger.debug("col=" + o);
            assertEquals("unexpected gender", Dog.Sex.FEMALE.ordinal(), ((Number)o).intValue());
            
            //get a new instance
            em.detach(dog);
            Dog dog2 = em.find(Dog.class, dog.getId());
            assertEquals("unexpected dog gender", dog.getGender(), dog2.getGender());
        }
  6. Build module and run the new test method

    $ mvn clean test -Dtest=myorg.entityex.AnimalTest#testEnums
    ...
     -col=1
    ...
    [INFO] BUILD SUCCESS

Okay -- the first goal is now complete. You have mapped an enum as an ordinal value to the database. It may also be useful to take a look at the row(s) in the table using the DB browser UI.

We will next look at mapping a name value for the enum. Names are convenient since they are more expressive in a raw database query and are not sensitive to their order within the Enum. However, the string value does take more space to represent and would be less efficient to implement certain comparisons.

  1. Add the following Color Enum to the Dog class. This will define the Enum Color, declare an instance of Color and define its mapping as the STRING name.

        public enum Color {
    
            WHITE, BLACK, BROWN, MIX 
        }
    ...    
        @Enumerated(EnumType.STRING)
        private Color color;
    ...
        public Color getColor() { return color; }
        public Dog setColor(Color color) {
            this.color = color;
            return this;
        }
  2. Rebuild the module and take a look at the generated schema. Color has been modeled as a varchar type with a default maximum size.

        create table ENTITYEX_DOG (
            id integer generated by default as identity,
            color varchar(255),
            gender integer,
            primary key (id)
        );
  3. You can optionally supply a length specification to the enum string to create a tuned column size for the Color type. This length value will get relfected in the new DDL output.

        @Column(length=16)
    
        private Color color;
  4. Update the test method with the following updates for the color property.

        @Test
    
        public void testEnums() {
            logger.info("testEnums");
            Dog dog = new Dog()
                .setGender(Dog.Sex.FEMALE)
                .setColor(Dog.Color.MIX);
            em.persist(dog);
            em.flush();
            
            //check the raw value stored in the database
            Object[] o = (Object[])em.createNativeQuery("select GENDER, COLOR from ENTITYEX_DOG where id=?")
                    .setParameter(1, dog.getId())
                    .getSingleResult();
            logger.debug("cols=" + Arrays.toString(o));
            assertEquals("unexpected gender", Dog.Sex.FEMALE.ordinal(), ((Number)o[0]).intValue());
            assertEquals("unexpected color", Dog.Color.MIX.name(), ((String)o[1]));
            
            //get a new instance
            em.detach(dog);
            Dog dog2 = em.find(Dog.class, dog.getId());
            assertEquals("unexpected dog gender", dog.getGender(), dog2.getGender());
            assertEquals("unexpected dog color", dog.getColor(), dog2.getColor());
        }
  5. Rebuild the module and observe that our new property storage has passed the provided asserts.

    mvn clean test -Dtest=myorg.entityex.AnimalTest#testEnums
    ...
     -cols=[1, MIX]
    ...
    [INFO] BUILD SUCCESS

If you look at the row values in the DB UI, you should be able to easily read the color string value.

In the previous sections we mapped enums based on built-in capabilities; ordinal and name. There are times when mapping a column is not as clean as above. Maybe there is a non-contiguous error code or maybe there is a pretty string with spaces in the name. In this section, we will look to make a pretty name to an internally stored enum. Officially it will be mapped as a string as far as JPA is concerned, but we will leverage getter/setter methods to covert to what we want to work with internally.

  1. Add the following Breed enum to the Dog entity class. Follow it up with a property of that type. Leave off any JPA mappings in this area. We will use PROPERTY mapping.

        public enum Breed {
    
            LABRADOR("Lab"),
            SAINT_BERNARD("Saint Bernard");
            public final String prettyName;
            private Breed(String prettyName) { this.prettyName = prettyName; }
            
            public static Breed getBreed(String prettyName) {
                for (Breed breed : values()) {
                    if (breed.prettyName.equals(prettyName)) {
                        return breed;
                    }
                }
                return null;
            }
        }
        private Breed breed;
  2. Define the getter/setter to be used by JPA to access the breed property. Note that we have defined these methods separate from the getBreed/setBreed methods because they accept and return the Breed enum. These methods form a contract with the provider to return a string. We also need to tell the provider the database column name to use because the default column naming rules will not work in in this case.

        @Access(AccessType.PROPERTY)
    
        @Column(name="BREED", length=32)
        protected String getDBBreed() {
            return breed==null ? null : breed.prettyName;
        }
        protected void setDBBreed(String dbValue) {
            breed=Breed.getBreed(dbValue);
        }

    Notice how the methods declared above are non-public. They where defined that way since the public clients of the Dog class want to work with the Breed enum and not strings. To have them both public could be confusing and -- if they want strings -- they can always get that from the Breed enum itself.

  3. Define the existing property as transient since the persistence of the property is being handled through dBBreed and leaving it non-Transient would cause an extra database column to be created for the enum.

        @Transient
    
        private Breed breed;
  4. Rebuild the module and observe the generated schema for the updated entity. Note the varchar BREED column that has also been restricted to 32 characters according to our @Column specification. Notice that there is only a single column for breed and it comes from the dBBreed property definition.

        create table ENTITYEX_DOG (
            id integer generated by default as identity,
            BREED varchar(32),
            color varchar(16),
            gender integer,
            primary key (id)
        );
  5. Update the test method with the following to assign and test the persistence of breed using an alternate mapping.

        @Test
    
        public void testEnums() {
            logger.info("testEnums");
            Dog dog = new Dog()
                .setGender(Dog.Sex.FEMALE)
                .setColor(Dog.Color.MIX)
                .setBreed(Dog.Breed.SAINT_BERNARD);
            em.persist(dog);
            em.flush();
            
            //check the raw value stored in the database
            Object[] o = (Object[])em.createNativeQuery("select GENDER, COLOR, BREED from ENTITYEX_DOG where id=?")
                    .setParameter(1, dog.getId())
                    .getSingleResult();
            logger.debug("cols=" + Arrays.toString(o));
            assertEquals("unexpected gender", Dog.Sex.FEMALE.ordinal(), ((Number)o[0]).intValue());
            assertEquals("unexpected color", Dog.Color.MIX.name(), ((String)o[1]));
            assertEquals("unexpected breed", Dog.Breed.SAINT_BERNARD.prettyName, ((String)o[2]));
            
            //get a new instance
            em.detach(dog);
            Dog dog2 = em.find(Dog.class, dog.getId());
            assertEquals("unexpected dog gender", dog.getGender(), dog2.getGender());
            assertEquals("unexpected dog color", dog.getColor(), dog2.getColor());
            assertEquals("unexpected dog breed", dog.getBreed(), dog2.getBreed());
        }
  6. Re-run the enum test and note how text "Saint Bernard" with camel case and spaces was stored in the database and not the ordinal or SAINT_BERNARD name.

    mvn clean test -Dtest=myorg.entityex.AnimalTest#testEnums
    ...
     -cols=[1, MIX, Saint Bernard]
    ...
    [INFO] BUILD SUCCESS

You may want to verify the contents of the database using the DB UI

SELECT * FROM ENTITYEX_DOG;
ID      BREED   COLOR   GENDER  
1   Saint Bernard   MIX 1

In this chapter we took a detailed look at mapping a special property type -- the enum. You mapped it using two built-in mapping techniques (ordinal and string/name) and a custom way using alternate getters/setters and PROPERTY access. Since the additional getter/setter pair was strictly for the OR mapping, you defined them as non-public accessors to keep the interface from being polluted with OR mapping details. You also declared the enum breed attribute as @Transient so the provider knew to ignore that particular FIELD.