Cara untuk menyimpan enum di database

123

Apa cara terbaik untuk menyimpan enum ke dalam database?

Saya tahu Java menyediakan name()dan valueOf()metode untuk mengubah nilai enum menjadi String dan kembali. Tetapi apakah ada opsi (fleksibel) lain untuk menyimpan nilai-nilai ini?

Adakah cara cerdas untuk membuat enum menjadi angka unik ( ordinal()tidak aman digunakan)?

Memperbarui:

Terima kasih untuk semua jawaban yang luar biasa dan cepat! Seperti yang saya duga.

Namun catatan untuk 'toolkit'; Itu salah satu cara. Masalahnya adalah saya harus menambahkan metode yang sama untuk setiap jenis Enum yang saya buat. Itu banyak kode duplikat dan, saat ini, Java tidak mendukung solusi apa pun untuk ini (enum Java tidak dapat memperluas kelas lain).

pengguna20298
sumber
2
Mengapa ordinal () tidak aman digunakan?
Michael Myers
Jenis database apa? MySQL memiliki tipe enum, tapi menurut saya itu bukan SQL ANSI standar.
Sherm Pendley
6
Karena penambahan enumeratif apa pun harus diletakkan di akhir. Mudah bagi pengembang yang tidak curiga untuk mengacaukan ini dan menyebabkan malapetaka
oxbow_lakes
1
Saya melihat. Kurasa itu hal yang baik karena saya tidak terlalu banyak berurusan dengan database, karena saya mungkin tidak akan memikirkannya sampai semuanya sudah terlambat.
Michael Myers

Jawaban:

165

Kami tidak pernah lagi menyimpan enumerasi sebagai nilai ordinal numerik; itu membuat proses debug dan dukungan menjadi terlalu sulit. Kami menyimpan nilai pencacahan aktual yang dikonversi ke 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());

dan kemudian baca kembali dengan:

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

Masalahnya adalah di masa lalu saat menatap Manajer Perusahaan dan mencoba menguraikan:

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

ayat-ayat

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

yang terakhir jauh lebih mudah. Yang pertama membutuhkan kode sumber dan menemukan nilai-nilai numerik yang diberikan kepada anggota pencacahan.

Ya, ini membutuhkan lebih banyak ruang, tetapi nama anggota pencacahan pendek, dan hard drive murah, dan jauh lebih berharga untuk membantu saat Anda mengalami masalah.

Selain itu, jika Anda menggunakan nilai numerik, Anda terikat padanya. Anda tidak dapat memasukkan atau mengatur ulang anggota dengan baik tanpa harus memaksakan nilai numerik lama. Misalnya, mengubah pencacahan Suit menjadi:

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

harus menjadi:

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

untuk mempertahankan nilai numerik warisan yang disimpan dalam database.

Bagaimana mengurutkannya di database

Muncul pertanyaan: katakanlah saya ingin memesan nilai. Beberapa orang mungkin ingin mengurutkannya berdasarkan nilai ordinal enum. Tentu saja, memesan kartu dengan nilai numerik dari pencacahan tidak ada artinya:

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

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

Itu bukan urutan yang kami inginkan - kami menginginkannya dalam urutan pencacahan:

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

Pekerjaan yang sama yang diperlukan jika Anda menyimpan nilai integer diperlukan jika Anda menyimpan string:

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

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

Tapi itu bukan urutan yang kami inginkan - kami ingin mereka dalam urutan pencacahan:

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

Pendapat saya adalah bahwa peringkat semacam ini termasuk dalam antarmuka pengguna. Jika Anda menyortir item berdasarkan nilai pencacahannya: Anda melakukan kesalahan.

Tetapi jika Anda benar-benar ingin melakukannya, saya akan membuat Suitstabel dimensi:

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

Dengan cara ini, ketika Anda ingin mengubah kartu Anda untuk menggunakan Kissing Kings New Deck Order, Anda dapat mengubahnya untuk tujuan tampilan tanpa membuang semua data Anda:

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

Sekarang kami memisahkan detail pemrograman internal (nama enumerasi, nilai enumerasi) dengan pengaturan tampilan yang ditujukan untuk pengguna:

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
Ian Boyd
sumber
23
toString sering diganti untuk memberikan nilai tampilan. name () adalah pilihan yang lebih baik karena menurut definisi merupakan mitra valueOf ()
ddimitrov
9
Saya sangat tidak setuju dengan ini, jika persistensi enum diperlukan maka nama tidak boleh dipertahankan. sejauh membacanya kembali berjalan itu bahkan lebih sederhana dengan nilai daripada nama hanya dapat mengetikkannya sebagai SomeEnum enum1 = (SomeEnum) 2;
mamu
3
mamu: Apa yang terjadi jika padanan numeriknya berubah?
Ian Boyd
2
Saya akan mengecilkan hati siapa pun yang menggunakan pendekatan ini. Mengikat diri Anda sendiri pada representasi string membatasi fleksibilitas dan pemfaktoran ulang kode. Sebaiknya Anda menggunakan id unik. Menyimpan string juga membuang-buang ruang penyimpanan.
Tautvydas
2
@LuisGouveia Saya setuju dengan Anda bahwa waktu dapat berlipat ganda. Menyebabkan kueri yang mengambil 12.37 msalih-alih mengambil 12.3702 ms. Itulah yang saya maksud dengan "dalam kebisingan" . Anda menjalankan kueri lagi dan itu membutuhkan 13.29 ms, atau 11.36 ms. Dengan kata lain, keacakan penjadwal utas akan secara drastis membanjiri pengoptimalan mikro yang secara teoritis Anda miliki yang sama sekali tidak terlihat oleh siapa pun dengan cara apa pun.
Ian Boyd
42

Kecuali Anda memiliki alasan kinerja tertentu untuk menghindarinya, saya akan merekomendasikan menggunakan tabel terpisah untuk pencacahan. Gunakan integritas kunci asing kecuali pencarian ekstra benar-benar membunuh Anda.

Meja setelan:

suit_id suit_name
1       Clubs
2       Hearts
3       Spades
4       Diamonds

Meja pemain

player_name suit_id
Ian Boyd           4
Shelby Lake        2
  1. Jika Anda pernah merefaktor enumerasi Anda menjadi kelas dengan perilaku (seperti prioritas), database Anda sudah memodelkannya dengan benar
  2. DBA Anda senang karena skema Anda dinormalisasi (menyimpan satu bilangan bulat per pemain, bukan seluruh string, yang mungkin memiliki kesalahan ketik atau tidak).
  3. Nilai database Anda ( suit_id) tidak bergantung pada nilai enumerasi Anda, yang membantu Anda mengerjakan data dari bahasa lain juga.
Tom
sumber
14
Meskipun saya setuju itu bagus untuk menormalkannya, dan dibatasi di DB, ini memang menyebabkan pembaruan di dua tempat untuk menambahkan nilai baru (kode dan db), yang mungkin menyebabkan lebih banyak overhead. Selain itu, kesalahan ejaan seharusnya tidak ada jika semua pembaruan dilakukan secara programatik dari nama Enum.
Jason
3
Saya setuju dengan komentar di atas. Mekanisme penegakan alternatif di tingkat database adalah menulis pemicu batasan, yang akan menolak penyisipan atau pembaruan yang mencoba menggunakan nilai yang tidak valid.
Steve Perkins
1
Mengapa saya ingin menyatakan informasi yang sama di dua tempat? Keduanya dalam CODE public enum foo {bar}dan CREATE TABLE foo (name varchar);dapat dengan mudah keluar dari sinkronisasi.
ebyrob
Jika kita mengambil jawaban yang diterima pada nilai nominalnya, yaitu nama enum hanya digunakan untuk penyelidikan manual, maka jawaban ini memang merupakan pilihan terbaik. Juga, jika Anda terus mengubah urutan pencacahan atau nilai atau nama, Anda akan selalu memiliki lebih banyak masalah daripada mempertahankan tabel tambahan ini. Terutama ketika Anda hanya membutuhkannya (dan mungkin memilih untuk membuat hanya sementara) untuk debugging dan dukungan.
afk5min
5

Saya berpendapat bahwa satu-satunya mekanisme yang aman di sini adalah menggunakan nilai String name(). Saat menulis ke DB, Anda dapat menggunakan sproc untuk memasukkan nilai dan saat membaca, gunakan View. Dengan cara ini, jika enum berubah, ada tingkat tipuan di sproc / view untuk dapat menyajikan data sebagai nilai enum tanpa "memaksakan" ini pada DB.

oxbow_lakes
sumber
1
Saya menggunakan pendekatan hibrid dari solusi Anda dan solusi @Ian Boyd dengan sukses besar. Terima kasih atas tipnya!
technomalogical
5

Seperti yang Anda katakan, ordinal agak berisiko. Pertimbangkan misalnya:

public enum Boolean {
    TRUE, FALSE
}

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

Jika Anda menyimpannya sebagai ordinal, Anda mungkin memiliki baris seperti:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

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

Tetapi apa yang terjadi jika Anda memperbarui Boolean?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

Ini berarti semua kebohongan Anda akan disalahartikan sebagai 'file-not-found'

Lebih baik hanya menggunakan representasi string

toolkit
sumber
4

Untuk database yang besar, saya enggan kehilangan keunggulan ukuran dan kecepatan representasi numerik. Saya sering berakhir dengan tabel database yang mewakili Enum.

Anda dapat menerapkan konsistensi database dengan mendeklarasikan kunci asing - meskipun dalam beberapa kasus mungkin lebih baik untuk tidak mendeklarasikannya sebagai batasan kunci asing, yang membebankan biaya pada setiap transaksi. Anda dapat memastikan konsistensi dengan melakukan pemeriksaan secara berkala, pada waktu yang Anda pilih, dengan:

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

Bagian lain dari solusi ini adalah menulis beberapa kode pengujian yang memeriksa bahwa enum Java dan tabel enum database memiliki konten yang sama. Itu tersisa sebagai latihan untuk pembaca.

Roger Hayes
sumber
1
Katakanlah rata-rata panjang nama pencacahan adalah 7 karakter. Anda enumIDadalah empat byte, jadi Anda memiliki tiga byte tambahan per baris dengan menggunakan nama. 3 byte x 1 juta baris adalah 3MB.
Ian Boyd
@IanBoyd: Tapi enumIdpasti cocok dengan dua byte (enum yang lebih panjang tidak dimungkinkan di Java) dan kebanyakan dari mereka muat dalam satu byte (yang didukung oleh beberapa DB). Ruang yang disimpan dapat diabaikan, tetapi perbandingan yang lebih cepat dan panjang yang tetap akan membantu.
maaartinus
3

Kami hanya menyimpan nama enum itu sendiri - ini lebih mudah dibaca.

Kami bermain-main dengan menyimpan nilai spesifik untuk enum di mana ada sekumpulan nilai terbatas, misalnya, enum ini yang memiliki sekumpulan status terbatas yang kami gunakan untuk mewakili char (lebih bermakna daripada nilai numerik):

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

dan ketika Anda memiliki banyak nilai, Anda perlu memiliki Peta di dalam enum Anda untuk menjaga metode getFromXYZ tetap kecil.

JeeBee
sumber
Jika Anda tidak ingin mempertahankan pernyataan switch dan dapat memastikan bahwa dbChar unik, Anda dapat menggunakan sesuatu seperti: public static EmailStatus getFromStatusChar (char statusChar) {return Arrays.stream (EmailStatus.values ​​()) .filter (e -> e.statusChar () == statusChar) .findFirst () .orElse (BELUM DITENTUKAN); }
Kuchi
2

Jika menyimpan enum sebagai string dalam database, Anda dapat membuat metode utilitas untuk (de) membuat serialisasi enum apa pun:

   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
sumber
Senang menggunakan ini dengan nilai enum default untuk kembali ke deserialize. Misalnya, tangkap IllegalArgEx dan kembalikan Suit.None.
Jason
2

Semua pengalaman saya memberi tahu saya bahwa cara teraman untuk mempertahankan enum di mana saja adalah dengan menggunakan nilai kode tambahan atau id (semacam evolusi dari jawaban @jeebee). Ini bisa menjadi contoh ide yang bagus:

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

    private final String code;

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

    public String getCode() {
        return code;
    }
}

Sekarang Anda dapat menggunakan ketekunan apa pun yang mereferensikan konstanta enum Anda dengan kodenya. Bahkan jika Anda akan memutuskan untuk mengubah beberapa nama konstan, Anda selalu dapat menyimpan nilai kode (misalnya DWARF("dwarf")ke GNOME("dwarf"))

Oke, selami lebih dalam dengan konsep ini. Berikut adalah beberapa metode utilitas, yang membantu Anda menemukan nilai enum apa pun, tetapi pertama-tama mari kita perluas pendekatan kita.

interface CodeValue {
    String getCode();
}

Dan biarkan enum kita mengimplementasikannya:

enum Race implement CodeValue {...}

Ini adalah waktu untuk metode pencarian ajaib:

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

Dan gunakan seperti pesona: Race race = resolveByCode(Race.class, "elf")

Metafora
sumber
2

Saya telah menghadapi masalah yang sama di mana tujuan saya adalah untuk mempertahankan nilai Enum String ke dalam database, bukan nilai Ordinal.

Untuk mengatasi masalah ini, saya telah menggunakan @Enumerated(EnumType.STRING)dan tujuan saya terselesaikan.

Misalnya, Anda memiliki EnumKelas:

public enum FurthitMethod {

    Apple,
    Orange,
    Lemon
}

Di kelas entitas, tentukan @Enumerated(EnumType.STRING):

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

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

Saat Anda mencoba menyetel nilai Anda ke Database, nilai String akan disimpan ke dalam Database sebagai " APPLE", " ORANGE" atau " LEMON".

SaravanaC
sumber
0

Anda dapat menggunakan nilai tambahan dalam konstanta enum yang dapat bertahan dari perubahan nama dan penggunaan enum:

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

Untuk mendapatkan id dari enum:

int id = MyFirstValue.getId();

Untuk mendapatkan enum dari id:

MyEnum e = MyEnum.of(id);

Saya menyarankan menggunakan nilai tanpa arti untuk menghindari kebingungan jika nama enum harus diubah.

Dalam contoh di atas, saya telah menggunakan beberapa varian dari "Penomoran baris dasar" meninggalkan spasi sehingga angka tersebut kemungkinan akan tetap dalam urutan yang sama seperti enum.

Versi ini lebih cepat daripada menggunakan tabel sekunder, tetapi membuat sistem lebih bergantung pada kode dan pengetahuan kode sumber.

Untuk mengatasinya, Anda juga dapat menyiapkan tabel dengan id enum di database. Atau pergi ke arah lain dan pilih id untuk enum dari tabel saat Anda menambahkan baris ke dalamnya.

Catatan kecil : Selalu verifikasi bahwa Anda tidak mendesain sesuatu yang harus disimpan dalam tabel database dan dipertahankan sebagai objek biasa. Jika Anda dapat membayangkan bahwa Anda harus menambahkan konstanta baru ke enum pada saat ini, saat Anda menyiapkannya, itu adalah indikasi Anda mungkin lebih baik membuat objek biasa dan tabel sebagai gantinya.

Erk
sumber