Подтвердить что ты не робот

Хранение EnumSet в базе данных?

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

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

4b9b3361

Ответ 1

// From Adamski 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 initializer.
        throw (RuntimeException) ex.getCause();
    } catch (NoSuchMethodException ex) {
        // Shouldn't happen
        throw new RuntimeException(ex);
    }
}

Ответ 2

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

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

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

Ответ 3

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

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

Отказ от ответственности. Я не тестировал это!

ИЗМЕНИТЬ

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

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

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

Ответ 4

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

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

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

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

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

Ответ 5

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

Ответ 6

Это старое сообщение, которое я нашел полезным, но с 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;
  }

}

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

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

Ответ 7

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

Здесь ссылка: http://claude-martin.ch/enumbitset/

Ответ 8

Не вдаваясь в дебаты о плюсах и минусах порядковых значений в базе данных - я опубликовал возможный ответ на данный вопрос здесь: Коллекция карт 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();
}

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

Ответ 9

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

// From Adamski 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 initializer.
        throw (RuntimeException) ex.getCause();
    } catch (NoSuchMethodException ex) {
        // Shouldn't happen
        throw new RuntimeException(ex);
    }
}