Formas de guardar enumeraciones en la base de datos

123

¿Cuál es la mejor manera de guardar enumeraciones en una base de datos?

Sé Java proporciona name()y valueOf()métodos para convertir los valores de enumeración en una cadena y la espalda. Pero, ¿existen otras opciones (flexibles) para almacenar estos valores?

¿Existe una forma inteligente de convertir enumeraciones en números únicos ( ordinal()no es seguro de usar)?

Actualizar:

¡Gracias por todas las respuestas increíbles y rápidas! Fue como sospechaba.

Sin embargo, una nota al 'kit de herramientas'; Esa es una forma. El problema es que tendría que agregar los mismos métodos a cada tipo de Enum que creo. Eso es mucho código duplicado y, por el momento, Java no admite ninguna solución para esto (una enumeración de Java no puede extender otras clases).

user20298
fuente
2
¿Por qué ordinal () no es seguro de usar?
Michael Myers
¿Qué tipo de base de datos? MySQL tiene un tipo de enumeración, pero no creo que sea ANSI SQL estándar.
Sherm Pendley
6
Porque cualquier adición enumerativa debe colocarse al final. Fácil para un desarrollador desprevenido estropear esto y causar estragos
oxbow_lakes
1
Veo. Supongo que es bueno que no me ocupe mucho de las bases de datos, porque probablemente no habría pensado en eso hasta que fue demasiado tarde.
Michael Myers

Respuestas:

165

Ya no almacenamos enumeraciones como valores ordinales numéricos; hace que la depuración y el soporte sean demasiado difíciles. Almacenamos el valor de enumeración real convertido a cadena:

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

Suit theSuit = Suit.Heart;

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

y luego vuelva a leer con:

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

El problema estaba en el pasado mirando a Enterprise Manager y tratando de descifrar:

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

versos

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

este último es mucho más fácil. El primero requería obtener el código fuente y encontrar los valores numéricos que se asignaron a los miembros de la enumeración.

Sí, ocupa más espacio, pero los nombres de los miembros de la enumeración son cortos y los discos duros son baratos, y vale mucho más la pena ayudar cuando tiene un problema.

Además, si usa valores numéricos, está atado a ellos. No puede insertar o reorganizar los miembros sin tener que forzar los valores numéricos antiguos. Por ejemplo, cambiando la enumeración Traje a:

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

tendría que convertirse en:

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

para mantener los valores numéricos heredados almacenados en la base de datos.

Cómo ordenarlos en la base de datos

Surge la pregunta: digamos que quería ordenar los valores. Algunas personas pueden querer ordenarlos por el valor ordinal de la enumeración. Por supuesto, ordenar las tarjetas por el valor numérico de la enumeración no tiene sentido:

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

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

Ese no es el orden que queremos, los queremos en orden de enumeración:

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

El mismo trabajo que se requiere si guarda valores enteros es necesario si guarda cadenas:

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

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

Pero ese no es el orden que queremos, los queremos en orden de enumeración:

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

Mi opinión es que este tipo de clasificación pertenece a la interfaz de usuario. Si está ordenando elementos según su valor de enumeración: está haciendo algo mal.

Pero si realmente quisiera hacer eso, crearía una Suitstabla de dimensiones:

| 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 esta manera, cuando quieras cambiar tus cartas para usar Kissing Kings New Deck Order , puedes cambiarlo con fines de visualización sin tirar todos tus datos:

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

Ahora estamos separando un detalle de programación interno (nombre de enumeración, valor de enumeración) con una configuración de visualización destinada a los usuarios:

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
Ian Boyd
fuente
23
toString a menudo se anula para proporcionar valor de visualización. name () es una mejor opción ya que es, por definición, la contraparte de valueOf ()
ddimitrov
9
Estoy totalmente en desacuerdo con esto, si se requiere persistencia enum, entonces no debería persistir los nombres. en cuanto a leerlo, es incluso más simple con valor en lugar de nombre, simplemente puede encasillarlo como SomeEnum enum1 = (SomeEnum) 2;
mamu
3
mamu: ¿Qué sucede cuando cambian los equivalentes numéricos?
Ian Boyd
2
Desanimaría a cualquiera que use este enfoque. Vincularse a la representación de cadenas limita la flexibilidad y la refactorización del código. Es mejor que utilice identificadores únicos. También almacenar cuerdas desperdicia espacio de almacenamiento.
Tautvydas
2
@LuisGouveia Estoy de acuerdo contigo en que el tiempo podría duplicarse. Provocando una consulta que lleva 12.37 msa tomar en su lugar 12.3702 ms. Eso es lo que quiero decir con "en el ruido" . Ejecuta la consulta nuevamente y toma 13.29 ms, o 11.36 ms. En otras palabras, la aleatoriedad del programador de subprocesos inundará drásticamente cualquier microoptimización que teóricamente tengas y que no sea visible para nadie de ninguna manera.
Ian Boyd
42

A menos que tenga razones específicas de rendimiento para evitarlo, le recomendaría usar una tabla separada para la enumeración. Use la integridad de la clave externa a menos que la búsqueda adicional realmente lo mate.

Mesa de trajes:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Mesa de jugadores

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Si alguna vez refactoriza su enumeración para que sean clases con comportamiento (como prioridad), su base de datos ya la modela correctamente
  2. Su DBA está contento porque su esquema está normalizado (almacenando un solo entero por jugador, en lugar de una cadena completa, que puede o no tener errores tipográficos).
  3. Los valores de su base de datos ( suit_id) son independientes de su valor de enumeración, lo que le ayuda a trabajar con los datos de otros idiomas también.
Tom
fuente
14
Si bien estoy de acuerdo en que es bueno tenerlo normalizado y restringido en la base de datos, esto causa actualizaciones en dos lugares para agregar un nuevo valor (código y base de datos), lo que podría causar más gastos generales. Además, los errores de ortografía no deberían existir si todas las actualizaciones se realizan mediante programación desde el nombre Enum.
Jason
3
Estoy de acuerdo con el comentario anterior. Un mecanismo de aplicación alternativo a nivel de la base de datos sería escribir un activador de restricción, que rechazaría inserciones o actualizaciones que intentan usar un valor no válido.
Steve Perkins
1
¿Por qué querría declarar la misma información en dos lugares? Tanto en CODE public enum foo {bar}como CREATE TABLE foo (name varchar);que pueden desincronizarse fácilmente.
ebyrob
Si tomamos la respuesta aceptada al pie de la letra, es decir, que los nombres de enumeración solo se usan para investigaciones manuales, entonces esta respuesta es de hecho la mejor opción. Además, si continúa cambiando el orden de enumeración, los valores o los nombres, siempre tendrá muchos más problemas que mantener esta tabla adicional. Especialmente cuando solo lo necesita (y puede optar por crear solo temporalmente) para la depuración y el soporte.
afk5min
5

Yo diría que el único mecanismo seguro aquí es usar el name()valor String . Al escribir en la base de datos, puede usar un sproc para insertar el valor y, al leer, usar una vista. De esta manera, si las enumeraciones cambian, hay un nivel de indirección en la vista sproc / para poder presentar los datos como el valor de enumeración sin "imponer" esto en la base de datos.

oxbow_lakes
fuente
1
Estoy usando un enfoque híbrido de su solución y la solución de @Ian Boyd con gran éxito. ¡Gracias por el consejo!
technomalogical
5

Como dices, ordinal es un poco arriesgado. Considere, por ejemplo:

public enum Boolean {
    TRUE, FALSE
}

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

Si almacenó esto como ordinales, es posible que tenga filas como:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

Pero, ¿qué pasa si actualizas Boolean?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Esto significa que todas sus mentiras serán malinterpretadas como 'archivo no encontrado'

Es mejor usar una representación de cadena

caja de herramientas
fuente
4

Para una base de datos grande, soy reacio a perder las ventajas de tamaño y velocidad de la representación numérica. A menudo termino con una tabla de base de datos que representa el Enum.

Puede imponer la coherencia de la base de datos declarando una clave externa, aunque en algunos casos puede ser mejor no declararla como una restricción de clave externa, lo que impone un costo en cada transacción. Puede garantizar la coherencia haciendo una verificación periódica, en el momento que elija, con:

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

La otra mitad de esta solución es escribir un código de prueba que verifique que la enumeración de Java y la tabla de enumeración de la base de datos tengan el mismo contenido. Eso queda como ejercicio para el lector.

Roger Hayes
fuente
1
Supongamos que la longitud promedio del nombre de la enumeración es de 7 caracteres. Su enumIDes de cuatro bytes, por lo que tiene tres bytes adicionales por fila al usar nombres. 3 bytes x 1 millón de filas son 3 MB.
Ian Boyd
@IanBoyd: Pero enumIdseguramente cabe en dos bytes (las enumeraciones más largas no son posibles en Java) y la mayoría de ellas caben en un solo byte (que algunas bases de datos admiten). El espacio ahorrado es insignificante, pero la comparación más rápida y la longitud fija deberían ayudar.
maaartinus
3

Solo almacenamos el nombre de la enumeración en sí, es más legible.

Nos equivocamos al almacenar valores específicos para enumeraciones donde hay un conjunto limitado de valores, por ejemplo, esta enumeración que tiene un conjunto limitado de estados que usamos un carácter para representar (más significativo que un valor numérico):

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

y cuando tiene muchos valores, necesita tener un mapa dentro de su enumeración para mantener pequeño ese método getFromXYZ.

JeeBee
fuente
Si no desea mantener una declaración de cambio y puede asegurarse de que dbChar sea único, puede usar algo como: public static EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (INDEFINIDO); }
Kuchi
2

Si guarda enumeraciones como cadenas en la base de datos, puede crear métodos de utilidad para (des) serializar cualquier enumeración:

   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
fuente
Es bueno usar esto con un valor de enumeración predeterminado al que recurrir en deserialización. Por ejemplo, coge el IllegalArgEx y devuelve Suit.None.
Jason
2

Toda mi experiencia me dice que la forma más segura de persistir enums en cualquier lugar es usar un valor de código adicional o id (algún tipo de evolución de la respuesta de @jeebee). Este podría ser un buen ejemplo de 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;
    }
}

Ahora puede usar cualquier persistencia que haga referencia a sus constantes de enumeración por su código. Incluso si decide cambiar algunos de los nombres de las constantes, siempre puede guardar el valor del código (por ejemplo, DWARF("dwarf")a GNOME("dwarf"))

Ok, profundiza un poco más con esta concepción. Aquí hay un método de utilidad que lo ayuda a encontrar cualquier valor de enumeración, pero primero extendamos nuestro enfoque.

interface CodeValue {
    String getCode();
}

Y deje que nuestra enumeración lo implemente:

enum Race implement CodeValue {...}

Este es el momento del método de búsqueda mágica:

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

Y úsalo como un amuleto: Race race = resolveByCode(Race.class, "elf")

Metáfora
fuente
2

Me he enfrentado al mismo problema en el que mi objetivo es conservar el valor de Enum String en la base de datos en lugar del valor ordinal.

Para superar este problema, he utilizado @Enumerated(EnumType.STRING)y mi objetivo se resolvió.

Por ejemplo, tienes una Enumclase:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

En la clase de entidad, defina @Enumerated(EnumType.STRING):

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

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

Mientras intenta establecer su valor en Base de datos, el valor de cadena se conservará en la base de datos como " APPLE", " ORANGE" o " LEMON".

SaravanaC
fuente
0

Puede usar un valor adicional en la constante de enumeración que puede sobrevivir tanto a los cambios de nombre como al recurso de las enumeraciones:

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

Para obtener la identificación de la enumeración:

int id = MyFirstValue.getId();

Para obtener la enumeración de una identificación:

MyEnum e = MyEnum.of(id);

Sugiero usar valores sin significado para evitar confusiones si los nombres de la enumeración deben cambiarse.

En el ejemplo anterior, he usado alguna variante de "Numeración básica de filas" dejando espacios para que los números permanezcan probablemente en el mismo orden que las enumeraciones.

Esta versión es más rápida que usar una tabla secundaria, pero hace que el sistema dependa más del código y del conocimiento del código fuente.

Para remediar eso, también puede configurar una tabla con los identificadores de enumeración en la base de datos. O vaya al otro lado y elija los identificadores de las enumeraciones de una tabla a medida que le agrega filas.

Nota al margen : Verifique siempre que no está diseñando algo que deba almacenarse en una tabla de base de datos y mantenerse como un objeto normal. Si puede imaginar que tiene que agregar nuevas constantes a la enumeración en este punto, cuando lo está configurando, es una indicación de que es mejor que cree un objeto regular y una tabla en su lugar.

Erk
fuente