Способи збереження перерахунків у базі даних


123

Який найкращий спосіб зберегти перерахунки в базі даних?

Я знаю, що Java пропонує name()та valueOf()методи перетворення значень перерахувань у рядки та назад. Але чи існують інші (гнучкі) варіанти зберігання цих значень?

Чи є розумний спосіб зробити перерахунки на унікальні номери ( ordinal()це не безпечно для використання)?

Оновлення:

Дякую за всі приголомшливі та швидкі відповіді! Це було так, як я підозрював.

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


2
Чому ordinal () не є безпечним у використанні?
Майкл Майерс

Що це за база даних? MySQL має тип перерахунку, але я не думаю, що це стандартний ANSI SQL.
Шерм Пендлі

6
Тому що будь-які перелічені доповнення повинні бути поставлені в кінці. Легко підозрілому розробникові легко зіпсувати це і спричинити хаос
oxbow_lakes

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

Відповіді:


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

останнє набагато простіше. Перший вимагав отримати вихідний код і знайти числові значення, які були призначені членам перерахування.

Так, це займає більше місця, але імена членів перерахування короткі, а жорсткі диски - дешеві, і це набагато більше того, щоб допомогти, коли у вас виникли проблеми.

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

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 New Deck, ви можете змінити їх для відображення без викидання всіх своїх даних:

| 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;
маму

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. Ваша DBA задоволена тим, що ваша схема нормалізована (зберігання одного цілого числа на гравця, а не цілого рядка, в якому можуть бути або не бути помилки).
  3. Значення вашої бази даних ( suit_id) не залежать від вашого значення перерахунку, що допомагає вам працювати над даними також з інших мов.

14
Хоча я погоджуюся, що приємно, щоб він нормалізувався і обмежився в БД, але це призводить до оновлень у двох місцях, щоб додати нове значення (код та db), що може спричинити більше накладних витрат. Крім того, орфографічні помилки повинні бути відсутніми, якщо всі оновлення виконуються програмно від імені Enum.
Джейсон

3
Я згоден з коментарем вище. Альтернативним механізмом примусового виконання на рівні бази даних було б написання тригера обмеження, який би відхиляв вставки або оновлення, які намагаються використовувати недійсне значення.
Стів Перкінс

1
Чому я б хотів оголосити одну і ту ж інформацію в двох місцях? І в CODE, public enum foo {bar}і CREATE TABLE foo (name varchar);це може легко вийти з синхронізації.
ebyrob

Якщо взяти прийняту відповідь за номіналом, тобто імена перерахунків використовуються лише для ручного дослідження, то ця відповідь справді є найкращим варіантом. Крім того, якщо ви перейдете до зміни порядку числення чи значень чи імен, у вас завжди буде набагато більше проблем, ніж із підтримкою цієї додаткової таблиці. Особливо, коли вам це потрібно лише (і ви можете створити лише тимчасово) для налагодження та підтримки.
afk5min

5

Я заперечую, що єдиний безпечний механізм тут - використовувати name()значення String . Під час запису в БД, ви можете використовувати відросток, щоб вставити значення, а під час читання використовувати View. Таким чином, якщо перерахунки змінюються, в проростку / виду існує рівень непрямості, щоб можна було представити дані як значення перерахунку, не "накладаючи" це на БД.


1
Я з великим успіхом використовую гібридний підхід до вашого рішення та рішення @Ian Boyd. Дякую за пораду!
техномалогічний

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 байти х 1 мільйон рядків - 3 МБ.
Ян Бойд

@IanBoyd: Але, enumIdбезумовно, вміщується в два байти (довші перерахунки неможливі на Java), і більшість з них вміщуються в один байт (який підтримують деякі БД). Заощаджений простір мізерно малий, але швидше порівняння та фіксована довжина повинні допомогти.
maaartinus

3

Ми просто зберігаємо саме ім’я 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;
        }
    }
}

і коли у вас багато значень, вам потрібно мати Карту всередині перерахунку, щоб цей метод getFromXYZ малий.


Якщо ви не хочете підтримувати оператор перемикання і можете переконатися, що dbChar унікальний, ви можете використовувати щось на зразок: загальнодоступний статичний EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (НЕ ВКАЗАНО); }
Кучі

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 в базі даних замість звичайного значення.

Щоб над цим питанням я вже скористався @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".



0

Ви можете використовувати додаткове значення в константі enum, яке може пережити як зміни назви, так і вдавання переліків:

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

Щоб отримати ідентифікатор від enum:

int id = MyFirstValue.getId();

Щоб отримати перерахунок з ідентифікатора:

MyEnum e = MyEnum.of(id);

Я пропоную використовувати значення без сенсу, щоб уникнути плутанини, якщо імена перерахунків потрібно змінити.

У наведеному вище прикладі я використав деякий варіант залишення пробілів "Основна рядок", тому цифри, ймовірно, залишатимуться в тому ж порядку, що і перерахунки.

Ця версія швидша, ніж використання вторинної таблиці, але вона робить систему більш залежною від знань коду та вихідного коду.

Щоб виправити це, ви також можете створити таблицю з ідентифікаторами перерахунків у базі даних. Або йдіть іншим шляхом і виберіть ідентифікатори для переліків із таблиці, додаючи до неї рядки.

Sidenote : Завжди переконайтеся, що ви не проектуєте те, що повинно зберігатися в таблиці бази даних і хоча підтримуватися як звичайний об'єкт. Якщо ви можете уявити, що вам потрібно додати нові константи до перерахунку в цей момент, коли ви його налаштовуєте, це свідчить про те, що вам може бути краще створити звичайний об’єкт і таблицю замість цього.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.