Façons d'enregistrer les énumérations dans la base de données

123

Quelle est la meilleure façon d'enregistrer les énumérations dans une base de données?

Je sais que Java fournit name()et valueOf()méthodes pour convertir les valeurs enum dans une chaîne et le dos. Mais existe-t-il d'autres options (flexibles) pour stocker ces valeurs?

Existe-t-il un moyen intelligent de transformer les énumérations en nombres uniques ( ordinal()n'est pas sûr à utiliser)?

Mettre à jour:

Merci pour toutes les réponses géniales et rapides! C'était comme je le soupçonnais.

Cependant une note à «boîte à outils»; C'est une manière. Le problème est que je devrais ajouter les mêmes méthodes à chaque type Enum que je crée. C'est beaucoup de code dupliqué et, pour le moment, Java ne prend en charge aucune solution pour cela (une énumération Java ne peut pas étendre d'autres classes).

user20298
la source
2
Pourquoi l'utilisation d'ordinal () n'est-elle pas sûre?
Michael Myers
Quel genre de base de données? MySQL a un type enum, mais je ne pense pas que ce soit du SQL ANSI standard.
Sherm Pendley
6
Parce que tous les ajouts énumératifs doivent alors être mis à la fin. Facile pour un développeur sans méfiance de gâcher cela et de causer des ravages
oxbow_lakes
1
Je vois. Je suppose que c'est une bonne chose que je ne traite pas beaucoup des bases de données, car je n'y aurais probablement pas pensé avant qu'il ne soit trop tard.
Michael Myers

Réponses:

165

Nous ne stockons plus jamais les énumérations sous forme de valeurs ordinales numériques; cela rend le débogage et le support beaucoup trop difficiles. Nous stockons la valeur d'énumération réelle convertie en chaîne:

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

Suit theSuit = Suit.Heart;

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

puis relisez avec:

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

Le problème était dans le passé de regarder Enterprise Manager et d'essayer de déchiffrer:

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

versets

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

ce dernier est beaucoup plus facile. Le premier nécessitait d'obtenir le code source et de trouver les valeurs numériques affectées aux membres de l'énumération.

Oui, cela prend plus de place, mais les noms des membres de l'énumération sont courts et les disques durs sont bon marché, et cela vaut bien plus la peine d'aider lorsque vous rencontrez un problème.

De plus, si vous utilisez des valeurs numériques, vous y êtes lié. Vous ne pouvez pas insérer ou réorganiser correctement les membres sans avoir à forcer les anciennes valeurs numériques. Par exemple, en modifiant l'énumération Suit en:

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

devrait devenir:

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

afin de conserver les valeurs numériques héritées stockées dans la base de données.

Comment les trier dans la base de données

La question se pose: disons que je voulais ordonner les valeurs. Certaines personnes peuvent vouloir les trier par la valeur ordinale de l'énumération. Bien sûr, ordonner les cartes en fonction de la valeur numérique de l'énumération n'a pas de sens:

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

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

Ce n'est pas l'ordre que nous voulons - nous les voulons dans l'ordre d'énumération:

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

Le même travail qui est requis si vous enregistrez des valeurs entières est requis si vous enregistrez des chaînes:

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

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

Mais ce n'est pas l'ordre que nous voulons - nous les voulons dans l'ordre d'énumération:

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

Mon avis est que ce type de classement appartient à l'interface utilisateur. Si vous triez des éléments en fonction de leur valeur d'énumération: vous faites quelque chose de mal.

Mais si vous vouliez vraiment faire cela, je créerais une Suitstable de dimension:

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

De cette façon, lorsque vous souhaitez modifier vos cartes pour utiliser Kissing Kings New Deck Order, vous pouvez le modifier à des fins d'affichage sans perdre toutes vos données:

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

Nous séparons maintenant un détail de programmation interne (nom d'énumération, valeur d'énumération) avec un paramètre d'affichage destiné aux utilisateurs:

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
Ian Boyd
la source
23
toString est souvent remplacé pour fournir une valeur d'affichage. name () est un meilleur choix car c'est par définition la contrepartie de valueOf ()
ddimitrov
9
Je ne suis pas du tout d'accord avec cela, si la persistance d'énumération est requise, les noms ne devraient pas persister. en ce qui concerne sa lecture, il est encore plus simple avec valeur au lieu de nom, il suffit de le convertir en SomeEnum enum1 = (SomeEnum) 2;
mamu
3
mamu: Que se passe-t-il lorsque les équivalents numériques changent?
Ian Boyd
2
Je découragerais quiconque utilisant cette approche. S'attacher à la représentation sous forme de chaîne limite la flexibilité du code et la refactorisation. Vous devriez mieux utiliser des identifiants uniques. Le stockage des chaînes gaspille également de l'espace de stockage.
Tautvydas
2
@LuisGouveia Je suis d'accord avec vous que le temps pourrait doubler. Causant une requête qui prend 12.37 msà la place 12.3702 ms. C'est ce que j'entends par «dans le bruit» . Vous exécutez à nouveau la requête et cela prend 13.29 ms, ou 11.36 ms. En d'autres termes, le caractère aléatoire du planificateur de threads submergera considérablement toute micro-optimisation que vous avez théoriquement et qui n'est en aucun cas visible par quiconque.
Ian Boyd
42

À moins que vous n'ayez des raisons de performances spécifiques pour l'éviter, je vous recommande d'utiliser une table distincte pour l'énumération. Utilisez l'intégrité de la clé étrangère à moins que la recherche supplémentaire ne vous tue vraiment.

Table des costumes:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Table des joueurs

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Si jamais vous refactorisez votre énumération en classes avec un comportement (tel que la priorité), votre base de données la modélise déjà correctement
  2. Votre DBA est content car votre schéma est normalisé (stockage d'un seul entier par joueur, au lieu d'une chaîne entière, qui peut ou non avoir des fautes de frappe).
  3. Les valeurs de votre base de données ( suit_id) sont indépendantes de votre valeur d'énumération, ce qui vous aide également à travailler sur les données d'autres langues.
À M
la source
14
Bien que je convienne qu'il est agréable de l'avoir normalisé et contraint dans la base de données, cela entraîne des mises à jour à deux endroits pour ajouter une nouvelle valeur (code et base de données), ce qui pourrait entraîner plus de surcharge. En outre, les fautes d'orthographe doivent être inexistantes si toutes les mises à jour sont effectuées par programme à partir du nom Enum.
Jason
3
Je suis d'accord avec le commentaire ci-dessus. Un autre mécanisme d'application au niveau de la base de données consisterait à écrire un déclencheur de contrainte, qui rejetterait les insertions ou les mises à jour qui tentent d'utiliser une valeur non valide.
Steve Perkins
1
Pourquoi voudrais-je déclarer les mêmes informations à deux endroits? À la fois dans CODE public enum foo {bar}et CREATE TABLE foo (name varchar);qui peuvent facilement se désynchroniser.
ebyrob
Si nous prenons la réponse acceptée au pied de la lettre, c'est-à-dire que les noms d'énumérations ne sont utilisés que pour des enquêtes manuelles, alors cette réponse est en effet la meilleure option. De plus, si vous continuez à modifier l'ordre d'énumération ou les valeurs ou les noms, vous aurez toujours beaucoup plus de problèmes que de maintenir cette table supplémentaire. Surtout lorsque vous n'en avez besoin que (et que vous pouvez choisir de créer uniquement temporairement) pour le débogage et le support.
afk5min
5

Je dirais que le seul mécanisme sûr ici est d'utiliser la name()valeur String . Lors de l'écriture dans la base de données, vous pouvez utiliser un sproc pour insérer la valeur et lors de la lecture, utiliser une vue. De cette manière, si les énumérations changent, il existe un niveau d'indirection dans le sproc / vue pour pouvoir présenter les données en tant que valeur d'énumération sans «imposer» cela au DB.

oxbow_lakes
la source
1
J'utilise une approche hybride de votre solution et de la solution @Ian Boyd avec beaucoup de succès. Merci pour le conseil!
technomalogical
5

Comme vous le dites, ordinal est un peu risqué. Considérez par exemple:

public enum Boolean {
    TRUE, FALSE
}

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

Si vous avez stocké ceci en tant qu'ordinaux, vous pourriez avoir des lignes comme:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

Mais que se passe-t-il si vous avez mis à jour Boolean?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Cela signifie que tous vos mensonges seront mal interprétés comme `` fichier introuvable ''

Mieux vaut simplement utiliser une représentation sous forme de chaîne

boîte à outils
la source
4

Pour une grande base de données, je suis réticent à perdre les avantages de taille et de vitesse de la représentation numérique. Je me retrouve souvent avec une table de base de données représentant le Enum.

Vous pouvez appliquer la cohérence de la base de données en déclarant une clé étrangère - même si dans certains cas, il peut être préférable de ne pas déclarer cela comme une contrainte de clé étrangère, ce qui impose un coût à chaque transaction. Vous pouvez assurer la cohérence en effectuant périodiquement une vérification, aux moments de votre choix, avec:

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

L'autre moitié de cette solution consiste à écrire du code de test qui vérifie que l'énumération Java et la table d'énumération de la base de données ont le même contenu. Cela reste un exercice pour le lecteur.

Roger Hayes
la source
1
Supposons que la longueur moyenne du nom d'énumération soit de 7 caractères. Votre enumIDest de quatre octets, vous avez donc trois octets supplémentaires par ligne en utilisant des noms. 3 octets x 1 million de lignes équivaut à 3 Mo.
Ian Boyd
@IanBoyd: Mais an enumIdtient sûrement dans deux octets (des énumérations plus longues ne sont pas possibles en Java) et la plupart d'entre elles tiennent dans un seul octet (que certaines bases de données prennent en charge). L'espace économisé est négligeable, mais la comparaison plus rapide et la longueur fixe devraient aider.
maaartinus
3

Nous stockons simplement le nom enum lui-même - il est plus lisible.

Nous avons joué avec le stockage de valeurs spécifiques pour les énumérations où il y a un ensemble limité de valeurs, par exemple, cette énumération qui a un ensemble limité de statuts que nous utilisons un caractère pour représenter (plus significatif qu'une valeur numérique):

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

et quand vous avez beaucoup de valeurs, vous devez avoir une carte dans votre énumération pour garder cette méthode getFromXYZ petite.

JeeBee
la source
Si vous ne voulez pas maintenir une instruction switch et pouvez vous assurer que dbChar est unique, vous pouvez utiliser quelque chose comme: public static EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (UNDEFINED); }
Kuchi
2

Si vous enregistrez les énumérations sous forme de chaînes dans la base de données, vous pouvez créer des méthodes utilitaires pour (dé) sérialiser toute énumération:

   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
la source
C'est bien de l'utiliser avec une valeur d'énumération par défaut sur laquelle se rabattre dans la désérialisation. Par exemple, interceptez IllegalArgEx et renvoyez Suit.None.
Jason
2

Toute mon expérience me dit que le moyen le plus sûr de conserver des énumérations n'importe où est d'utiliser une valeur de code ou un identifiant supplémentaire (une sorte d'évolution de la réponse @jeebee). Cela pourrait être un bel exemple d'idée:

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

    private final String code;

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

    public String getCode() {
        return code;
    }
}

Vous pouvez maintenant utiliser n'importe quelle persistance référençant vos constantes d'énumération par son code. Même si vous décidez de changer certains des noms de constantes, vous pouvez toujours enregistrer la valeur du code (par exemple DWARF("dwarf")dans GNOME("dwarf"))

Ok, plongez un peu plus profondément avec cette conception. Voici une méthode utilitaire qui vous aide à trouver n'importe quelle valeur d'énumération, mais étendons d'abord notre approche.

interface CodeValue {
    String getCode();
}

Et laissez notre enum l'implémenter:

enum Race implement CodeValue {...}

C'est le moment de la méthode de recherche magique:

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

Et utilisez-le comme un charme: Race race = resolveByCode(Race.class, "elf")

Métaphore
la source
2

J'ai rencontré le même problème où mon objectif est de conserver la valeur Enum String dans la base de données au lieu de la valeur ordinale.

Pour surmonter ce problème, j'ai utilisé @Enumerated(EnumType.STRING) et mon objectif a été résolu.

Par exemple, vous avez une Enumclasse:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

Dans la classe d'entité, définissez @Enumerated(EnumType.STRING):

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

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

Pendant que vous essayez de définir votre valeur sur Database, la valeur String sera conservée dans Database en tant que " APPLE", " ORANGE" ou " LEMON".

SaravanaC
la source
0

Vous pouvez utiliser une valeur supplémentaire dans la constante enum qui peut survivre à la fois aux changements de nom et au recours aux énumérations:

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

Pour obtenir l'identifiant de l'énumération:

int id = MyFirstValue.getId();

Pour obtenir l'énumération à partir d'un identifiant:

MyEnum e = MyEnum.of(id);

Je suggère d'utiliser des valeurs sans signification pour éviter toute confusion si les noms d'énumérations doivent être modifiés.

Dans l'exemple ci-dessus, j'ai utilisé une variante de "Numérotation de ligne de base" en laissant des espaces afin que les nombres restent probablement dans le même ordre que les énumérations.

Cette version est plus rapide que l'utilisation d'une table secondaire, mais elle rend le système plus dépendant du code et de la connaissance du code source.

Pour remédier à cela, vous pouvez également configurer une table avec les identifiants d'énumération dans la base de données. Ou allez dans l'autre sens et choisissez des identifiants pour les énumérations dans une table lorsque vous y ajoutez des lignes.

Note de bas de page : vérifiez toujours que vous ne concevez pas quelque chose qui devrait être stocké dans une table de base de données et conservé comme un objet normal. Si vous pouvez imaginer que vous devez ajouter de nouvelles constantes à l'énumération à ce stade, lorsque vous la configurez, c'est une indication que vous feriez peut-être mieux de créer un objet normal et une table à la place.

Erk
la source