Хранение EnumSet в базе данных? - PullRequest
18 голосов
/ 04 февраля 2010

Таким образом, в C ++ / C # вы можете создавать перечисления флагов для хранения нескольких значений, и сохранение одного значащего целого числа в базе данных, конечно, тривиально.

В Java у вас есть EnumSets, который является довольно хорошим способом передачи перечислений в памяти, но как вы выводите объединенный EnumSet в целое число для хранения? Есть ли другой способ приблизиться к этому?

Ответы [ 10 ]

16 голосов
/ 04 февраля 2010

Хранить порядковый номер как представление EnumSet не очень хорошая идея. Порядковые числа зависят от порядка определения в классе Enum ( соответствующее обсуждение здесь ). Ваша база данных может быть легко взломана с помощью рефакторинга, который изменяет порядок значений Enum или вводит новые в середине.

Вы должны ввести стабильное представление отдельных значений перечисления. Они могут снова быть значениями типа int и представлены предложенным способом для EnumSet .

Ваши Enums могут реализовывать интерфейсы, поэтому стабильное представление может быть непосредственно в значении enum (адаптировано из 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;
    }
}

адаптировано из кода Адамски:

public <E extends Stable> int encode(EnumSet<E> set) {
  int ret = 0;

  for (E val : set) {
    ret |= (1 << val.getStableId());
  }

  return ret;
}
12 голосов
/ 04 февраля 2010

Если ваше перечисление вписывается в int (то есть <= 32 значения), я бы развернул собственную реализацию, используя порядковый номер каждого перечисления; например, </p>

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;
}

Отказ от ответственности : я не проверял это!

EDIT

Как Томас упоминает в комментариях, порядковые номера нестабильны в том смысле, что любое изменение вашего определения enum в вашем коде приведет к повреждению кодировок в вашей базе данных (например, если вы вставите новое значение enum в середину вашего существующего определения) , Мой подход к решению этой проблемы заключается в определении таблицы «Enum» для каждого перечисления, содержащей числовой идентификатор ( не порядковый номер ) и значение перечисления String. Когда мое приложение Java запускается, первое, что делает слой DAO, - это читает каждую таблицу Enum в память и:

  • Убедитесь, что все значения перечисления String в базе данных соответствуют определению Java.
  • Инициализировать двунаправленную карту идентификатора для перечисления и наоборот, которую я затем использую всякий раз, когда сохраняю перечисление (Другими словами, все таблицы «данных» ссылаются на специфичный для базы данных идентификатор перечисления, а не хранят строку значение явно).

Это намного чище / надежнее ИМХО, чем порядковый подход, который я описал выше.

7 голосов
/ 04 февраля 2010
// From Adamski's answer
public static <E extends Enum<E>> int encode(EnumSet<E> set) {
    int ret = 0;

    for (E val : set) {
        ret |= 1 << val.ordinal();
    }

    return ret;
}

@SuppressWarnings("unchecked")
private static <E extends Enum<E>> EnumSet<E> decode(int code,
        Class<E> enumType) {
    try {
        E[] values = (E[]) enumType.getMethod("values").invoke(null);
        EnumSet<E> result = EnumSet.noneOf(enumType);
        while (code != 0) {
            int ordinal = Integer.numberOfTrailingZeros(code);
            code ^= Integer.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);
    }
}
5 голосов
/ 04 февраля 2010

Если вы посмотрите в источнике RegularEnumSet, который является реализацией для членов Enum <= 64, вы увидите, что он содержит: </p>

/**
 * Bit vector representation of this set.  The 2^k bit indicates the
 * presence of universe[k] in this set.
 */
private long elements = 0L;

elements - это битовая маска, где битовые позиции равны порядковым номерам enum, что именно то, что вам нужно. Однако этот атрибут не доступен через метод получения или установки, поскольку он не будет соответствовать эквивалентным методам доступа для JumboEnumSet.

Это не одно из самых хороших решений, но если вам нужна простота и скорость, вы можете создать 2 статических служебных метода, которые извлекают и устанавливают атрибут elements, используя отражение.

Для меня, вероятно, я бы просто настроил класс констант, содержащий значения перечисления в виде целочисленных констант, где я могу быть уверен, какому перечислению присваивается какой бит.

4 голосов
/ 30 января 2018

Меня удивило, что никто не предлагал библиотеку в хорошем состоянии вместо того, чтобы писать свою. Приведенный выше ответ точен и полезен, но он просто побуждает людей копировать и вставлять код (тогда в основном забывают о кредитах).

Вот мои 2 цента:

EnumSet<YourEnum> mySet = EnumSet.of(YourEnum.FIRST);
long vector = EnumUtils.generateBitVector(YourEnum.class, mySet);
EnumSet<YourEnum> sameSet = EnumUtils.processBitVector(YourEnum.class, vector);

См. https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/EnumUtils.html

3 голосов
/ 04 февраля 2010

EnumSet реализует Serializable, но если вы используете это, то возникает много издержек (записывается как массив идентификаторов, а не BitSet, как вы могли бы ожидать, плюс заголовок потока объекта.)

2 голосов
/ 03 мая 2017

Это старый пост, который я нашел полезным, но с Java 8 или новее я адаптировал решение, опубликованное @finnw, в этот интерфейс:

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;
  }

}

Использование для такого перечисления ( примечание значения bmOrdinal не соответствуют порядковым номерам встроенных перечислений):

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;
  }
}

в таком случае:

// 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);
1 голос
/ 26 августа 2015

Я внес некоторые изменения в код finnw, поэтому он работает с перечислениями, имеющими до 64 элементов.

// 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);
    }
}
1 голос
/ 28 января 2015

Не вдаваясь в дебаты о плюсах и минусах порядковых значений в базе данных - я разместил возможный ответ на данный вопрос здесь: JPA карта коллекции Enums

Идея состоит в том, чтобы создать новый PersistentEnumSet, который использует реализацию java.util.RegularEnumSet, но предлагает elements битовую маску для JPA.

Что можно использовать в встраиваемом объекте:

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

И этот набор используется в сущности:

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

Для дальнейших комментариев смотрите мой ответ там.

1 голос
/ 26 апреля 2014

С помощью методов, приведенных в ответах, можно преобразовать целое число в EnumSet и наоборот.Но я обнаружил, что это часто подвержено ошибкам.Особенно, когда вы получаете отрицательные значения, поскольку java имеет только int и long со знаком.Поэтому, если вы планируете выполнять такие преобразования для всех наборов перечислений, вы можете использовать структуру данных, которая уже поддерживает это.Я создал такую ​​структуру данных, которую можно использовать так же, как BitSet или EnumSet, но у нее также есть такие методы, как toLong () и toBitSet ().Обратите внимание, что для этого требуется Java 8 или новее.

Вот ссылка: http://claude -martin.ch / enumbitset /

...