Möglichkeiten zum Speichern von Aufzählungen in der Datenbank

123

Was ist der beste Weg, um Aufzählungen in einer Datenbank zu speichern?

Ich weiß, dass Java Methoden name()und valueOf()Methoden zum Konvertieren von Enum-Werten in einen String und zurück bereitstellt . Aber gibt es noch andere (flexible) Optionen zum Speichern dieser Werte?

Gibt es eine clevere Möglichkeit, Aufzählungen in eindeutige Zahlen umzuwandeln ( ordinal()ist nicht sicher zu verwenden)?

Aktualisieren:

Vielen Dank für alle tollen und schnellen Antworten! Es war wie ich vermutet hatte.

Allerdings ein Hinweis zum 'Toolkit'; Das ist eine Möglichkeit. Das Problem ist, dass ich jedem von mir erstellten Aufzählungstyp die gleichen Methoden hinzufügen müsste. Das ist viel duplizierter Code und im Moment unterstützt Java keine Lösungen dafür (eine Java-Aufzählung kann andere Klassen nicht erweitern).

user20298
quelle
2
Warum ist ordinal () nicht sicher zu verwenden?
Michael Myers
Was für eine Datenbank? MySQL hat einen Aufzählungstyp, aber ich glaube nicht, dass es Standard-ANSI-SQL ist.
Sherm Pendley
6
Denn alle aufzählenden Ergänzungen müssen dann am Ende gesetzt werden. Leicht für einen ahnungslosen Entwickler, dies durcheinander zu bringen und Chaos zu verursachen
oxbow_lakes
1
Aha. Ich denke, es ist gut, dass ich mich nicht viel mit Datenbanken beschäftige, weil ich wahrscheinlich nicht daran gedacht hätte, bis es zu spät war.
Michael Myers

Antworten:

165

Wir speichern Aufzählungen nie mehr als numerische Ordnungswerte. es macht das Debuggen und die Unterstützung viel zu schwierig. Wir speichern den tatsächlichen Aufzählungswert, der in eine Zeichenfolge konvertiert wurde:

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

Suit theSuit = Suit.Heart;

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

und dann zurücklesen mit:

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

Das Problem bestand in der Vergangenheit darin, Enterprise Manager anzustarren und zu entschlüsseln:

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

Verse

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

Letzteres ist viel einfacher. Ersteres erforderte das Aufrufen des Quellcodes und das Finden der numerischen Werte, die den Aufzählungselementen zugewiesen wurden.

Ja, es nimmt mehr Platz in Anspruch, aber die Namen der Aufzählungsmitglieder sind kurz und die Festplatten sind billig, und es lohnt sich viel mehr, bei Problemen zu helfen.

Wenn Sie numerische Werte verwenden, sind Sie außerdem an diese gebunden. Sie können die Elemente nicht gut einfügen oder neu anordnen, ohne die alten Zahlenwerte erzwingen zu müssen. Ändern Sie beispielsweise die Anzugaufzählung in:

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

müsste werden:

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

um die in der Datenbank gespeicherten alten numerischen Werte beizubehalten.

So sortieren Sie sie in der Datenbank

Die Frage kommt auf: Nehmen wir an, ich wollte die Werte bestellen. Einige Leute möchten sie möglicherweise nach dem Ordnungswert der Aufzählung sortieren. Natürlich ist es bedeutungslos, die Karten nach dem numerischen Wert der Aufzählung zu ordnen:

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

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

Das ist nicht die Reihenfolge, die wir wollen - wir wollen sie in Aufzählungsreihenfolge:

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

Die gleiche Arbeit, die zum Speichern von Ganzzahlwerten erforderlich ist, ist zum Speichern von Zeichenfolgen erforderlich:

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

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

Aber das ist nicht die Reihenfolge, die wir wollen - wir wollen sie in Aufzählungsreihenfolge:

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

Meiner Meinung nach gehört diese Art von Ranking in die Benutzeroberfläche. Wenn Sie Elemente nach ihrem Aufzählungswert sortieren: Sie machen etwas falsch.

Aber wenn Sie das wirklich tun wollten, würde ich eine SuitsDimensionstabelle erstellen :

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

Wenn Sie Ihre Karten ändern möchten, um Kissing Kings New Deck Order zu verwenden , können Sie sie auf diese Weise zu Anzeigezwecken ändern, ohne alle Ihre Daten wegzuwerfen:

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

Jetzt trennen wir ein internes Programmierdetail (Aufzählungsname, Aufzählungswert) mit einer Anzeigeeinstellung, die für Benutzer bestimmt ist:

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
Ian Boyd
quelle
23
toString wird häufig überschrieben, um den Anzeigewert bereitzustellen. name () ist eine bessere Wahl, da es per Definition das Gegenstück zu valueOf () ist
ddimitrov
9
Ich bin damit nicht einverstanden. Wenn eine Aufzählung von Persistenzen erforderlich ist, sollten Namen nicht beibehalten werden. Was das Zurücklesen angeht, ist es noch einfacher, wenn der Wert anstelle des Namens als SomeEnum enum1 = (SomeEnum) 2 typisiert wird.
Mamu
3
mamu: Was passiert, wenn sich die numerischen Äquivalente ändern?
Ian Boyd
2
Ich würde jeden entmutigen, der diesen Ansatz verwendet. Wenn Sie sich an die Zeichenfolgendarstellung binden, wird die Codeflexibilität und das Refactoring eingeschränkt. Sie sollten besser eindeutige IDs verwenden. Durch das Speichern von Zeichenfolgen wird außerdem Speicherplatz verschwendet.
Tautvydas
2
@ LuisGouveia Ich stimme dir zu, dass sich die Zeit verdoppeln könnte. Verursachen einer Abfrage, die 12.37 msstattdessen dauert 12.3702 ms. Das meine ich mit "im Lärm" . Sie führen die Abfrage erneut aus und es dauert 13.29 ms, oder 11.36 ms. Mit anderen Worten, die Zufälligkeit des Thread-Schedulers wird jede theoretische Mikrooptimierung, die für niemanden in irgendeiner Weise sichtbar ist, drastisch überschwemmen.
Ian Boyd
42

Sofern Sie keine besonderen Leistungsgründe haben, um dies zu vermeiden, würde ich empfehlen, eine separate Tabelle für die Aufzählung zu verwenden. Verwenden Sie die Fremdschlüsselintegrität, es sei denn, die zusätzliche Suche bringt Sie wirklich um.

Anzugstabelle:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Spielertisch

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Wenn Sie Ihre Aufzählung jemals so umgestalten, dass sie Klassen mit Verhalten (z. B. Priorität) sind, modelliert Ihre Datenbank sie bereits korrekt
  2. Ihr DBA ist glücklich, weil Ihr Schema normalisiert ist (Speichern einer einzelnen Ganzzahl pro Spieler anstelle einer gesamten Zeichenfolge, die möglicherweise Tippfehler enthält oder nicht).
  3. Ihre Datenbankwerte ( suit_id) sind unabhängig von Ihrem Aufzählungswert, sodass Sie auch Daten aus anderen Sprachen bearbeiten können.
Tom
quelle
14
Ich bin damit einverstanden, dass es schön ist, es in der Datenbank zu normalisieren und einzuschränken, aber dies führt an zwei Stellen zu Aktualisierungen, um einen neuen Wert (Code und Datenbank) hinzuzufügen, was zu mehr Overhead führen kann. Außerdem sollten Rechtschreibfehler nicht vorhanden sein, wenn alle Aktualisierungen programmgesteuert über den Enum-Namen erfolgen.
Jason
3
Ich stimme dem obigen Kommentar zu. Ein alternativer Durchsetzungsmechanismus auf Datenbankebene wäre das Schreiben eines Einschränkungstriggers, der Einfügungen oder Aktualisierungen ablehnt, die versuchen, einen ungültigen Wert zu verwenden.
Steve Perkins
1
Warum sollte ich die gleichen Informationen an zwei Stellen deklarieren wollen? Sowohl in CODE public enum foo {bar}als auch CREATE TABLE foo (name varchar);das kann leicht aus der Synchronisation geraten.
Ebyrob
Wenn wir die akzeptierte Antwort zum Nennwert nehmen, dh die Aufzählungsnamen werden nur für manuelle Untersuchungen verwendet, ist diese Antwort in der Tat die beste Option. Wenn Sie die Aufzählungsreihenfolge oder Werte oder Namen weiter ändern, haben Sie immer viel mehr Probleme als die Pflege dieser zusätzlichen Tabelle. Insbesondere, wenn Sie es nur zum Debuggen und zur Unterstützung benötigen (und möglicherweise nur vorübergehend erstellen möchten).
afk5min
5

Ich würde argumentieren, dass der einzig sichere Mechanismus hier darin besteht, den String- name()Wert zu verwenden. Wenn Sie mit dem DB Schreiben Sie könnten einen sproc , um den Wert zu setzen und beim Lesen eine Ansicht verwenden. Auf diese Weise gibt es, wenn sich die Aufzählungen ändern, eine Indirektionsebene in der Sproc / Ansicht, um die Daten als Aufzählungswert darstellen zu können, ohne dies der DB "aufzuzwingen".

oxbow_lakes
quelle
1
Ich verwende einen hybriden Ansatz aus Ihrer Lösung und der Lösung von @Ian Boyd mit großem Erfolg. Danke für den Tipp!
technomalogical
5

Wie Sie sagen, ist Ordnungszahl ein bisschen riskant. Betrachten Sie zum Beispiel:

public enum Boolean {
    TRUE, FALSE
}

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

Wenn Sie dies als Ordnungszahlen gespeichert haben, haben Sie möglicherweise Zeilen wie:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

Aber was passiert, wenn Sie Boolean aktualisiert haben?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Dies bedeutet, dass alle Ihre Lügen als "Datei nicht gefunden" falsch interpretiert werden.

Verwenden Sie besser nur eine Zeichenfolgendarstellung

Toolkit
quelle
4

Bei einer großen Datenbank zögere ich, die Größen- und Geschwindigkeitsvorteile der numerischen Darstellung zu verlieren. Am Ende habe ich oft eine Datenbanktabelle, die die Aufzählung darstellt.

Sie können die Datenbankkonsistenz erzwingen, indem Sie einen Fremdschlüssel deklarieren. In einigen Fällen ist es jedoch möglicherweise besser, diesen nicht als Fremdschlüsseleinschränkung zu deklarieren, was bei jeder Transaktion Kosten verursacht. Sie können die Konsistenz sicherstellen, indem Sie zu Zeiten Ihrer Wahl regelmäßig Folgendes überprüfen:

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

Die andere Hälfte dieser Lösung besteht darin, einen Testcode zu schreiben, der überprüft, ob die Java-Aufzählung und die Datenbankaufzählungstabelle denselben Inhalt haben. Das bleibt als Übung für den Leser.

Roger Hayes
quelle
1
Angenommen, die durchschnittliche Länge des Aufzählungsnamens beträgt 7 Zeichen. Ihr enumIDist vier Bytes, also haben Sie zusätzliche drei Bytes pro Zeile, indem Sie Namen verwenden. 3 Bytes x 1 Million Zeilen sind 3 MB.
Ian Boyd
@ IanBoyd: Aber ein enumIdpasst sicherlich in zwei Bytes (längere Aufzählungen sind in Java nicht möglich) und die meisten passen in ein einzelnes Byte (was einige DB unterstützen). Der eingesparte Platz ist vernachlässigbar, aber der schnellere Vergleich und die feste Länge sollten helfen.
Maaartinus
3

Wir speichern nur den Namen der Aufzählung selbst - er ist besser lesbar.

Wir haben mit dem Speichern bestimmter Werte für Aufzählungen herumgespielt, bei denen es nur eine begrenzte Anzahl von Werten gibt, z. B. diese Aufzählung mit einer begrenzten Anzahl von Status, für deren Darstellung wir ein Zeichen verwenden (aussagekräftiger als ein numerischer Wert):

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

und wenn Sie viele Werte haben, müssen Sie eine Map in Ihrer Aufzählung haben, um diese getFromXYZ-Methode klein zu halten.

JeeBee
quelle
Wenn Sie keine switch-Anweisung verwalten möchten und sicherstellen können, dass dbChar eindeutig ist, können Sie Folgendes verwenden: public static EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (UNDEFINED); }
Kuchi
2

Wenn Sie Aufzählungen als Zeichenfolgen in der Datenbank speichern, können Sie Dienstprogrammmethoden erstellen, um jede Aufzählung (zu) serialisieren:

   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
quelle
Es ist schön, dies mit einem Standard-Enum-Wert zu verwenden, auf den beim Deserialisieren zurückgegriffen werden kann. Fangen Sie beispielsweise IllegalArgEx ab und geben Sie Suit.None zurück.
Jason
2

Alle meine Erfahrungen zeigen mir, dass der sicherste Weg, Enums überall beizubehalten, darin besteht, zusätzlichen Codewert oder eine zusätzliche ID zu verwenden (eine Art Weiterentwicklung der @ jeebee-Antwort). Dies könnte ein schönes Beispiel für eine Idee sein:

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

    private final String code;

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

    public String getCode() {
        return code;
    }
}

Jetzt können Sie mit jeder Persistenz fortfahren, die Ihre Enum-Konstanten anhand ihres Codes referenziert. Selbst wenn Sie sich entscheiden, einige konstante Namen zu ändern, können Sie immer den Codewert speichern (z. B. DWARF("dwarf")bis GNOME("dwarf")).

Ok, tauchen Sie mit dieser Konzeption etwas tiefer ein. Hier ist eine Dienstprogrammmethode, mit der Sie einen beliebigen Aufzählungswert finden können. Lassen Sie uns jedoch zunächst unseren Ansatz erweitern.

interface CodeValue {
    String getCode();
}

Und lassen Sie unsere Aufzählung es umsetzen:

enum Race implement CodeValue {...}

Dies ist die Zeit für die magische Suchmethode:

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

Und benutze es wie einen Zauber: Race race = resolveByCode(Race.class, "elf")

Metapher
quelle
2

Ich habe das gleiche Problem, bei dem mein Ziel darin besteht, den Enum-String-Wert anstelle des Ordnungswerts in der Datenbank beizubehalten.

Um dieses Problem zu lösen, habe ich verwendet @Enumerated(EnumType.STRING) lösen und mein Ziel wurde gelöst.

Zum Beispiel haben Sie eine EnumKlasse:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

Definieren Sie in der Entitätsklasse @Enumerated(EnumType.STRING):

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

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

Während Sie versuchen, Ihren Wert auf "Datenbank" zu setzen, wird der Wert "Zeichenfolge" als " APPLE", " ORANGE" oder " LEMON" in der Datenbank beibehalten .

SaravanaC
quelle
0

Sie können einen zusätzlichen Wert in der Aufzählungskonstante verwenden, der sowohl Namensänderungen als auch das erneute Aufrufen der Aufzählungen überlebt:

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

So erhalten Sie die ID aus der Aufzählung:

int id = MyFirstValue.getId();

So erhalten Sie die Aufzählung von einer ID:

MyEnum e = MyEnum.of(id);

Ich schlage vor, Werte ohne Bedeutung zu verwenden, um Verwirrung zu vermeiden, wenn die Aufzählungsnamen geändert werden müssen.

Im obigen Beispiel habe ich eine Variante der "Grundlegenden Zeilennummerierung" verwendet, bei der Leerzeichen verbleiben, damit die Zahlen wahrscheinlich in derselben Reihenfolge wie die Aufzählungen bleiben.

Diese Version ist schneller als die Verwendung einer sekundären Tabelle, macht das System jedoch stärker von Code- und Quellcode-Kenntnissen abhängig.

Um dies zu beheben, können Sie auch eine Tabelle mit den Enum-IDs in der Datenbank einrichten. Oder gehen Sie in die andere Richtung und wählen Sie IDs für die Aufzählungen aus einer Tabelle aus, während Sie Zeilen hinzufügen.

Nebenbemerkung : Stellen Sie immer sicher , dass Sie nichts entwerfen, das in einer Datenbanktabelle gespeichert und als reguläres Objekt verwaltet werden soll. Wenn Sie sich vorstellen können, dass Sie der Enumeration zu diesem Zeitpunkt beim Einrichten neue Konstanten hinzufügen müssen, ist dies ein Hinweis darauf, dass Sie möglicherweise besser dran sind, stattdessen ein reguläres Objekt und eine Tabelle zu erstellen.

Erk
quelle