Maneiras de salvar enumerações no banco de dados

123

Qual é a melhor maneira de salvar enumerações em um banco de dados?

Eu sei que o Java fornece name()e valueOf()métodos para converter valores de enum em uma String e vice-versa. Mas existem outras opções (flexíveis) para armazenar esses valores?

Existe uma maneira inteligente de transformar enums em números únicos ( ordinal()não é seguro de usar)?

Atualizar:

Obrigado por todas as respostas impressionantes e rápidas! Era como eu suspeitava.

No entanto, uma observação para 'toolkit'; Essa é uma maneira. O problema é que eu precisaria adicionar os mesmos métodos para cada tipo de Enum que eu criar. Há muito código duplicado e, no momento, o Java não suporta nenhuma solução para isso (um enum do Java não pode estender outras classes).

user20298
fonte
2
Por que ordinal () não é seguro?
Michael Myers
Que tipo de banco de dados? O MySQL tem um tipo de enum, mas não acho que seja o padrão ANSI SQL.
Sherm Pendley
6
Porque quaisquer adições enumerativas devem ser colocadas no final. Fácil para um programador desavisado estragar isso e causa estragos
oxbow_lakes
1
Entendo. Acho que é bom não lidar muito com bancos de dados, porque provavelmente não pensaria nisso até que fosse tarde demais.
Michael Myers

Respostas:

165

Nós não armazenar enumerações como valores ordinais numéricas mais; torna a depuração e o suporte muito difíceis. Armazenamos o valor real da enumeração convertido em string:

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 depois leia novamente com:

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

O problema estava no passado, encarando o Enterprise Manager e tentando decifrar:

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

versos

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

o último é muito mais fácil. O primeiro exigia obter o código-fonte e localizar os valores numéricos atribuídos aos membros da enumeração.

Sim, é preciso mais espaço, mas os nomes dos membros da enumeração são curtos e os discos rígidos são baratos, e vale muito a pena ajudar quando você está tendo um problema.

Além disso, se você usar valores numéricos, estará vinculado a eles. Você não pode inserir ou reorganizar os membros sem precisar forçar os valores numéricos antigos. Por exemplo, alterando a enumeração Suit para:

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

teria que se tornar:

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

para manter os valores numéricos herdados armazenados no banco de dados.

Como classificá-los no banco de dados

A pergunta surge: digamos que eu queria ordenar os valores. Algumas pessoas podem querer classificá-las pelo valor ordinal da enumeração. Obviamente, ordenar os cartões pelo valor numérico da enumeração não tem sentido:

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

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

Essa não é a ordem que queremos - queremos em ordem de enumeração:

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

O mesmo trabalho necessário se você salvar valores inteiros será necessário se você salvar seqüências de caracteres:

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

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

Mas essa não é a ordem que queremos - queremos em ordem de enumeração:

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

Minha opinião é que esse tipo de classificação pertence à interface do usuário. Se você estiver classificando itens com base no valor de enumeração: está fazendo algo errado.

Mas se você realmente quiser fazer isso, eu criaria uma Suitstabela de dimensões:

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

Dessa forma, quando você desejar alterar suas cartas para usar o Kissing Kings New Deck Order, poderá alterá-las para fins de exibição sem jogar fora todos os seus dados:

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

Agora, estamos separando um detalhe de programação interno (nome da enumeração, valor da enumeração) com uma configuração de exibição destinada aos usuários:

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
Ian Boyd
fonte
23
toString geralmente é substituído para fornecer valor de exibição. name () é uma escolha melhor como é, por definição, a contrapartida da valueOf ()
ddimitrov
9
Eu discordo totalmente disso, se a persistência enum é necessária, os nomes não devem persistir. no que diz respeito à leitura, é ainda mais simples com valor, em vez de nome, apenas pode ser classificado como SomeEnum enum1 = (SomeEnum) 2;
mamu 03/09/09
3
mamu: O que acontece quando os equivalentes numéricos mudam?
3107 Ian Boyd
2
Eu desencorajaria qualquer um que usasse essa abordagem. Amarrar-se à representação de cadeias limita a flexibilidade e refatoração do código. Você deve usar IDs únicos. O armazenamento de strings também desperdiça espaço de armazenamento.
Tautvydas
2
@LuisGouveia Concordo com você que o tempo pode dobrar. Causando uma consulta que leva 12.37 msa tomar 12.3702 ms. É isso que eu quero dizer com "no barulho" . Você executa a consulta novamente e é preciso 13.29 ms, ou 11.36 ms. Em outras palavras, a aleatoriedade do agendador de threads inundará drasticamente qualquer micro otimização que você teoricamente tenha que não seja de modo algum visível para ninguém de nenhuma maneira.
22719 Ian Boyd
42

A menos que você tenha motivos específicos de desempenho para evitá-lo, eu recomendaria o uso de uma tabela separada para a enumeração. Use integridade de chave estrangeira, a menos que a pesquisa extra realmente o mate.

Ternos tabela:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Tabela de jogadores

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Se você refatorar sua enumeração para ser classes com comportamento (como prioridade), seu banco de dados já a modelará corretamente
  2. Seu DBA está satisfeito porque seu esquema está normalizado (armazenando um único número inteiro por jogador, em vez de uma sequência inteira, que pode ou não ter erros de digitação).
  3. Os valores do seu banco de dados ( suit_id) são independentes do valor da enumeração, o que ajuda você a trabalhar com os dados de outros idiomas também.
Tom
fonte
14
Embora eu concorde que é bom tê-lo normalizado e restrito no banco de dados, isso causa atualizações em dois locais para adicionar um novo valor (código e db), o que pode causar mais sobrecarga. Além disso, erros de ortografia devem ser inexistentes se todas as atualizações forem feitas de forma programática a partir do nome do Enum.
1313 Jason
3
Eu concordo com o comentário acima. Um mecanismo de imposição alternativo no nível do banco de dados seria gravar um gatilho de restrição, que rejeitaria inserções ou atualizações que tentam usar um valor inválido.
Steve Perkins
1
Por que eu gostaria de declarar a mesma informação em dois lugares? Tanto no CODE public enum foo {bar}quanto no CREATE TABLE foo (name varchar);que pode facilmente ficar fora de sincronia.
Ebyrob 11/11/16
Se considerarmos a resposta aceita pelo valor nominal, ou seja, os nomes de enum são usados ​​apenas para investigações manuais, essa resposta é realmente a melhor opção. Além disso, se você continuar alterando a ordem de enumeração, valores ou nomes, sempre terá muito mais problemas do que manter essa tabela extra. Especialmente quando você só precisa (e pode optar por criar apenas temporariamente) para depuração e suporte.
afk5min
5

Eu argumentaria que o único mecanismo seguro aqui é usar o name()valor String . Ao escrever no banco de dados, você pode usar um sproc para inserir o valor e, ao ler, usar uma Visualização. Dessa maneira, se as enumerações mudarem, há um nível de indireção no sproc / view para poder apresentar os dados como o valor da enumeração sem "impor" isso no banco de dados.

oxbow_lakes
fonte
1
Estou usando uma abordagem híbrida da sua solução e a solução de @Ian Boyd com grande sucesso. Obrigado pela dica!
13159
5

Como você diz, ordinal é um pouco arriscado. Considere, por exemplo:

public enum Boolean {
    TRUE, FALSE
}

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

Se você armazenou isso como ordinais, poderá ter linhas como:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

Mas o que acontece se você atualizou o booleano?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Isso significa que todas as suas mentiras serão mal interpretadas como 'arquivo não encontrado'

Melhor usar apenas uma representação de string

kit de ferramentas
fonte
4

Para um banco de dados grande, reluto em perder as vantagens de tamanho e velocidade da representação numérica. Costumo terminar com uma tabela de banco de dados representando o Enum.

Você pode reforçar a consistência do banco de dados declarando uma chave estrangeira - embora em alguns casos seja melhor não declarar isso como uma restrição de chave estrangeira, o que impõe um custo a cada transação. Você pode garantir consistência, periodicamente, verificando, no momento da sua escolha, com:

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

A outra metade dessa solução é escrever um código de teste que verifique se a enum Java e a tabela de enum do banco de dados têm o mesmo conteúdo. Isso é deixado como um exercício para o leitor.

Roger Hayes
fonte
1
Digamos que o comprimento médio do nome da enumeração seja 7 caracteres. Como você enumIDtem quatro bytes, você tem três bytes extras por linha usando nomes. 3 bytes x 1 milhão de linhas são 3 MB.
11116 Ian
@ IanBoyd: Mas um enumIdcertamente cabe em dois bytes (enums mais longos não são possíveis em Java) e a maioria deles cabe em um único byte (que alguns DB suportam). O espaço economizado é insignificante, mas a comparação mais rápida e o comprimento fixo devem ajudar.
Maaartinus
3

Apenas armazenamos o próprio nome da enumeração - é mais legível.

Nós brincamos com o armazenamento de valores específicos para enumerações onde há um conjunto limitado de valores, por exemplo, essa enumeração que possui um conjunto limitado de status que usamos um caractere para representar (mais significativo que um 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;
        }
    }
}

e quando você tem muitos valores, precisa ter um mapa dentro da sua enumeração para manter pequeno o método getFromXYZ.

JeeBee
fonte
Se você não deseja manter uma instrução switch e pode garantir que o dbChar seja exclusivo, você pode usar algo como: public estático EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .ouElse (UNDEFINED); }
Kuchi
2

Se salvar enumerações como seqüências de caracteres no banco de dados, você poderá criar métodos utilitários para (des) serializar qualquer enumeração:

   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
fonte
É bom usar isso com um valor de enum padrão para voltar a desserializar. Por exemplo, pegue o IllegalArgEx e retorne Suit.None.
1313 Jason
2

Toda a minha experiência me diz que a maneira mais segura de persistir enumerações em qualquer lugar é usar valor ou código adicional de código (algum tipo de evolução da resposta do @jeebee). Este poderia ser um bom exemplo de uma ideia:

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

    private final String code;

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

    public String getCode() {
        return code;
    }
}

Agora você pode usar qualquer persistência referenciando suas constantes enum por seu código. Mesmo que você decida alterar alguns nomes de constantes, sempre poderá salvar o valor do código (por exemplo, DWARF("dwarf")para GNOME("dwarf"))

Ok, mergulhe um pouco mais fundo com essa concepção. Aqui está um método utilitário, que ajuda a encontrar qualquer valor de enumeração, mas primeiro vamos estender nossa abordagem.

interface CodeValue {
    String getCode();
}

E deixe nosso enum implementá-lo:

enum Race implement CodeValue {...}

Este é o momento do método de pesquisa 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;
}

E use-o como um encanto: Race race = resolveByCode(Race.class, "elf")

Metaphore
fonte
2

Eu enfrentei o mesmo problema em que meu objetivo é persistir o valor Enum String no banco de dados em vez do valor Ordinal.

Para superar esse problema, usei @Enumerated(EnumType.STRING)e meu objetivo foi resolvido.

Por exemplo, você tem uma Enumclasse:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

Na classe da entidade, defina @Enumerated(EnumType.STRING):

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

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

Enquanto você tenta definir seu valor como Banco de Dados, o valor da String será mantido no Banco de Dados como " APPLE", " ORANGE" ou " LEMON".

SaravanaC
fonte
0

Você pode usar um valor extra na constante de enumeração que possa sobreviver às alterações de nome e ao recurso das enumerações:

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 obter o ID da enumeração:

int id = MyFirstValue.getId();

Para obter a enumeração de um ID:

MyEnum e = MyEnum.of(id);

Sugiro usar valores sem significado para evitar confusão se os nomes de enum precisarem ser alterados.

No exemplo acima, usei uma variante da "Numeração básica de linhas", deixando espaços para que os números provavelmente permaneçam na mesma ordem que as enumerações.

Essa versão é mais rápida do que usar uma tabela secundária, mas torna o sistema mais dependente do código e do conhecimento do código-fonte.

Para remediar isso, você também pode configurar uma tabela com os IDs de enumeração no banco de dados. Ou siga o caminho inverso e escolha IDs para as enumerações de uma tabela à medida que você adiciona linhas a ela.

Nota : Sempre verifique sempre se você não está projetando algo que deve ser armazenado em uma tabela de banco de dados e mantido como um objeto regular. Se você pode imaginar que precisa adicionar novas constantes à enum neste momento, quando a estiver configurando, isso é uma indicação de que é melhor criar um objeto regular e uma tabela.

Erk
fonte