Способы сохранения перечислений в базе данных

Каков наилучший способ сохранить перечисления в базе данных?

Я знаю, что Java обеспечивает name() а также valueOf() методы для преобразования значений перечисления в строку и обратно. Но есть ли другие (гибкие) варианты для хранения этих значений?

Есть ли умный способ сделать перечисления в уникальные числа (ordinal() не безопасно использовать)?

Обновить:

Спасибо за все классные и быстрые ответы! Это было, как я подозревал.

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

10 ответов

Решение

Мы больше не храним перечисления как числовые порядковые значения; это делает отладку и поддержку слишком сложной. Мы сохраняем фактическое значение перечисления, преобразованное в строку:

public enum Suit { Spade, Heart, Diamond, Club }

Suit theSuit = Suit.Heart;

szQuery = "INSERT INTO Customers (Name, Suit) " +
          "VALUES ('Ian Boyd', %s)".format(theSuit.name());

и затем прочитайте обратно с:

Suit theSuit = Suit.valueOf(reader["Suit"]);

В прошлом проблема была в том, чтобы посмотреть на Enterprise Manager и попытаться расшифровать:

Name                Suit
==================  ==========
Shelby Jackson      2
Ian Boyd            1

вирши

Name                Suit
==================  ==========
Shelby Jackson      Diamond
Ian Boyd            Heart

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

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

Кроме того, если вы используете числовые значения, вы привязаны к ним. Вы не можете красиво вставить или переставить элементы, не форсируя старые числовые значения. Например, изменив перечисление Suit на:

public enum Suit { Unknown, Heart, Club, Diamond, Spade }

должен был бы стать:

public enum Suit { 
      Unknown = 4,
      Heart = 1,
      Club = 3,
      Diamond = 2,
      Spade = 0 }

чтобы поддерживать устаревшие числовые значения, хранящиеся в базе данных.

Как отсортировать их в базе данных

Возникает вопрос: допустим, я хотел заказать значения. Некоторые люди могут захотеть отсортировать их по порядковому значению перечисления. Конечно, упорядочивать карточки по числовому значению перечисления бессмысленно:

SELECT Suit FROM Cards
ORDER BY SuitID; --where SuitID is integer value(4,1,3,2,0)

Suit
------
Spade
Heart
Diamond
Club
Unknown

Это не тот порядок, который мы хотим - мы хотим, чтобы они были в порядке перечисления:

SELECT Suit FROM Cards
ORDER BY CASE SuitID OF
    WHEN 4 THEN 0 --Unknown first
    WHEN 1 THEN 1 --Heart
    WHEN 3 THEN 2 --Club
    WHEN 2 THEN 3 --Diamond
    WHEN 0 THEN 4 --Spade
    ELSE 999 END

Та же самая работа, которая требуется, если вы сохраняете целочисленные значения, требуется, если вы сохраняете строки:

SELECT Suit FROM Cards
ORDER BY Suit; --where Suit is an enum name

Suit
-------
Club
Diamond
Heart
Spade
Unknown

Но это не тот порядок, который мы хотим - мы хотим, чтобы они были в порядке перечисления:

SELECT Suit FROM Cards
ORDER BY CASE Suit OF
    WHEN 'Unknown' THEN 0
    WHEN 'Heart'   THEN 1
    WHEN 'Club'    THEN 2
    WHEN 'Diamond' THEN 3
    WHEN 'Space'   THEN 4
    ELSE 999 END

Мое мнение, что этот вид рейтинга принадлежит пользовательскому интерфейсу. Если вы сортируете элементы по значению перечисления: вы делаете что-то не так.

Но если бы вы действительно хотели это сделать, я бы создал Suits таблица размеров:

| Suit       | SuitID       | Rank          | Color  |
|------------|--------------|---------------|--------|
| Unknown    | 4            | 0             | NULL   |
| Heart      | 1            | 1             | Red    |
| Club       | 3            | 2             | Black  |
| Diamond    | 2            | 3             | Red    |
| Spade      | 0            | 4             | Black  |

Таким образом, когда вы захотите поменять свои карты, чтобы использовать новый порядокколод Kissing Kings, вы можете изменить его для отображения, не выбрасывая все свои данные:

| Suit       | SuitID       | Rank          | Color  | CardOrder |
|------------|--------------|---------------|--------|-----------|
| Unknown    | 4            | 0             | NULL   | NULL      |
| Spade      | 0            | 1             | Black  | 1         |
| Diamond    | 2            | 2             | Red    | 1         |
| Club       | 3            | 3             | Black  | -1        |
| Heart      | 1            | 4             | Red    | -1        |

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

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder

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

Костюмы столовые:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Стол игроков

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Если вы когда-либо реорганизовали свое перечисление в классы с поведением (например, приоритетом), ваша база данных уже смоделирует его правильно
  2. Ваш администратор базы данных счастлив, потому что ваша схема нормализована (хранение одного целого числа на игрока, а не всей строки, которая может иметь или не иметь опечатки).
  3. Ваши значения базы данных (suit_id) не зависят от значения перечисления, что также помогает вам работать с данными на других языках.

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

Чтобы преодолеть эту проблему, я использовал @Enumerated(EnumType.STRING) и моя цель была решена.

Например, у вас есть Enum Учебный класс:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

В классе сущности определите @Enumerated(EnumType.STRING):

@Enumerated(EnumType.STRING)
@Column(name = "Fruits")
public FurthitMethod getFuritMethod() {
    return fruitMethod;
}

public void setFruitMethod(FurthitMethod authenticationMethod) {
    this.fruitMethod= fruitMethod;
}

Пока вы пытаетесь установить значение базы данных, строковое значение будет сохранено в базе данных как "APPLE","ORANGE" или же "LEMON".

Как вы говорите, порядковый номер немного рискован. Рассмотрим для примера:

public enum Boolean {
    TRUE, FALSE
}

public class BooleanTest {
    @Test
    public void testEnum() {
        assertEquals(0, Boolean.TRUE.ordinal());
        assertEquals(1, Boolean.FALSE.ordinal());
    }
}

Если вы сохранили это как порядковые номера, у вас могут быть строки вроде:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

"Alice is a boy"      1
"Graham is a boy"     0

Но что произойдет, если вы обновите Boolean?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Это означает, что вся ваша ложь будет неверно истолкована как "файл не найден"

Лучше просто использовать строковое представление

Я бы сказал, что единственным безопасным механизмом здесь является использование строки name() значение. При записи в БД вы можете использовать sproc для вставки значения, а при чтении использовать View. Таким образом, если перечисления изменяются, в sproc/view есть уровень косвенности, чтобы можно было представить данные в виде значения перечисления, не "накладывая" это на DB.

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

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

SELECT reftable.* FROM reftable
  LEFT JOIN enumtable ON reftable.enum_ref_id = enumtable.enum_id
WHERE enumtable.enum_id IS NULL;

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

Мы просто храним само имя enum - оно более читаемо.

Мы возились с сохранением определенных значений для перечислений, где существует ограниченный набор значений, например, это перечисление, которое имеет ограниченный набор состояний, которые мы используем для представления символа (более значимого, чем числовое значение):

public enum EmailStatus {
    EMAIL_NEW('N'), EMAIL_SENT('S'), EMAIL_FAILED('F'), EMAIL_SKIPPED('K'), UNDEFINED('-');

    private char dbChar = '-';

    EmailStatus(char statusChar) {
        this.dbChar = statusChar;
    }

    public char statusChar() {
        return dbChar;
    }

    public static EmailStatus getFromStatusChar(char statusChar) {
        switch (statusChar) {
        case 'N':
            return EMAIL_NEW;
        case 'S':
            return EMAIL_SENT;
        case 'F':
            return EMAIL_FAILED;
        case 'K':
            return EMAIL_SKIPPED;
        default:
            return UNDEFINED;
        }
    }
}

и когда у вас много значений, вам нужно иметь Map внутри вашего перечисления, чтобы этот метод getFromXYZ был маленьким.

Весь мой опыт подсказывает мне, что самый безопасный способ сохранения перечислений в любом месте - это использовать дополнительное значение кода или id (своего рода эволюция ответа @jeebee). Это может быть хорошим примером идеи:

enum Race {
    HUMAN ("human"),
    ELF ("elf"),
    DWARF ("dwarf");

    private final String code;

    private Race(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

Теперь вы можете с любой настойчивостью ссылаться на ваши константы перечисления по его коду. Даже если вы решите изменить некоторые имена констант, вы всегда можете сохранить значение кода (например, DWARF("dwarf") в GNOME("dwarf"))

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

interface CodeValue {
    String getCode();
}

И пусть наш enum реализует это:

enum Race implement CodeValue {...}

Это время для волшебного метода поиска:

static <T extends Enum & CodeValue> T resolveByCode(Class<T> enumClass, String code) {
    T[] enumConstants = enumClass.getEnumConstants();
    for (T entry : enumConstants) {
        if (entry.getCode().equals(code)) return entry;
    }
    // In case we failed to find it, return null.
    // I'd recommend you make some log record here to get notified about wrong logic, perhaps.
    return null;
}

И используйте это как очарование: Race race = resolveByCode(Race.class, "elf")

Если вы сохраняете перечисления в виде строк в базе данных, вы можете создать служебные методы для (де) сериализации любого перечисления:

   public static String getSerializedForm(Enum<?> enumVal) {
        String name = enumVal.name();
        // possibly quote value?
        return name;
    }

    public static <E extends Enum<E>> E deserialize(Class<E> enumType, String dbVal) {
        // possibly handle unknown values, below throws IllegalArgEx
        return Enum.valueOf(enumType, dbVal.trim());
    }

    // Sample use:
    String dbVal = getSerializedForm(Suit.SPADE);
    // save dbVal to db in larger insert/update ...
    Suit suit = deserialize(Suit.class, dbVal);

Несколько значений с отношением ИЛИ для одного, перечисляемого поля. Концепция для.NET с хранением перечислимых типов в базе данных, таких как байты или целые числа, и использованием FlagsAttribute в вашем коде.

http://blogs.msdn.com/b/efdesign/archive/2011/06/29/enumeration-support-in-entity-framework.aspx

Вы можете использовать дополнительное значение в константе перечисления, которое может выжить как при изменении имени, так и при использовании перечислений:

public enum MyEnum {
    MyFirstValue(10),
    MyFirstAndAHalfValue(15),
    MySecondValue(20);

    public int getId() {
        return id;
    }
    public static MyEnum of(int id) {
        for (MyEnum e : values()) {
            if (id == e.id) {
                return e;
            }
        }
        return null;
    }
    MyEnum(int id) {
        this.id = id;
    }
    private final int id;
}

Чтобы получить идентификатор из перечисления:

int id = MyFirstValue.getId();

Чтобы получить перечисление из идентификатора:

MyEnum e = MyEnum.of(id);

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

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

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

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

Примечание: всегда проверяйте, что вы не разрабатываете что-то, что следует хранить в таблице базы данных и поддерживать как обычный объект. Если вы можете представить, что вам нужно добавить новые константы в перечисление на этом этапе, когда вы его настраиваете, это признак того, что вам может быть лучше вместо этого создать обычный объект и таблицу.

Другие вопросы по тегам