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

123

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

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

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

Обновить:

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

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

user20298
источник
2
Почему использование ordinal () небезопасно?
Майкл Майерс
Что за база данных? MySQL имеет тип enum, но я не думаю, что это стандартный ANSI SQL.
Шерм Пендли,
6
Потому что любые перечисляющие дополнения потом надо ставить в конец. Ничего не подозревающий разработчик легко испортит это и вызовет хаос
oxbow_lakes,
1
Понимаю. Думаю, это хорошо, что я мало занимаюсь базами данных, потому что, вероятно, я бы не подумал об этом, пока не стало слишком поздно.
Майкл Майерс
Обратитесь к softwareengineering.stackexchange.com/questions/298472/…
Кишан Вайшнав,

Ответы:

165

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

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
Ян Бойд
источник
23
toString часто переопределяется для предоставления отображаемого значения. name () - лучший выбор, так как он по определению является аналогом valueOf ()
ddimitrov
9
Я категорически не согласен с этим, если требуется постоянство перечисления, тогда не следует сохранять имена. что касается обратного чтения, это еще проще со значением вместо имени, можно просто привести его к типу SomeEnum enum1 = (SomeEnum) 2;
mamu
3
mamu: Что происходит, когда меняются числовые эквиваленты?
Ян Бойд,
2
Я бы отговорил любого, кто использует этот подход. Привязка к строковому представлению ограничивает гибкость кода и рефакторинг. Лучше использовать уникальные идентификаторы. Кроме того, хранение строк тратит впустую пространство для хранения.
Tautvydas
2
@LuisGouveia Я согласен с вами, что время может удвоиться. Вызывая запрос, который 12.37 msвместо этого принимает 12.3702 ms. Вот что я имею в виду под «в шуме» . Вы снова запускаете запрос, и он принимает 13.29 ms, или 11.36 ms. Другими словами, случайность планировщика потоков резко заглушит любую теоретически имеющуюся микрооптимизацию, которая никоим образом не видна никому и никогда.
Ян Бойд
42

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

Таблица костюмов:

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) не зависят от значения вашего перечисления, что помогает вам работать с данными из других языков.
Том
источник
14
Хотя я согласен, что это хорошо, если его нормализовать и ограничить в БД, это вызывает обновления в двух местах для добавления нового значения (код и БД), что может вызвать дополнительные накладные расходы. Кроме того, орфографические ошибки должны отсутствовать, если все обновления выполняются программно из имени Enum.
Джейсон
3
Я согласен с комментарием выше. Альтернативным механизмом принудительного исполнения на уровне базы данных было бы создание триггера ограничения, который отклонял бы вставки или обновления, которые пытаются использовать недопустимое значение.
Стив Перкинс,
1
Зачем мне указывать одну и ту же информацию в двух местах? И в КОДЕ, public enum foo {bar}и CREATE TABLE foo (name varchar);это может легко рассинхронизироваться.
ebyrob
Если мы примем принятый ответ за чистую монету, то есть имена перечислений используются только для ручных исследований, то этот ответ действительно лучший вариант. Кроме того, если вы продолжите изменять порядок перечисления или значения или имена, у вас всегда будет гораздо больше проблем, чем с поддержанием этой дополнительной таблицы. Особенно, когда он вам нужен (и вы можете создать его только временно) для отладки и поддержки.
afk5min 01
5

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

oxbow_lakes
источник
1
Я с большим успехом использую гибридный подход вашего решения и решения @Ian Boyd. Спасибо за чаевые!
technomalogical
5

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

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
}

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

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

Инструментарий
источник
4

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

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

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

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

Роджер Хейс
источник
1
Скажем, средняя длина имени перечисления составляет 7 символов. У вас enumIDчетыре байта, поэтому у вас есть дополнительные три байта на строку, используя имена. 3 байта x 1 миллион строк составляют 3 МБ.
Ян Бойд
@IanBoyd: Но, enumIdбезусловно, помещается в два байта (более длинные перечисления невозможны в Java), и большинство из них умещается в одном байте (который поддерживается некоторыми БД). Экономия места незначительна, но более быстрое сравнение и фиксированная длина должны помочь.
maaartinus
3

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

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

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 был небольшим.

JeeBee
источник
Если вы не хотите поддерживать оператор переключения и можете гарантировать уникальность dbChar, вы можете использовать что-то вроде: public static EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (НЕ ОПРЕДЕЛЕННО); }
Kuchi
2

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

   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);
Дов Вассерман
источник
Приятно использовать это со значением перечисления по умолчанию, чтобы вернуться к десериализации. Например, поймайте IllegalArgEx и верните Suit.None.
Джейсон
2

Весь мой опыт подсказывает мне, что самый безопасный способ сохранения перечислений в любом месте - это использовать дополнительное значение кода или идентификатор (своего рода эволюция ответа @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 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")

метафора
источник
2

Я столкнулся с той же проблемой, когда моя цель - сохранить значение 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".

SaravanaC
источник
0

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

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

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

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

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

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

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

простофиля
источник