데이터베이스에 열거 형을 저장하는 방법


123

열거 형을 데이터베이스에 저장하는 가장 좋은 방법은 무엇입니까?

Java가 제공 name()하고 valueOf()열거 형 값을 문자열로 변환하는 방법을 알고 있습니다. 그러나 이러한 값을 저장하는 다른 (유연한) 옵션이 있습니까?

열거 형을 고유 한 숫자로 만드는 현명한 방법이 ordinal()있습니까 (사용하기에 안전하지 않음)?

최신 정보:

모든 멋지고 빠른 답변에 감사드립니다! 내가 생각했던대로였다.

그러나 '툴킷'에 대한 참고 사항; 그것은 한 가지 방법입니다. 문제는 내가 만든 각 Enum 유형에 동일한 메서드를 추가해야한다는 것입니다. 그것은 많은 중복 코드이며 현재 Java는 이에 대한 솔루션을 지원하지 않습니다 (Java 열거 형은 다른 클래스를 확장 할 수 없음).


2
ordinal ()이 사용하기에 안전하지 않은 이유는 무엇입니까?
Michael Myers

어떤 종류의 데이터베이스? MySQL에는 enum 유형이 있지만 표준 ANSI SQL이라고 생각하지 않습니다.
Sherm Pendley

6
열거 형 추가는 끝에 넣어야하기 때문입니다. 순진한 개발자가 이것을 엉망으로 만들고 혼란을 야기하기 쉬움
oxbow_lakes

1
내가 참조. 데이터베이스를 많이 다루지 않는 것이 좋은 것 같아요. 왜냐하면 너무 늦기 전까지는 생각하지 않았을 것이기 때문입니다.
Michael Myers

답변:


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

9
열거 형 지속성이 필요한 경우 이름을 지속해서는 안됩니다. 다시 읽는 한 이름 대신 값을 사용하는 것이 더 간단합니다. SomeEnum enum1 = (SomeEnum) 2;
mamu

3
mamu : 숫자 등가물이 변경되면 어떻게됩니까?
Ian Boyd

2
이 방법을 사용하는 사람은 누구에게나 권장하지 않습니다. 문자열 표현을 사용하면 코드 유연성과 리팩토링이 제한됩니다. 고유 ID를 사용하는 것이 좋습니다. 또한 문자열을 저장하면 저장 공간이 낭비됩니다.
Tautvydas 2014

2
@LuisGouveia 시간이 두 배가 될 수 있다는 데 동의합니다. 소요 쿼리 유발 12.37 ms대신 취할를 12.3702 ms. 그것이 내가 의미하는 "in the noise" 입니다. 쿼리를 다시 실행하면 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는 스키마가 정규화 되었기 때문에 만족합니다 (오타가있을 수도 있고 없을 수도있는 전체 문자열 대신 플레이어 당 단일 정수 저장).
  3. 데이터베이스 값 ( suit_id)은 열거 값과 독립적이므로 다른 언어의 데이터에 대해서도 작업 할 수 있습니다.

14
정규화하고 DB에서 제한하는 것이 좋지만 두 곳의 업데이트로 인해 새 값 (코드 및 DB)이 추가되어 더 많은 오버 헤드가 발생할 수 있습니다. 또한 모든 업데이트가 Enum 이름에서 프로그래밍 방식으로 수행되는 경우 철자 오류가 없어야합니다.
Jason

3
위의 의견에 동의합니다. 데이터베이스 수준의 다른 적용 메커니즘은 잘못된 값을 사용하려는 삽입 또는 업데이트를 거부하는 제약 조건 트리거를 작성하는 것입니다.
Steve Perkins

1
두 곳에서 동일한 정보를 선언해야하는 이유는 무엇입니까? CODE에서 모두 public enum foo {bar}CREATE TABLE foo (name varchar);그 쉽게 동기화 밖으로 얻을 수 있습니다.
ebyrob

액면 그대로 받아 들여진 대답을 취한다면, 즉 열거 형 이름은 수동 조사에만 사용된다는 것입니다.이 대답이 실제로 최선의 선택입니다. 또한 열거 순서 나 값 또는 이름을 계속 변경하면이 추가 테이블을 유지하는 것보다 항상 훨씬 더 많은 문제가 발생합니다. 특히 디버깅 및 지원을 위해 필요한 경우 (일시적으로 만 생성하도록 선택할 수 있음).
afk5min

5

여기서 유일하게 안전한 메커니즘은 String name()값 을 사용하는 것 입니다. DB를 쓸 때, 당신은 할 수 값을 삽입 할 SPROC를 사용하여 읽을 때, 뷰를 사용합니다. 이러한 방식으로 열거 형이 변경되면 sproc / view에 간접적 인 수준이있어이를 DB에 "적용"하지 않고 열거 형 값으로 데이터를 표시 할 수 있습니다.


1
귀하의 솔루션과 @Ian Boyd의 솔루션의 하이브리드 접근 방식을 사용하고 있습니다. 팁 고마워!
technomalogical

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당신이 이름을 사용하여 행마다 추가로 3 바이트를 가지고 있으므로, 4 바이트입니다. 3 바이트 x 1 백만 행은 3MB입니다.
Ian Boyd

@IanBoyd :하지만 enumId2 바이트에 확실히 맞고 (자바에서는 더 긴 열거 형은 불가능) 대부분이 단일 바이트 (일부 DB 지원)에 맞습니다. 절약 된 공간은 무시할 수 있지만 빠른 비교와 고정 길이가 도움이 될 것입니다.
maaartinus

3

열거 형 이름 자체 만 저장합니다. 더 읽기 쉽습니다.

제한된 값 집합이있는 열거 형에 대한 특정 값을 저장하는 데 어려움을 겪었습니다. 예를 들어 문자를 사용하여 나타내는 제한된 상태 집합이있는 열거 형 (숫자 값보다 더 의미 있음) :

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 메서드를 작게 유지하려면 열거 형 내에 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);

deserialize에서 폴백하기 위해 기본 열거 형 값과 함께 사용하는 것이 좋습니다. 예를 들어 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 String 값을 유지하는 것과 동일한 문제에 직면했습니다.

이 문제를 극복하기 위해 나는 사용 @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);

열거 형 이름을 변경해야하는 경우 혼동을 피하기 위해 의미가없는 값을 사용하는 것이 좋습니다.

위의 예에서는 공백을 남겨두고 "기본 행 번호 매기기"의 변형을 사용하여 숫자가 열거 형과 동일한 순서로 유지 될 가능성이 높습니다.

이 버전은 보조 테이블을 사용하는 것보다 빠르지 만 시스템이 코드 및 소스 코드 지식에 더 의존하게 만듭니다.

이를 해결하기 위해 데이터베이스에 열거 형 ID를 사용하여 테이블을 설정할 수도 있습니다. 또는 다른 방법으로 행을 추가 할 때 테이블에서 열거 형의 ID를 선택하십시오.

사이드 노트 : 데이터베이스 테이블에 저장하고 일반 객체로 유지해야하는 것을 디자인하고 있지 않은지 항상 확인하십시오. 이 시점에서 열거 형에 새로운 상수를 추가해야한다고 상상할 수 있다면, 설정할 때 일반 객체와 테이블을 만드는 것이 더 나을 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.