Storing EnumSet in a database?

前端 未结 10 1715
借酒劲吻你
借酒劲吻你 2020-12-03 21:24

So in C++/C# you can create flags enums to hold multiple values, and storing a single meaningful integer in the database is, of course, trivial.

In Java you have Enu

相关标签:
10条回答
  • 2020-12-03 21:32

    Without going into the debate about pros and cons of ordinal values in the database - I posted a possible answer to the given question here: JPA map collection of Enums

    The idea is to create a new PersistentEnumSet which uses the implementation of java.util.RegularEnumSet, but offers the elements bitmask to JPA.

    That one can than be used in an embeddable:

    @Embeddable
    public class InterestsSet extends PersistentEnumSet<InterestsEnum> {
      public InterestsSet() {
        super(InterestsEnum.class);
      }
    }
    

    And that set is used in the entity:

    @Entity
    public class MyEntity {
      // ...
      @Embedded
      private InterestsSet interests = new InterestsSet();
    }
    

    For further comments see my answer over there.

    0 讨论(0)
  • 2020-12-03 21:34

    This is an old post that I found helpful, but with Java 8 or newer I've adapted the solution posted by @finnw into this interface:

    public interface BitMaskable {
    
      int getBitMaskOrdinal();
    
      static int bitMaskValue(Set<? extends BitMaskable> set) {
        int mask = 0;
    
        for (BitMaskable val : set) {
          mask |= (1 << val.getBitMaskOrdinal());
        }
    
        return mask;
      }
    
      static <E extends Enum<E> & BitMaskable> Set<E> valueOfBitMask(int mask, Class<E> enumType) {
        E[] values = enumType.getEnumConstants();
        EnumSet<E> result = EnumSet.noneOf(enumType);
        Map<Integer, E> ordinalCache = null;
        while (mask != 0) {
          int ordinal = Integer.numberOfTrailingZeros(mask);
          mask ^= Integer.lowestOneBit(mask);
          E value = null;
          if (ordinalCache != null) {
            value = ordinalCache.get(ordinal);
          }
          if (value == null) {
            for (E e : values) {
              if (e.getBitMaskOrdinal() == ordinal) {
                value = e;
                break;
              }
              // if there are more values to decode and e has a higher
              // ordinal than what we've seen, cache that for later
              if (mask != 0 && e.getBitMaskOrdinal() > ordinal) {
                if (ordinalCache == null) {
                  ordinalCache = new HashMap<>(values.length);
                }
                ordinalCache.put(e.getBitMaskOrdinal(), e);
              }
            }
          }
          if (value != null) {
            result.add(value);
          }
        }
        return result;
      }
    
    }
    

    Usage for an enum like this (note the bmOrdinal values are out-of-order from the built-in enum ordinal values):

    public enum BitMaskEnum implements BitMaskable {
      A(0),
      B(2),
      C(1),
      D(3);
    
      private int bmOrdinal;
    
      private BitMaskEnum(int bmOrdinal) {
        this.bmOrdinal = bmOrdinal;
      }
    
      @Override
      public int getBitMaskOrdinal() {
        return bmOrdinal;
      }
    }
    

    is then along these lines:

    // encode as bit mask; result == 5
    int result = BitMaskable.bitMaskValue(EnumSet.of(BitMaskEnum.A, BitMaskEnum.B));
    
    // decode into set; result contains A & B
    Set<BitMaskEnum> result = BitMaskable.valueOfBitMask(5, BitMaskEnum.class);
    
    0 讨论(0)
  • 2020-12-03 21:36

    Storing the ordinal as a representation of the EnumSet is not a good idea. The ordinal numbers depend on the order of the definition in the Enum class (a related discussion is here). Your database may be easily broken by a refactoring that changes the order of Enum values or introduces new ones in the middle.

    You have to introduce a stable representation of individual enum values. These can be int values again and represented in the proposed way for the EnumSet.

    Your Enums can implement interfaces so the stable represenation can be directly in the enum value (adapted from Adamski):

    interface Stable{
        int getStableId();
    }
    public enum X implements Stable {
        A(1), B(2);
    
        private int stableId;
    
        X(int id){
            this.stableId = id;
        }
    
        @Override public int getStableId() {
            return stableId;
        }
    }
    

    adapted from Adamski's code:

    public <E extends Stable> int encode(EnumSet<E> set) {
      int ret = 0;
    
      for (E val : set) {
        ret |= (1 << val.getStableId());
      }
    
      return ret;
    }
    
    0 讨论(0)
  • 2020-12-03 21:41

    Providing your enum fits into an int (i.e. there are <= 32 values) I would roll my own implementation by using each enum's ordinal value; e.g.

    public <E extends Enum<E>> int encode(EnumSet<E> set) {
      int ret = 0;
    
      for (E val : set) {
        // Bitwise-OR each ordinal value together to encode as single int.
        ret |= (1 << val.ordinal());
      }
    
      return ret;
    }
    
    public <E extends Enum<E>> EnumSet<E> decode(int encoded, Class<E> enumKlazz) {
      // First populate a look-up map of ordinal to Enum value.
      // This is fairly disgusting: Anyone know of a better approach?
      Map<Integer, E> ordinalMap = new HashMap<Integer, E>();
      for (E val : EnumSet.allOf(enumKlazz)) {
        ordinalMap.put(val.ordinal(), val);
      }
    
      EnumSet<E> ret= EnumSet.noneOf(enumKlazz);
      int ordinal = 0;
    
      // Now loop over encoded value by analysing each bit independently.
      // If the bit is set, determine which ordinal that corresponds to
      // (by also maintaining an ordinal counter) and use this to retrieve
      // the correct value from the look-up map.
      for (int i=1; i!=0; i <<= 1) {
        if ((i & encoded) != 0) {
          ret.add(ordinalMap.get(ordinal));
        }
    
        ++ordinal;
      }
    
      return ret;
    }
    

    Disclaimer: I haven't tested this!

    EDIT

    As Thomas mentions in the comments the ordinal numbers are unstable in that any change to your enum definition within your code will render the encodings in your database corrupt (e.g. if you insert a new enum value in the middle of your existing definition). My approach to solving this problem is to define an "Enum" table per enumeration, containing a numerical ID (not the ordinal) and the String enum value. When my Java application starts, the first thing the DAO layer does is to read each Enum table into memory and:

    • Verify that all String enum values in the database match the Java definition.
    • Initialise a Bi-directional map of ID to enum and vice-versa, which I then use whenever I persist an enum (In other words, all "data" tables reference the database-specific Enum ID, rather than store the String value explicitly).

    This is much cleaner / more robust IMHO than the ordinal approach I describe above.

    0 讨论(0)
  • 2020-12-03 21:42

    I have done some changes on finnw's code, so it works with enumerations having up to 64 items.

    // From Adamski's answer
    public static <E extends Enum<E>> long encode(EnumSet<E> set) {
        long ret = 0;
    
        for (E val : set) {
            ret |= 1L << val.ordinal();
        }
    
        return ret;
    }
    
    @SuppressWarnings("unchecked")
    public static <E extends Enum<E>> EnumSet<E> decode(long code,
                                                         Class<E> enumType) {
        try {
            E[] values = (E[]) enumType.getMethod("values").invoke(null);
            EnumSet<E> result = EnumSet.noneOf(enumType);
            while (code != 0) {
                int ordinal = Long.numberOfTrailingZeros(code);
                code ^= Long.lowestOneBit(code);
                result.add(values[ordinal]);
            }
            return result;
        } catch (IllegalAccessException ex) {
            // Shouldn't happen
            throw new RuntimeException(ex);
        } catch (InvocationTargetException ex) {
            // Probably a NullPointerException, caused by calling this method
            // from within E's initializer.
            throw (RuntimeException) ex.getCause();
        } catch (NoSuchMethodException ex) {
            // Shouldn't happen
            throw new RuntimeException(ex);
        }
    }
    
    0 讨论(0)
  • 2020-12-03 21:45

    With the methods given in the answers it is possible to convert a integer to an EnumSet and vice versa. But I found that this is often error prone. Especially when you get negative values as java only has signed int and long. So if you plan to do such conversions on all sets of enums you might want to use a data structure that already supports this. I have created such a data structure, that can be used just like a BitSet or an EnumSet, but it also has methods such as toLong() and toBitSet(). Note that this requires Java 8 or newer.

    Here's the link: http://claude-martin.ch/enumbitset/

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