Lesson for java.math.BigDecimal in Avro
Doskonale! Ten fragment kodu, który pokazałeś, jest kluczowy do zrozumienia drugiego, bardziej zaawansowanego sposobu pracy z Avro.
Mój poprzedni opis koncentrował się na API Specyficznym (SpecificRecord), gdzie klasy Javy są generowane z pliku .avsc. Jest to najprostsze i najczęstsze podejście.
Twój kod jest przykładem pracy z API Generycznym (GenericRecord), które jest używane, gdy nie chcesz lub nie możesz generować klas Javy. Jest to typowe w systemach przetwarzania danych (np. Spark, Flink) lub w narzędziach, które muszą obsługiwać dowolne schematy Avro w locie.
Przeanalizujmy Twój kod i jego kontekst.
Dlaczego Twój kod jest potrzebny? Różnica między API Specyficznym a Generycznym
- API Specyficzne (
SpecificRecord, np. wygenerowana klasaProduct)
- Zapis: Wywołujesz
product.setPrice(new BigDecimal("123.45")). - Co się dzieje w tle: Wygenerowany setter
setPricesam wykonuje konwersję zBigDecimalnaByteBuffer(lubbyte[]) za pomocą dokładnie tej samej logiki, którą masz w swojej metodziegetByteBuffer. Ta konwersja jest dla Ciebie ukryta.
- API Generyczne (
GenericRecord)
GenericRecordto w zasadzie mapa (Map<String, Object>). Nie ma metodysetPrice. Używaszrecord.put("price", value).- Biblioteka Avro, pracując z
GenericRecord, oczekuje, że dla pola zdefiniowanego w schemacie jakobytespodasz wartość typujava.nio.ByteBuffer. - Nie możesz po prostu zrobić
record.put("price", new BigDecimal("123.45")), ponieważBigDecimalto nieByteBuffer. - I tu właśnie wkracza Twoja metoda
getByteBuffer! Jej zadaniem jest ręczne przeprowadzenie tej konwersji, którą API Specyficzne robi automatycznie. Tworzy onaByteBufferz obiektuBigDecimalzgodnie z metadanymiprecisioniscaleze schematu.
Wniosek: Twój kod jest absolutnie poprawny i jest to standardowy sposób ręcznej konwersji BigDecimal na ByteBuffer przy pracy z GenericRecord i typami logicznymi.
Poprawna obsługa BigDecimal przy użyciu API Generycznego (GenericRecord)
Poniżej znajduje się kompletny przykład zapisu i odczytu z wykorzystaniem Twojego podejścia.
Schemat (product.avsc - bez zmian)
{
"type": "record",
"name": "Product",
"namespace": "pl.example.avro",
"fields": [
{ "name": "name", "type": "string" },
{
"name": "price",
"type": {
"type": "bytes",
"logicalType": "decimal",
"precision": 9,
"scale": 2
}
}
]
}
Zapis (Serializacja) z GenericRecord
import org.apache.avro.Conversions;
import org.apache.avro.LogicalType;
import org.apache.avro.Schema;
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumWriter;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
public class AvroGenericWriteExample {
// Twoja metoda pomocnicza (może być statyczna)
private static ByteBuffer bigDecimalToByteBuffer(Schema fieldSchema, BigDecimal value) {
// Upewnijmy się, że skala jest poprawna! To jest best practice.
int scale = fieldSchema.getJsonProp("scale").asInt();
BigDecimal scaledValue = value.setScale(scale, RoundingMode.HALF_UP);
// Używamy wbudowanego w Avro konwertera
return new Conversions.DecimalConversion().toBytes(scaledValue, fieldSchema, fieldSchema.getLogicalType());
}
public static void main(String[] args) throws IOException {
// 1. Wczytaj schemat
Schema schema = new Schema.Parser().parse(new File("src/main/resources/avro/product.avsc"));
// 2. Utwórz obiekt GenericRecord
GenericRecord productRecord = new GenericData.Record(schema);
productRecord.put("name", "Tablet Pro");
// 3. Przygotuj BigDecimal i skonwertuj go za pomocą Twojej metody
BigDecimal price = new BigDecimal("999.99");
Schema priceSchema = schema.getField("price").schema(); // Pobierz schemat dla pola 'price'
ByteBuffer priceBytes = bigDecimalToByteBuffer(priceSchema, price);
// 4. Umieść skonwertowany ByteBuffer w rekordzie
productRecord.put("price", priceBytes);
// 5. Zapisz do pliku
DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(schema);
try (DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(schema, new File("products_generic.avro"));
dataFileWriter.append(productRecord);
System.out.println("Zapisano generyczny rekord do pliku.");
System.out.println("Rekord: " + productRecord);
}
}
}
Odczyt (Deserializacja) do GenericRecord
Tutaj dzieje się "magia" typów logicznych. Podczas odczytu biblioteka Avro automatycznie wykryje typ logiczny decimal i sama skonwertuje ByteBuffer z powrotem na BigDecimal. Nie potrzebujesz metody byteBufferToBigDecimal.
import org.apache.avro.file.DataFileReader;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumReader;
import java.io.File;
import java.io.IOException;
public class AvroGenericReadExample {
public static void main(String[] args) throws IOException {
// 1. Skonfiguruj odczyt generyczny
DatumReader<GenericRecord> datumReader = new GenericDatumReader<>();
try (DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(new File("products_generic.avro"), datumReader)) {
// 2. Odczytaj rekord
GenericRecord productRecord = dataFileReader.next();
System.out.println("Odczytano generyczny rekord: " + productRecord);
// 3. Pobierz pole 'price'
// Avro automatycznie konwertuje ByteBuffer -> BigDecimal!
Object priceObject = productRecord.get("price");
System.out.println("Typ odczytanej ceny: " + priceObject.getClass().getName()); // Będzie to java.math.BigDecimal
java.math.BigDecimal price = (java.math.BigDecimal) priceObject;
System.out.println("Wartość ceny: " + price);
}
}
}
Podsumowanie i Best Practices dla Twojego przypadku
- Wybór API: Świadomie zdecyduj, którego API używasz.
SpecificRecord(generowane klasy): Zalecane dla większości aplikacji, gdzie model danych jest stabilny. Prostsze i bezpieczniejsze typowanie.GenericRecord(Twój przypadek): Idealne dla narzędzi do przetwarzania danych, dynamicznych schematów, gdzie elastyczność jest ważniejsza niż bezpieczeństwo typów na etapie kompilacji.
-
Ręczna konwersja przy zapisie: Jeśli używasz
GenericRecord, musisz ręcznie konwertowaćBigDecimalnaByteBufferprzed umieszczeniem go w rekordzie. Twoja metoda jest do tego idealna. -
Automatyczna konwersja przy odczycie: Dobra wiadomość jest taka, że przy odczycie
GenericRecordbiblioteka Avro sama zajmie się konwersją w drugą stronę (zByteBufferdoBigDecimal), o ile typ logiczny jest obecny w schemacie. -
Ustawianie skali: Zawsze przed konwersją upewnij się, że Twój
BigDecimalma skalę zgodną ze schematem (yourDecimal.setScale(schemaScale, ...)). To zapobiega błędom i gwarantuje spójność danych.
Twój kod jest więc kluczowym elementem układanki przy pracy z BigDecimal w generycznym API Avro.