Avro 中的枚舉值序列化
1. 簡介
Apache Avro 是一個資料序列化框架,它提供豐富的資料結構和緊湊、快速的二進位資料格式。在 Java 應用程式中使用 Avro 時,我們經常需要序列化枚舉值。如果我們處理不當,這可能會很棘手。
在本教學中,我們將探討如何使用 Avro 正確序列化 Java 枚舉值。此外,我們將解決在 Avro 中使用枚舉時可能面臨的常見挑戰。
2. 理解 Avro 枚舉序列化
在 Avro 中,枚舉由一個名稱和一組符號定義。在序列化 Java 枚舉時,我們必須確保模式中的枚舉定義與 Java 枚舉定義相符。這很重要,因為 Avro 在序列化期間驗證枚舉值。
Avro 使用基於模式的方法,這意味著模式定義資料的結構,包括欄位名稱、類型以及在枚舉的情況下允許的符號值。因此,該模式充當序列化器和反序列化器之間的契約,從而有助於實現資料一致性。
讓我們先為我們的專案新增必要的 Avro Maven依賴項:
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.12.0</version>
</dependency>
3. 在 Avro Schema 中定義枚舉
首先,讓我們看看在建立 Avro 模式時如何正確定義枚舉:
Schema colorEnum = SchemaBuilder.enumeration("Color")
.namespace("com.baeldung.apache.avro")
.symbols("UNKNOWN", "GREEN", "RED", "BLUE");
這將建立一個具有四個可用值的枚舉模式。命名空間有助於防止命名衝突。此外,符號定義了有效的枚舉值。
現在,讓我們在記錄模式中使用這個枚舉:
Schema recordSchema = SchemaBuilder.record("ColorRecord")
.namespace("com.baeldung.apache.avro")
.fields()
.name("color")
.type(colorEnum)
.noDefault()
.endRecord();
此初始化創建了一個記錄模式ColorRecord
其中有一個名為color
的字段,屬於我們先前定義的Enum
類型。
4. 序列化枚舉值
現在我們已經定義了枚舉模式,讓我們來探索如何序列化枚舉值。
在本節中,我們將討論基本枚舉序列化的標準方法。此外,我們將解決處理聯合類型中的枚舉的常見挑戰,這常常引起混淆。
4.1.基本枚舉序列化的正確方法
為了正確序列化枚舉值,我們需要建立一個EnumSymbol
物件。因此,我們將使用適當的枚舉模式(colorEnum)
:
public void serializeEnumValue() throws IOException {
GenericRecord record = new GenericData.Record(recordSchema);
GenericData.EnumSymbol colorSymbol = new GenericData.EnumSymbol(colorEnum, "RED");
record.put("color", colorSymbol);
DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(recordSchema);
try (DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(recordSchema, new File("color.avro"));
dataFileWriter.append(record);
}
}
首先,我們根據recordSchema
建立一個GenericRecord
。接下來,我們使用枚舉模式( colorEnum
)和值「 RED
」來建立一個EnumSymbol
。最後,我們將其添加到我們的記錄中,並使用DatumWriter
和DataFileWriter.
現在,讓我們測試一下我們的實作:
@Test
void whenSerializingEnum_thenSuccess() throws IOException {
File file = tempDir.resolve("color.avro").toFile();
serializeEnumValue();
DatumReader<GenericRecord> datumReader = new GenericDatumReader<>(recordSchema);
try (DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(file, datumReader)) {
GenericRecord result = dataFileReader.next();
assertEquals("RED", result.get("color").toString());
}
}
此測試確認我們可以成功序列化和反序列化枚舉值。
4.2.使用枚舉處理聯合類型
現在,讓我們看看如何處理我們可能面臨的常見問題——在聯合類型中序列化枚舉:
Schema colorEnum = SchemaBuilder.enumeration("Color")
.namespace("com.baeldung.apache.avro")
.symbols("UNKNOWN", "GREEN", "RED", "BLUE");
Schema unionSchema = SchemaBuilder.unionOf()
.type(colorEnum)
.and()
.nullType()
.endUnion();
Schema recordWithUnionSchema = SchemaBuilder.record("ColorRecordWithUnion")
.namespace("com.baeldung.apache.avro")
.fields()
.name("color")
.type(unionSchema)
.noDefault()
.endRecord();
讓我們來分析一下定義的模式。我們定義了一個聯合模式,它可以是我們的枚舉類型,也可以是空。當欄位是可選的時,這種模式很常見。接下來,我們使用此聯合類型建立了一個帶有欄位的記錄模式。
因此,當我們在聯合中序列化枚舉時,我們仍然會使用EnumSymbol
,但使用正確的模式引用:
GenericRecord record = new GenericData.Record(recordWithUnionSchema);
GenericData.EnumSymbol colorSymbol = new GenericData.EnumSymbol(colorEnum, "RED");
record.put("color", colorSymbol);
這裡我們需要牢記的一個重要方面是,我們使用枚舉模式而不是聯合模式創建了EnumSymbol
。這是一個導致序列化錯誤的常見錯誤。
現在,讓我們測試聯合處理的實作:
@Test
void whenSerializingEnumInUnion_thenSuccess() throws IOException {
File file = tempDir.resolve("colorUnion.avro").toFile();
GenericRecord record = new GenericData.Record(recordWithUnionSchema);
GenericData.EnumSymbol colorSymbol = new GenericData.EnumSymbol(colorEnum, "GREEN");
record.put("color", colorSymbol);
DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(recordWithUnionSchema);
try (DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(recordWithUnionSchema, file);
dataFileWriter.append(record);
}
DatumReader<GenericRecord> datumReader = new GenericDatumReader<>(recordWithUnionSchema);
try (DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(file, datumReader)) {
GenericRecord result = dataFileReader.next();
assertEquals("GREEN", result.get("color").toString());
}
}
此外,我們還可以測試在聯合中處理null
值:
@Test
void whenSerializingNullInUnion_thenSuccess() throws IOException {
File file = tempDir.resolve("colorNull.avro").toFile();
GenericRecord record = new GenericData.Record(recordWithUnionSchema);
record.put("color", null);
DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(recordWithUnionSchema);
assertDoesNotThrow(() -> {
try (DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(recordWithUnionSchema, file);
dataFileWriter.append(record);
}
});
}
5. 使用枚舉進行模式演化
處理枚舉時,模式演進是一個特別敏感的領域,因為新增或刪除枚舉值可能會導致相容性問題。在本節中,我們將探討如何隨著需求的變化更新資料結構。我們將專注於使用枚舉類型並透過適當的預設值配置保持向後相容性。
5.1.新增新的枚舉值
當我們必須擴展我們的模式時,添加新的枚舉值需要仔細考慮。我們需要考慮相容性問題。因此,為了向後相容,添加預設值至關重要:
@Test
void whenSchemaEvolution_thenDefaultValueUsed() throws IOException {
String evolvedSchemaJson = "{\"type\":\"record\",
\"name\":\"ColorRecord\",
\"namespace\":\"com.baeldung.apache.avro\",
\"fields\":
[{\"name\":\"color\",
\"type\":
{\"type\":\"enum\",
\"name\":\"Color\",
\"symbols\":[\"UNKNOWN\",\"GREEN\",\"RED\",\"BLUE\",\"YELLOW\"],
\"default\":\"UNKNOWN\"
}}]
}";
Schema evolvedRecordSchema = new Schema.Parser().parse(evolvedSchemaJson);
Schema evolvedEnum = evolvedRecordSchema.getField("color").schema();
File file = tempDir.resolve("colorEvolved.avro").toFile();
GenericRecord record = new GenericData.Record(evolvedRecordSchema);
GenericData.EnumSymbol colorSymbol = new GenericData.EnumSymbol(evolvedEnum, "YELLOW");
record.put("color", colorSymbol);
DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(evolvedRecordSchema);
try (DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(evolvedRecordSchema, file);
dataFileWriter.append(record);
}
String originalSchemaJson = "{\"type\":\"record\",
\"name\":\"ColorRecord\",
\"namespace\":\"com.baeldung.apache.avro\",
\"fields\":[{
\"name\":\"color\",
\"type\":
{\"type\":\"enum\",
\"name\":\"Color\",
\"symbols\":[\"UNKNOWN\",\"GREEN\",\"RED\",\"BLUE\"],
\"default\":\"UNKNOWN\"}}]
}";
Schema originalRecordSchema = new Schema.Parser().parse(originalSchemaJson);
DatumReader<GenericRecord> datumReader =
new GenericDatumReader<>(evolvedRecordSchema, originalRecordSchema);
try (DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(file, datumReader)) {
GenericRecord result = dataFileReader.next();
assertEquals("UNKNOWN", result.get("color").toString());
}
}
現在,我們來分析一下上面的程式碼。我們改進了我們的模式( evolvedSchemaJson
)並添加了一個新符號“ YELLOW
”。接下來,我們建立了一個具有「 YELLOW
」枚舉值的記錄,並將其寫入檔案中。
然後,我們建立了一個「原始模式」( originalSchemaJson originalSchemaJson)
,但具有相同的預設值。為了避免忘記,我們之前已經指出添加預設值對於向後相容很重要。
最後,當我們使用原始模式讀取資料時,我們正在驗證使用預設值「 UNKNOWN
」而不是「 YELLOW
」。
為了使用枚舉進行正確的模式演變,我們需要在枚舉類型層級而不是欄位層級指定預設值。對於我們的例子,這就是我們使用 JSON 字串來定義我們的模式的原因,因為它讓我們可以直接控制結構。
6. 結論
在本文中,我們探討如何使用 Apache Avro 正確序列化枚舉值。我們研究了基本的枚舉序列化、處理枚舉聯合以及解決模式演變挑戰。
在 Avro 中使用枚舉時,我們應該記住一些關鍵點。首先,我們需要使用正確的命名空間和符號來定義我們的枚舉模式。使用帶有適當枚舉模式引用的GenericData.EnumSymbol
非常重要。
此外,對於聯合類型,我們使用枚舉模式而不是聯合模式來建立枚舉符號。
最後,關於模式演變,我們需要將預設值放在枚舉類型層級以實現適當的相容性。
與往常一樣,程式碼可在 GitHub 上取得。