列挙型をデータベースに保存する方法


123

列挙型をデータベースに保存する最良の方法は何ですか?

私は、Javaが提供する知っているname()valueOf()文字列と背中に列挙型の値を変換する方法。しかし、これらの値を保存する他の(柔軟な)オプションはありますか?

列挙型を一意の番号にするスマートな方法はありordinal()ますか?

更新:

素晴らしくて速い答えをありがとう!思った通りでした。

ただし、「ツールキット」へのメモ。それは片道です。問題は、作成する各Enum型に同じメソッドを追加する必要があることです。これは多くの重複コードであり、現時点ではJavaはこれに対するソリューションをサポートしていません(Java enumは他のクラスを拡張できません)。


2
ordinal()を使用しても安全ではないのはなぜですか?
マイケルマイヤーズ

どんなデータベース?MySQLには列挙型がありますが、標準のANSI SQLではないと思います。
シェルムペンドリー、2008年

6
列挙型の追加は最後に追加する必要があるためです。無防備な開発者がこれを台無しにして大混乱を引き起こすのは簡単
oxbow_lakes

1
そうですか。手遅れになるまでそれを考えていなかったので、データベースをあまり扱っていないのは良いことだと思います。
マイケルマイヤーズ

回答:


165

私たちは、決してもう数値序数値として列挙を格納しません。デバッグとサポートが非常に難しくなります。文字列に変換された実際の列挙値を保存します。

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

Suit theSuit = Suit.Heart;

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

そして次に読み直してください:

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

問題は過去にEnterprise Managerを見つめて解読しようとしていました:

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

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

後者の方がはるかに簡単です。前者は、ソースコードにアクセスして、列挙型メンバーに割り当てられた数値を見つける必要がありました。

はい、それはより多くのスペースを必要としますが、列挙メンバー名は短く、ハードドライブは安価であり、問​​題が発生しているときに役立つとはるかに価値があります。

さらに、数値を使用する場合は、それらに関連付けられます。古い数値を強制することなく、メンバーをうまく挿入または再配置することはできません。たとえば、Suit列挙を次のように変更します。

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

になる必要があります:

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

データベースに保存されている従来の数値を維持するため。

それらをデータベースでソートする方法

質問が出てきました:値を注文したいとしましょう。一部の人々は列挙型の序数値でそれらをソートしたいかもしれません。もちろん、列挙の数値によるカードの順序付けは無意味です。

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

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

それは私たちが望む順序ではありません-それらを列挙順にしたいです:

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

文字列を保存する場合、整数値を保存する場合と同じ作業が必要です。

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

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

しかし、それは私たちが望む順序ではありません-それらを列挙順にしたいです:

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

私の意見では、この種のランキングはユーザーインターフェイスに属します。列挙値に基づいてアイテムを並べ替えている場合:何かが間違っています。

しかし、本当にそうしたいのであれば、Suitsディメンションテーブルを作成します。

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

このように、Kissing Kings New Deck Orderを使用するようにカードを変更したい場合、すべてのデータを破棄せずに表示目的で変更できます。

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

ここで、内部プログラミングの詳細(列挙名、列挙値)をユーザー向けの表示設定で分離します。

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

23
toStringは多くの場合、表示値を提供するためにオーバーライドされます。name()は、valueOf()に対応するものであるため、より適切な選択です
ddimitrov 2008年

9
列挙型の永続化が必要な場合は名前を永続化しないでください。読み返す限り、名前の代わりに値を使用する方が簡単です。SomeEnumenum1 =(SomeEnum)2;として型キャストできます。
mamu

3
mamu:相当する数値が変わるとどうなりますか?
Ian Boyd

2
私はこのアプローチを使う人をだまします。文字列表現に縛られると、コードの柔軟性とリファクタリングが制限されます。一意のIDを使用することをお勧めします。また、ストリングを保管すると、保管スペースが無駄になります。
Tautvydas 2014

2
@LuisGouveia時間は倍になるかもしれないとあなたに同意します。12.37 ms代わりにtake をとるクエリを引き起こします12.3702 ms。それが「ノイズの中」という意味です。もう一度クエリを実行すると13.29 ms、またはがかかります11.36 ms。言い換えれば、スレッドスケジューラのランダム性は、理論的に持っているあらゆるマイクロ最適化を大幅に損なうことになります。
Ian Boyd

42

特定のパフォーマンス上の理由で回避できない場合を除き、列挙には別のテーブルを使用することをお勧めします。追加のルックアップが本当にあなたを殺さない限り、外部キーの整合性を使用してください。

スーツテーブル:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

プレイヤーズテーブル

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. 列挙を動作(優先度など)のあるクラスにリファクタリングする場合、データベースはすでにそれを正しくモデル化しています
  2. スキーマが正規化されているため、DBAは満足しています(タイプミスがある場合とない場合がある文字列全体ではなく、プレーヤーごとに1つの整数を格納します)。
  3. データベースの値(suit_id)は列挙値から独立しているため、他の言語のデータを操作する場合にも役立ちます。

14
それを正規化してDBで制約することは良いことだと私は同意しますが、これにより2つの場所で更新が行われ、新しい値(コードとdb)が追加され、オーバーヘッドが増える可能性があります。また、すべての更新がEnum名からプログラムで行われる場合、スペルミスは存在しないはずです。
ジェイソン・

3
上記のコメントに同意します。データベースレベルでの代替の強制メカニズムは、無効な値を使用しようとする挿入または更新を拒否する制約トリガーを作成することです。
スティーブパーキンス

1
2か所で同じ情報を宣言する必要があるのはなぜですか?CODEでもpublic enum foo {bar}CREATE TABLE foo (name varchar);簡単に同期しなくなる可能性もあります。
ebyrob 2016

受け入れられた回答を額面どおりに受け取る場合、つまり、列挙名は手動の調査にのみ使用される場合、この回答は確かに最良のオプションです。また、列挙の順序、値、または名前の変更を続けると、この余分なテーブルを維持するよりも常に多くの問題が発生します。特に、デバッグとサポートのために必要なだけの場合(および一時的に作成することを選択した場合)。
afk5min 2017年

5

ここでの唯一の安全なメカニズムは文字列name()値を使用することであると私は主張します。DBに書き込む場合、sprocを使用して値を挿入し、読み取る場合はビューを使用できます。このようにして、列挙型が変更された場合、sproc /ビューには間接参照のレベルがあり、これをDBに「課す」ことなく列挙型値としてデータを提示できます。


1
私はあなたのソリューションと@Ian Boydのソリューションのハイブリッドアプローチを使用しています。先端をありがとう!
テクノロジー09

5

あなたが言うように、序数は少し危険です。例を考えてみましょう:

public enum Boolean {
    TRUE, FALSE
}

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

これを序数として保存した場合、次のような行が含まれる可能性があります。

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

しかし、ブール値を更新するとどうなりますか?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

これは、すべての嘘が「ファイルが見つかりません」と誤って解釈されることを意味します

文字列表現を使用する方が良い


4

大規模なデータベースの場合、数値表現のサイズと速度の利点を失うことに消極的です。Enumを表すデータベーステーブルが作成されることがよくあります。

外部キーを宣言することでデータベースの一貫性を強制できます-場合によっては、それを外部キー制約として宣言しない方がよい場合があります。これは、すべてのトランザクションにコストを課します。以下を使用して、選択した時間に定期的にチェックを行うことにより、一貫性を確保できます。

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

このソリューションの残りの半分は、Java enumとデータベースenumテーブルが同じ内容であることを確認するテストコードを記述することです。それは読者の練習問題として残しておきます。


1
列挙名の平均の長さが7文字であるとします。あなたのenumIDは4バイトなので、名前を使用して行ごとに追加の3バイトがあります。3バイトx 100万行は3MBです。
Ian Boyd

@IanBoyd:しかし、enumId確かに2バイトに収まり(Javaでは長い列挙は不可能)、それらのほとんどは1バイトに収まります(一部のDBはこれをサポートします)。節約されるスペースはごくわずかですが、比較の高速化と固定長が役立つはずです。
maaartinus 2014年

3

enum名自体を保存するだけです。読みやすくなっています。

値のセットが限られている列挙型の特定の値を格納することをいじくり回しました。たとえば、この列挙型は、charを使用して表す数値のセットに制限があります(数値よりも意味があります)。

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

また、値が多い場合は、getFromXYZメソッドを小さく保つために、enum内にMapを含める必要があります。


switchステートメントを維持せず、dbCharが一意であることを確認できる場合は、次のようなものを使用できます。public static EmailStatus getFromStatusChar(char statusChar){return Arrays.stream(EmailStatus.values()).filter(e-> e.statusChar()== statusChar).findFirst().orElse(UNDEFINED); }
Kuchi

2

列挙型をデータベースに文字列として保存する場合、列挙型を(逆)シリアル化するユーティリティメソッドを作成できます。

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

これをデフォルトの列挙値とともに使用して、デシリアライズでフォールバックするのはいいことです。たとえば、IllegalArgExをキャッチしてSuit.Noneを返します。
Jason、

2

私のすべての経験から、列挙型を永続化する最も安全な方法は、追加のコード値またはID(@jeebee回答のある種の進化)を使用することです。これはアイデアの良い例かもしれません:

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

    private final String code;

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

    public String getCode() {
        return code;
    }
}

これで、列挙型定数をコードで参照する永続性を使用できます。あなたは定数名の一部を変更することにしたよ場合でも、あなたは常にコード値を保存することができます(たとえばDWARF("dwarf")までGNOME("dwarf")

では、この概念をさらに深く掘り下げましょう。これは、列挙値を見つけるのに役立つユーティリティメソッドですが、最初にアプローチを拡張します。

interface CodeValue {
    String getCode();
}

そして私たちの列挙型にそれを実装させましょう:

enum Race implement CodeValue {...}

これは魔法の検索方法の時です:

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

そしてそれを魅力のように使ってください: Race race = resolveByCode(Race.class, "elf")


2

私の目的が序数値の代わりにEnum文字列値をデータベースに永続化することであるのと同じ問題に直面しました。

この問題を克服するために、私は使用し@Enumerated(EnumType.STRING)、私の目的は解決しました。

たとえば、あなたはEnumクラスを持っています:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

エンティティークラスで、次を定義します@Enumerated(EnumType.STRING)

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

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

値をデータベースに設定しようとすると、文字列値は「APPLE」、「ORANGE」または「LEMON」としてデータベースに永続化されます。



0

名前の変更と列挙型の再ソートの両方に対応できる列挙型定数で追加の値を使用できます。

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

列挙型からIDを取得するには:

int id = MyFirstValue.getId();

IDから列挙型を取得するには:

MyEnum e = MyEnum.of(id);

enum名を変更する必要がある場合、混乱を避けるために、意味のない値を使用することをお勧めします。

上記の例では、「基本的な行番号付け」のバリアントをいくつか使用してスペースを残しているため、番号は列挙型と同じ順序にとどまる可能性があります。

このバージョンは、セカンダリテーブルを使用するよりも高速ですが、システムがコードとソースコードの知識により依存するようになります。

これを修正するには、データベースに列挙IDを含むテーブルを設定することもできます。または、逆に行を追加して、テーブルから列挙型のIDを選択します。

補足:データベーステーブルに格納し、通常のオブジェクトとして維持する必要があるものを設計していないことを常に確認してください。この時点で列挙型に新しい定数を追加する必要があると想像できる場合は、それを設定するときに、通常のオブジェクトとテーブルを作成した方がよい場合があることを示しています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.