Modi per salvare le enumerazioni nel database


123

Qual è il modo migliore per salvare le enumerazioni in un database?

So che Java fornisce name()e valueOf()metodi per convertire i valori enum in una stringa e viceversa. Ma ci sono altre opzioni (flessibili) per memorizzare questi valori?

Esiste un modo intelligente per trasformare le enumerazioni in numeri univoci ( ordinal()non è sicuro da usare)?

Aggiornare:

Grazie per tutte le fantastiche e veloci risposte! Era come sospettavo.

Tuttavia una nota a "toolkit"; Questo è un modo. Il problema è che dovrei aggiungere gli stessi metodi a ogni tipo di Enum che creo. Questo è un sacco di codice duplicato e, al momento, Java non supporta alcuna soluzione per questo (un enum Java non può estendere altre classi).


2
Perché ordinal () non è sicuro da usare?
Michael Myers

Che tipo di database? MySQL ha un tipo enum, ma non credo che sia SQL ANSI standard.
Sherm Pendley,

6
Perché ogni aggiunta enumerativa deve poi essere posta alla fine. Facile per uno sviluppatore ignaro rovinare tutto e causare il caos
oxbow_lakes

1
Vedo. Immagino sia un bene che non mi occupi molto di database, perché probabilmente non ci avrei pensato fino a quando non fosse stato troppo tardi.
Michael Myers

Risposte:


165

Noi non memorizzare le enumerazioni come valori numerici ordinali più; rende il debug e il supporto troppo difficili. Memorizziamo il valore di enumerazione effettivo convertito in stringa:

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

Suit theSuit = Suit.Heart;

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

e poi rileggi con:

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

Il problema in passato era fissare Enterprise Manager e cercare di decifrare:

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

versi

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

quest'ultimo è molto più semplice. Il primo richiedeva di ottenere il codice sorgente e di trovare i valori numerici assegnati ai membri dell'enumerazione.

Sì, richiede più spazio, ma i nomi dei membri dell'enumerazione sono brevi e i dischi rigidi sono economici, ed è molto più utile che ti aiuti quando hai un problema.

Inoltre, se usi valori numerici, sei legato ad essi. Non è possibile inserire o riorganizzare correttamente i membri senza dover forzare i vecchi valori numerici. Ad esempio, modificando l'enumerazione Suit in:

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

dovrebbe diventare:

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

per mantenere i valori numerici precedenti memorizzati nel database.

Come ordinarli nel database

La domanda sorge spontanea: diciamo che volevo ordinare i valori. Alcune persone potrebbero volerli ordinare in base al valore ordinale dell'enumerazione. Naturalmente, ordinare le carte in base al valore numerico dell'enumerazione non ha senso:

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

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

Non è l'ordine che vogliamo, li vogliamo in ordine di enumerazione:

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

Lo stesso lavoro richiesto se si salvano valori interi è richiesto se si salvano le stringhe:

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

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

Ma non è l'ordine che vogliamo, li vogliamo in ordine di enumerazione:

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

La mia opinione è che questo tipo di classifica appartenga all'interfaccia utente. Se stai ordinando gli elementi in base al loro valore di enumerazione: stai facendo qualcosa di sbagliato.

Ma se volessi farlo davvero, creerei una Suitstabella delle dimensioni:

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

In questo modo, quando vuoi cambiare le tue carte per usare Kissing Kings New Deck Order , puoi cambiarlo a scopo di visualizzazione senza buttare via tutti i tuoi dati:

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

Ora stiamo separando un dettaglio di programmazione interna (nome dell'enumerazione, valore dell'enumerazione) con un'impostazione di visualizzazione pensata per gli utenti:

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

23
toString viene spesso sovrascritto per fornire il valore visualizzato. name () è una scelta migliore in quanto è per definizione la controparte di valueOf ()
ddimitrov

9
Sono fortemente in disaccordo con questo, se è richiesta la persistenza enum, non dovrebbe persistere i nomi. per quanto riguarda la lettura, è ancora più semplice con value invece di name può semplicemente digitarlo come SomeEnum enum1 = (SomeEnum) 2;
mamu

3
mamu: Cosa succede quando cambiano gli equivalenti numerici?
Ian Boyd

2
Scoraggerei chiunque utilizzi questo approccio. Legarsi alla rappresentazione di stringa limita la flessibilità del codice e il refactoring. È meglio utilizzare ID univoci. Anche la memorizzazione delle stringhe spreca spazio di archiviazione.
Tautvydas

2
@LuisGouveia Sono d'accordo con te che il tempo potrebbe raddoppiare. Causando una query che richiede 12.37 msinvece di prendere 12.3702 ms. Questo è ciò che intendo con "nel rumore" . Esegui di nuovo la query e richiede 13.29 ms, o 11.36 ms. In altre parole, la casualità del thread scheduler inonderà drasticamente qualsiasi micro ottimizzazione che teoricamente hai che non è in alcun modo visibile a nessuno in alcun modo.
Ian Boyd

42

A meno che tu non abbia specifici motivi di prestazioni per evitarlo, ti consiglio di usare una tabella separata per l'enumerazione. Usa l'integrità della chiave esterna a meno che la ricerca extra non ti uccida davvero.

Tabella abiti:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Tavolo dei giocatori

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Se mai rifattori la tua enumerazione per essere classi con comportamento (come priorità), il tuo database la modella già correttamente
  2. Il tuo DBA è felice perché il tuo schema è normalizzato (memorizzando un singolo numero intero per giocatore, invece di un'intera stringa, che può o meno avere errori di battitura).
  3. I valori del database ( suit_id) sono indipendenti dal valore di enumerazione, il che aiuta a lavorare anche sui dati di altre lingue.

14
Anche se sono d'accordo che è bello averlo normalizzato e vincolato nel DB, questo fa sì che gli aggiornamenti in due punti aggiungano un nuovo valore (codice e db), che potrebbe causare un sovraccarico. Inoltre, gli errori di ortografia dovrebbero essere inesistenti se tutti gli aggiornamenti vengono eseguiti in modo programmatico dal nome Enum.
Jason

3
Sono d'accordo con il commento sopra. Un meccanismo di imposizione alternativo a livello di database sarebbe scrivere un trigger di vincolo, che rifiuterebbe inserimenti o aggiornamenti che tentano di utilizzare un valore non valido.
Steve Perkins

1
Perché dovrei dichiarare le stesse informazioni in due posti? Sia in CODE public enum foo {bar}che in CREATE TABLE foo (name varchar);quello possono facilmente perdere la sincronizzazione.
ebyrob

Se prendiamo la risposta accettata al valore nominale, cioè che i nomi enum sono usati solo per le indagini manuali, allora questa risposta è davvero l'opzione migliore. Inoltre, se continui a modificare l'ordine di enumerazione, i valori oi nomi, avrai sempre molti più problemi che mantenere questa tabella aggiuntiva. Soprattutto quando ne hai solo bisogno (e puoi scegliere di creare solo temporaneamente) per il debug e il supporto.
afk5min

5

Direi che l'unico meccanismo sicuro qui è usare il name()valore String . Quando si scrive nel DB, è possibile utilizzare uno sproc per inserire il valore e durante la lettura, utilizzare una vista. In questo modo, se le enumerazioni cambiano, c'è un livello di riferimento indiretto nello sproc / view per poter presentare i dati come valore enum senza "imporlo" al DB.


1
Sto usando un approccio ibrido della tua soluzione e della soluzione di @Ian Boyd con grande successo. Grazie per il consiglio!
tecnomalogico

5

Come dici tu, ordinale è un po 'rischioso. Considera ad esempio:

public enum Boolean {
    TRUE, FALSE
}

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

Se lo hai memorizzato come ordinali, potresti avere righe come:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

Ma cosa succede se hai aggiornato Boolean?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Ciò significa che tutte le tue bugie verranno interpretate erroneamente come "file non trovato"

Meglio usare solo una rappresentazione di stringa


4

Per un database di grandi dimensioni, sono riluttante a perdere i vantaggi in termini di dimensioni e velocità della rappresentazione numerica. Spesso mi ritrovo con una tabella di database che rappresenta l'Enum.

È possibile applicare la coerenza del database dichiarando una chiave esterna, sebbene in alcuni casi potrebbe essere meglio non dichiararla come un vincolo di chiave esterna, che impone un costo su ogni transazione. Puoi garantire la coerenza effettuando periodicamente un controllo, in momenti di tua scelta, con:

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

L'altra metà di questa soluzione consiste nello scrivere un codice di test che controlli che l'enumerazione Java e la tabella enum del database abbiano lo stesso contenuto. Questo è lasciato come esercizio per il lettore.


1
Supponiamo che la lunghezza media del nome dell'enumerazione sia di 7 caratteri. Il tuo enumIDè di quattro byte, quindi hai tre byte in più per riga usando i nomi. 3 byte x 1 milione di righe è 3MB.
Ian Boyd

@IanBoyd: Ma enumIdsicuramente si adatta a due byte (enumerazioni più lunghe non sono possibili in Java) e la maggior parte di esse si adatta a un singolo byte (che alcuni DB supportano). Lo spazio risparmiato è trascurabile, ma il confronto più veloce e la lunghezza fissa dovrebbero aiutare.
maaartinus

3

Memorizziamo solo il nome dell'enumerazione stesso: è più leggibile.

Abbiamo scherzato con la memorizzazione di valori specifici per enumerazioni in cui esiste un insieme limitato di valori, ad esempio, questo enum che ha un insieme limitato di stati che usiamo per rappresentare un carattere (più significativo di un valore numerico):

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

e quando hai molti valori devi avere una mappa all'interno della tua enum per mantenere piccolo il metodo getFromXYZ.


Se non vuoi mantenere un'istruzione switch e puoi assicurarti che dbChar sia univoco puoi usare qualcosa come: public static EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (UNDEFINED); }
Kuchi

2

Se si salvano enum come stringhe nel database, è possibile creare metodi di utilità per (de) serializzare qualsiasi enum:

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

Bello usarlo con un valore enum predefinito su cui ripiegare nella deserializzazione. Ad esempio, prendi IllegalArgEx e restituisci Suit.None.
Jason

2

Tutta la mia esperienza mi dice che il modo più sicuro di persistere enumerazioni ovunque è usare un valore di codice aggiuntivo o id (una sorta di evoluzione della risposta @jeebee). Questo potrebbe essere un bell'esempio di un'idea:

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

    private final String code;

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

    public String getCode() {
        return code;
    }
}

Ora puoi utilizzare qualsiasi persistenza facendo riferimento alle costanti enum tramite il suo codice. Anche se deciderai di modificare alcuni dei nomi delle costanti, puoi sempre salvare il valore del codice (ad esempio DWARF("dwarf")in GNOME("dwarf"))

Ok, approfondisci ancora di più questa concezione. Ecco alcuni metodi di utilità, che ti aiutano a trovare qualsiasi valore enum, ma prima estendiamo il nostro approccio.

interface CodeValue {
    String getCode();
}

E lascia che il nostro enum lo implementi:

enum Race implement CodeValue {...}

Questo è il momento del metodo di ricerca magico:

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

E usalo come un incantesimo: Race race = resolveByCode(Race.class, "elf")


2

Ho affrontato lo stesso problema in cui il mio obiettivo è quello di mantenere il valore Enum String nel database invece del valore ordinale.

Per superare questo problema, l'ho usato @Enumerated(EnumType.STRING)e il mio obiettivo è stato risolto.

Ad esempio, hai una Enumclasse:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

Nella classe di entità, definire @Enumerated(EnumType.STRING):

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

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

Mentre si tenta di impostare il valore su Database, il valore String verrà mantenuto nel database come " APPLE", " ORANGE" o " LEMON".



0

È possibile utilizzare un valore aggiuntivo nella costante enum che può sopravvivere sia alle modifiche del nome che al ricorso alle enumerazioni:

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

Per ottenere l'id dall'enumerazione:

int id = MyFirstValue.getId();

Per ottenere l'enumerazione da un id:

MyEnum e = MyEnum.of(id);

Suggerisco di usare valori senza significato per evitare confusione se i nomi enum devono essere modificati.

Nell'esempio precedente, ho utilizzato una variante della "Numerazione riga di base" lasciando spazi in modo che i numeri rimarranno probabilmente nello stesso ordine delle enumerazioni.

Questa versione è più veloce rispetto all'utilizzo di una tabella secondaria, ma rende il sistema più dipendente dalla conoscenza del codice e del codice sorgente.

Per rimediare a ciò, puoi anche impostare una tabella con gli enum id nel database. Oppure vai dall'altra parte e scegli gli ID per le enumerazioni da una tabella mentre aggiungi righe ad essa.

Nota a margine : verifica sempre di non progettare qualcosa che dovrebbe essere archiviato in una tabella di database e mantenuto come un oggetto normale. Se puoi immaginare di dover aggiungere nuove costanti all'enum a questo punto, quando lo imposti, è un'indicazione che potresti creare un oggetto normale e una tabella invece.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.