如果我使用模式版本1序列化对象,稍后将模式更新为版本2(比如通过添加字段) - 我是否需要在稍后反序列化对象时使用模式版本1?理想情况下,我只想使用模式版本2,并且反序列化对象具有在最初序列化对象后添加到模式的字段的默认值。
也许有些代码会更好地解释......
schema1:
{"type": "record",
"name": "User",
"fields": [
{"name": "firstName", "type": "string"}
]}
SCHEMA2:
{"type": "record",
"name": "User",
"fields": [
{"name": "firstName", "type": "string"},
{"name": "lastName", "type": "string", "default": ""}
]}
使用通用的非代码生成方法:
// serialize
ByteArrayOutputStream out = new ByteArrayOutputStream();
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
GenericDatumWriter writer = new GenericDatumWriter(schema1);
GenericRecord datum = new GenericData.Record(schema1);
datum.put("firstName", "Jack");
writer.write(datum, encoder);
encoder.flush();
out.close();
byte[] bytes = out.toByteArray();
// deserialize
// I would like to not have any reference to schema1 below here
DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>(schema2);
Decoder decoder = DecoderFactory.get().binaryDecoder(bytes, null);
GenericRecord result = reader.read(null, decoder);
导致EOFException。使用jsonEncoder
会导致AvroTypeException。
我知道如果我将schema1和schema2都传递给GenericDatumReader
构造函数,它会起作用,但是我不想保留所有以前模式的存储库,并且还要以某种方式跟踪使用的模式序列化每个特定对象。
我还尝试了代码生成方法,首先使用schema1生成的User类序列化到文件中:
User user = new User();
user.setFirstName("Jack");
DatumWriter<User> writer = new SpecificDatumWriter<User>(User.class);
FileOutputStream out = new FileOutputStream("user.avro");
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
writer.write(user, encoder);
encoder.flush();
out.close();
然后将架构更新为版本2,重新生成User类,并尝试读取文件:
DatumReader<User> reader = new SpecificDatumReader<User>(User.class);
FileInputStream in = new FileInputStream("user.avro");
Decoder decoder = DecoderFactory.get().binaryDecoder(in, null);
User user = reader.read(null, decoder);
但它也会导致EOFException。
为了比较,我正在尝试做的似乎与protobufs一起工作......
格式:的
option java_outer_classname = "UserProto";
message User {
optional string first_name = 1;
}
序列化:
UserProto.User.Builder user = UserProto.User.newBuilder();
user.setFirstName("Jack");
FileOutputStream out = new FileOutputStream("user.data");
user.build().writeTo(out);
添加可选的last_name以格式化,重新生成UserProto和反序列化:
FileInputStream in = new FileInputStream("user.data");
UserProto.User user = UserProto.User.parseFrom(in);
如预期的那样,user.getLastName()
是空字符串。
这样的事情可以用Avro完成吗?
答案 0 :(得分:31)
Avro和Protocol Buffers有不同的方法来处理版本控制,哪种方法更好取决于您的用例。
在协议缓冲区中,您必须使用数字显式标记每个字段,并将这些数字与字段的值一起存储在二进制表示中。因此,只要您在后续架构版本中永远不更改数字的含义,您仍然可以解码以不同架构版本编码的记录。如果解码器看到一个它无法识别的标签号,它就可以跳过它。
Avro采用了不同的方法:没有标签号,而二进制布局完全由执行编码的程序决定 - 这是编写器的架构。 (记录的字段只是在二进制编码中一个接一个地存储,没有任何标记或分隔符,并且顺序由编写者的模式决定。)这使得编码更紧凑,并且使您不必手动维护标记。架构。但它确实意味着,对于阅读,您必须知道数据写入的确切模式,否则您将无法理解它。
如果知道编写器的模式对于解码Avro至关重要,那么读者的模式就是它的一层好处。如果您在需要读取Avro数据的程序中进行代码生成,则可以从读取器的模式中执行codegen,这样您就不必在每次编写器的模式更改时重新生成它(假设它以可以的方式更改)得到解决)。但它并不能让你不必了解作者的架构。
Avro的方法在您拥有大量已知具有完全相同的架构版本的记录的环境中是好的,因为您可以只在文件开头的元数据中包含架构,并且知道下一个百万所有记录都可以使用该模式进行解码。这在MapReduce上下文中发生了很多,这解释了为什么Avro退出了Hadoop项目。
协议缓冲区的方法可能更适合RPC,其中单个对象通过网络发送(作为请求参数或返回值)。如果您在此处使用Avro,则可能有不同的客户端和不同的服务器都具有不同的架构版本,因此您必须使用它正在使用的Avro架构版本标记每个二进制编码的blob,并维护架构的注册表。此时您可能还使用了Protocol Buffers的内置标记。
答案 1 :(得分:2)
要执行您要执行的操作,您需要通过允许空值使last_name字段成为可选字段。 last_name的类型应为[“null”,“string”]而不是“string”
答案 2 :(得分:0)
我试图绕过这个问题。我把它放在这里:
我还尝试使用两个模式,一个模式只是使用Avro的refection API将另一个列添加到另一个模式。我有以下架构:
Employee (having name, age, ssn)
ExtendedEmployee (extending Employee and having gender column)
我假设现在早先有Employee
个对象的文件也有ExtendedEmployee
对象,我试图将该文件读作:
RecordHandler rh = new RecordHandler();
if (rh.readObject(employeeSchema, dbLocation) instanceof Employee) {
Employee e = (Employee) rh.readObject(employeeSchema, dbLocation);
System.out.print(e.toString());
} else if (rh.readObject(schema, dbLocation) instanceof ExtendedEmployee) {
ExtendedEmployee e = (ExtendedEmployee) rh.readObject(schema, dbLocation);
System.out.print(e.toString());
}
这解决了这里的问题。但是,我很想知道是否有一个API,我们可以让ExtendedEmployee
架构读取Employee
的对象。