Sposoby zapisywania wyliczeń w bazie danych

123

Jaki jest najlepszy sposób zapisywania wyliczeń w bazie danych?

Wiem, że Java zapewnia name()i valueOf()metody konwersji wartości wyliczenia na ciąg znaków iz powrotem. Ale czy są jakieś inne (elastyczne) opcje przechowywania tych wartości?

Czy istnieje sprytny sposób przekształcania wyliczeń w unikatowe liczby ( ordinal()nie jest bezpieczny w użyciu)?

Aktualizacja:

Dzięki za wszystkie niesamowite i szybkie odpowiedzi! Było tak, jak podejrzewałem.

Jednak uwaga do „zestawu narzędzi”; To jest jeden sposób. Problem w tym, że musiałbym dodać te same metody do każdego tworzonego przeze mnie typu Enum. To dużo zduplikowanego kodu, a obecnie Java nie obsługuje żadnych rozwiązań w tym zakresie (wyliczenie Java nie może rozszerzać innych klas).

user20298
źródło
2
Dlaczego użycie ordinal () nie jest bezpieczne?
Michael Myers
Jaki rodzaj bazy danych? MySQL ma typ wyliczeniowy, ale nie sądzę, że jest to standardowy ANSI SQL.
Sherm Pendley,
6
Ponieważ wszelkie wyliczeniowe dodatki trzeba wtedy umieścić na końcu.
Niczego
1
Widzę. Chyba dobrze, że nie zajmuję się zbytnio bazami danych, ponieważ prawdopodobnie nie pomyślałbym o tym, dopóki nie było za późno.
Michael Myers

Odpowiedzi:

165

Nigdy już nie przechowujemy wyliczeń jako liczbowych wartości porządkowych; to sprawia, że ​​debugowanie i wsparcie jest zbyt trudne. Przechowujemy rzeczywistą wartość wyliczenia przekonwertowaną na ciąg:

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

Suit theSuit = Suit.Heart;

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

a następnie przeczytaj:

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

W przeszłości problem polegał na wpatrywaniu się w Enterprise Manager i próbach rozszyfrowania:

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

wersety

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

ta ostatnia jest znacznie łatwiejsza. Pierwsza wymagała dotarcia do kodu źródłowego i znalezienia wartości liczbowych, które zostały przypisane członkom wyliczenia.

Tak, zajmuje więcej miejsca, ale nazwy członków wyliczenia są krótkie, a dyski twarde są tanie i o wiele bardziej opłaca się pomóc, gdy masz problem.

Ponadto, jeśli używasz wartości liczbowych, jesteś z nimi związany. Nie można ładnie wstawiać ani zmieniać kolejności elementów bez konieczności wymuszania starych wartości liczbowych. Na przykład zmiana wyliczenia koloru na:

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

musiałby się stać:

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

w celu zachowania starszych wartości liczbowych przechowywanych w bazie danych.

Jak posortować je w bazie danych

Pojawia się pytanie: powiedzmy, że chciałem uporządkować wartości. Niektórzy ludzie mogą chcieć posortować je według wartości porządkowej wyliczenia. Oczywiście porządkowanie kart według wartości numerycznej wyliczenia jest bez znaczenia:

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

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

To nie jest kolejność, której chcemy - chcemy, aby były one w kolejności wyliczania:

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

Ta sama praca, która jest wymagana przy zapisywaniu wartości całkowitych, jest wymagana przy zapisywaniu ciągów:

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

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

Ale to nie jest kolejność, której chcemy - chcemy, aby były one w kolejności wyliczania:

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

Uważam, że taki ranking należy do interfejsu użytkownika. Jeśli sortujesz elementy na podstawie ich wartości wyliczenia: robisz coś źle.

Ale jeśli naprawdę chcesz to zrobić, utworzyłbym Suitstabelę wymiarów:

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

W ten sposób, jeśli chcesz zmienić swoje karty, aby używać Kissing Kings New Deck Order , możesz to zmienić do celów wyświetlania bez wyrzucania wszystkich swoich danych:

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

Teraz oddzielamy wewnętrzny szczegół programowania (nazwa wyliczenia, wartość wyliczenia) ustawieniem wyświetlania przeznaczonym dla użytkowników:

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
Ian Boyd
źródło
23
toString jest często zastępowane, aby zapewnić wartość wyświetlaną. name () jest lepszym wyborem, ponieważ z definicji jest odpowiednikiem valueOf ()
ddimitrov
9
Zdecydowanie się z tym nie zgadzam, jeśli wymagana jest trwałość wyliczenia, nie należy utrwalać nazw. jeśli chodzi o odczytanie go wstecz, jest to jeszcze prostsze z wartością zamiast nazwy, można ją po prostu wpisać jako SomeEnum enum1 = (SomeEnum) 2;
mamu
3
mamu: Co się dzieje, gdy zmieniają się ekwiwalenty liczbowe?
Ian Boyd
2
Zniechęciłbym każdego stosującego takie podejście. Powiązanie się z reprezentacją ciągów ogranicza elastyczność kodu i jego refaktoryzację. Lepiej używaj unikalnych identyfikatorów. Również przechowywanie sznurków marnuje przestrzeń magazynową.
Tautvydas
2
@LuisGouveia Zgadzam się z tobą, że czas może się podwoić. Spowodowanie zapytania, które 12.37 mszamiast tego trwa 12.3702 ms. To właśnie mam na myśli mówiąc „w hałasie” . Uruchom zapytanie ponownie i trwa 13.29 ms, lub 11.36 ms. Innymi słowy, losowość harmonogramu wątków drastycznie zapełni każdą mikrooptymalizację, którą teoretycznie posiadasz, a która nie jest w żaden sposób widoczna dla nikogo w żaden sposób.
Ian Boyd
42

Jeśli nie masz konkretnych powodów wydajnościowych, aby tego uniknąć, zalecałbym użycie osobnej tabeli do wyliczenia. Użyj integralności klucza obcego, chyba że dodatkowe wyszukiwanie naprawdę cię zabije.

Tabela garniturów:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Tabela graczy

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Jeśli kiedykolwiek zmienisz wyliczenie na klasy z zachowaniem (takim jak priorytet), Twoja baza danych już je modeluje poprawnie
  2. Twój DBA jest zadowolony, ponieważ Twój schemat jest znormalizowany (przechowuje jedną liczbę całkowitą na gracza, zamiast całego ciągu znaków, który może zawierać literówki lub nie).
  3. Twoje wartości bazy danych ( suit_id) są niezależne od wartości wyliczenia, co ułatwia pracę z danymi z innych języków.
Tomek
źródło
14
Chociaż zgadzam się, że miło jest mieć to znormalizowane i ograniczone w bazie danych, powoduje to aktualizacje w dwóch miejscach, aby dodać nową wartość (kod i db), co może spowodować większe obciążenie. Ponadto błędy ortograficzne nie powinny istnieć, jeśli wszystkie aktualizacje są wykonywane programowo z nazwy Enum.
Jason
3
Zgadzam się z powyższym komentarzem. Alternatywnym mechanizmem egzekwowania na poziomie bazy danych byłoby napisanie wyzwalacza ograniczenia, który odrzucałby wstawienia lub aktualizacje próbujące użyć nieprawidłowej wartości.
Steve Perkins
1
Dlaczego miałbym podawać te same informacje w dwóch miejscach? Zarówno w CODE, jak public enum foo {bar}i CREATE TABLE foo (name varchar);to może łatwo stracić synchronizację.
ebyrob
Jeśli przyjmiemy zaakceptowaną odpowiedź za wartość nominalną, to znaczy, że nazwy wyliczeń są używane tylko do ręcznych badań, to ta odpowiedź jest rzeczywiście najlepszą opcją. Ponadto, jeśli będziesz dalej zmieniać kolejność wyliczeń, wartości lub nazwy, zawsze będziesz miał znacznie więcej problemów niż utrzymywanie tej dodatkowej tabeli. Zwłaszcza, gdy potrzebujesz go (i możesz zdecydować się na tworzenie tylko tymczasowo) do debugowania i wsparcia.
afk5min
5

Twierdziłbym, że jedynym bezpiecznym mechanizmem jest tutaj użycie name()wartości String . Podczas pisania do bazy danych można użyć sproc do wstawienia wartości, a podczas czytania użyć widoku. W ten sposób, jeśli wyliczenia ulegną zmianie, istnieje pewien poziom pośredni w sproc / widoku, aby móc przedstawić dane jako wartość wyliczenia bez „narzucania” tego na DB.

oxbow_lakes
źródło
1
Korzystam z hybrydowego podejścia Twojego rozwiązania i rozwiązania @Ian Boyd z wielkim sukcesem. Dzięki za wskazówkę!
technomalogiczne
5

Jak mówisz, porządek jest nieco ryzykowny. Rozważmy na przykład:

public enum Boolean {
    TRUE, FALSE
}

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

Jeśli zapisałeś to jako liczby porządkowe, możesz mieć wiersze takie jak:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

Ale co się stanie, jeśli zaktualizujesz wartość Boolean?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Oznacza to, że wszystkie twoje kłamstwa zostaną błędnie zinterpretowane jako „nie znaleziono pliku”

Lepiej po prostu użyć reprezentacji ciągu

zestaw narzędzi
źródło
4

W przypadku dużej bazy danych niechętnie tracę zalety rozmiaru i szybkości reprezentacji numerycznej. Często kończę z tabelą bazy danych reprezentującą Enum.

Możesz wymusić spójność bazy danych, deklarując klucz obcy - chociaż w niektórych przypadkach może być lepiej nie deklarować tego jako ograniczenia klucza obcego, które nakłada koszt na każdą transakcję. Możesz zapewnić spójność, okresowo sprawdzając, w wybranych przez siebie momentach:

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

Druga połowa tego rozwiązania polega na napisaniu kodu testowego, który sprawdza, czy wyliczenie Java i tabela wyliczeń bazy danych mają tę samą zawartość. Pozostawiamy to jako ćwiczenie dla czytelnika.

Roger Hayes
źródło
1
Powiedzmy, że średnia długość nazwy wyliczenia wynosi 7 znaków. Twoje enumIDto cztery bajty, więc masz dodatkowe trzy bajty na wiersz, używając nazw. 3 bajty x 1 milion wierszy to 3 MB.
Ian Boyd
@IanBoyd: Ale z enumIdpewnością mieści się w dwóch bajtach (dłuższe wyliczenia nie są możliwe w Javie), a większość z nich mieści się w jednym bajcie (który jest obsługiwany przez niektóre bazy danych). Oszczędność miejsca jest znikoma, ale szybsze porównanie i ustalona długość powinny pomóc.
maaartinus
3

Po prostu przechowujemy samą nazwę wyliczenia - jest bardziej czytelna.

Mieliśmy kłopoty z przechowywaniem określonych wartości dla wyliczeń, w których istnieje ograniczony zestaw wartości, np. To wyliczenie, które ma ograniczony zestaw statusów, które reprezentujemy za pomocą znaku (bardziej znaczące niż wartość liczbowa):

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

a jeśli masz dużo wartości, musisz mieć Map w swoim wyliczeniu, aby metoda getFromXYZ była mała.

JeeBee
źródło
Jeśli nie chcesz utrzymywać instrukcji switch i możesz zapewnić unikalność dbChar, możesz użyć czegoś takiego: public static EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (NIEZDEFINIOWANE); }
Kuchi
2

W przypadku zapisywania wyliczeń jako ciągów w bazie danych można utworzyć metody narzędziowe do (de) serializacji dowolnego wyliczenia:

   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);
Dov Wasserman
źródło
Przyjemnie jest używać tego z domyślną wartością wyliczenia, aby powrócić do deserializacji. Na przykład złap IllegalArgEx i zwróć Suit.None.
Jason
2

Z całego mojego doświadczenia wynika, że ​​najbezpieczniejszym sposobem na utrwalenie wyliczeń w dowolnym miejscu jest użycie dodatkowej wartości kodu lub identyfikatora (pewnego rodzaju ewolucja odpowiedzi @jeebee). To mógłby być dobry przykład pomysłu:

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

    private final String code;

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

    public String getCode() {
        return code;
    }
}

Teraz możesz korzystać z dowolnej trwałości odwołującej się do stałych wyliczenia za pomocą kodu. Nawet jeśli zdecydujesz się zmienić niektóre nazwy stałych, zawsze możesz zapisać wartość kodu (np. DWARF("dwarf")Do GNOME("dwarf"))

Ok, zanurkuj trochę głębiej z tą koncepcją. Oto kilka narzędzi, które pomogą Ci znaleźć dowolną wartość wyliczenia, ale najpierw rozszerzymy nasze podejście.

interface CodeValue {
    String getCode();
}

I niech nasz enum to zaimplementuje:

enum Race implement CodeValue {...}

To jest czas na magiczną metodę wyszukiwania:

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

I użyj go jak uroku: Race race = resolveByCode(Race.class, "elf")

Metafora
źródło
2

Napotkałem ten sam problem, w którym moim celem jest utrwalenie wartości ciągu Enum w bazie danych zamiast wartości porządkowej.

Aby rozwiązać ten problem, użyłem @Enumerated(EnumType.STRING)i mój cel został rozwiązany.

Na przykład masz Enumklasę:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

W klasie jednostki zdefiniuj @Enumerated(EnumType.STRING):

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

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

Podczas próby ustawienia wartości na bazę danych wartość ciągu zostanie utrwalona w bazie danych jako „ APPLE”, „ ORANGE” lub „ LEMON”.

SaravanaC
źródło
0

Możesz użyć dodatkowej wartości w stałej wyliczeniowej, która może przetrwać zarówno zmiany nazwy, jak i ponowne wykorzystanie wyliczeń:

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

Aby uzyskać identyfikator z wyliczenia:

int id = MyFirstValue.getId();

Aby uzyskać wyliczenie z identyfikatora:

MyEnum e = MyEnum.of(id);

Sugeruję użycie wartości bez znaczenia, aby uniknąć nieporozumień, jeśli nazwy wyliczeń muszą zostać zmienione.

W powyższym przykładzie użyłem pewnego wariantu „Podstawowej numeracji wierszy”, pozostawiając spacje, więc liczby prawdopodobnie pozostaną w tej samej kolejności co wyliczenia.

Ta wersja jest szybsza niż użycie tabeli pomocniczej, ale sprawia, że ​​system jest bardziej zależny od kodu i znajomości kodu źródłowego.

Aby temu zaradzić, możesz również skonfigurować tabelę z identyfikatorami wyliczeń w bazie danych. Lub przejdź w drugą stronę i wybierz identyfikatory wyliczeń z tabeli, dodając do niej wiersze.

Uwaga dodatkowa : zawsze sprawdzaj, czy nie projektujesz czegoś, co powinno być przechowywane w tabeli bazy danych i utrzymywane jak zwykły obiekt. Jeśli możesz sobie wyobrazić, że musisz dodać nowe stałe do wyliczenia w tym momencie, kiedy je konfigurujesz, jest to wskazówka, że ​​może lepiej będzie utworzyć zamiast tego zwykły obiekt i tabelę.

Erk
źródło