TechDogy

(paduvi)

You can do anything, but not everything..

Backward and Forward compatibility with Java Jackson ObjectMapper

Categories:

Đối với các chương trình nằm ở phía server-side, chúng ta sẽ gặp phải vấn đề downtime khi cần triển khai upgrade code mới. Để giải quyết vấn đề này, ta thường nhân rộng service thành nhiều replica (>= 3) và sử dụng kỹ thuật rolling upgrade: từ từ triển khai upgrade trên từng replica trước; kiểm tra nó có hoạt động trơn tru, có bị lỗi gì không; rồi từ từ tiến hành tiếp trên các replica còn lại.

Điều này dẫn tới phiên bản code cũ và mới, định dạng (format) dữ liệu cũ và mới tồn tại song song trong hệ thống cùng một lúc. Để đảm bảo cho service chạy trơn tru trên cả phiên bản cũ và mới, ta cần đảm bảo sự tương thích (compatibility) trên cả 2 chiều:

  • Backward compatibility: code mới có thể đọc được dữ liệu viết bởi code cũ.
  • Forward compatibility: code cũ có thể đọc được dữ liệu viết bởi code mới.

Backward compatibility thường không khó: biết được format của code cũ là gì, ta hoàn toàn có thể xử lý được nó (hoặc nếu đường cùng quá thì ta cứ giữ lại đoạn code đang đọc dữ liệu cũ, đừng xóa nó đi). Tuy nhiên, Forward compatibility sẽ phức tạp hơn, bởi vì nó đòi hỏi ta phải phòng ngừa trước những thứ sẽ được thêm bởi code mới.

Thay đổi data type của field

Ta cần tránh đổi data type, vì việc này sẽ dẫn tới không thỏa mãn cả 2 chiều compatibility: code mới không đọc được định dạng cũ, và code cũ cũng không đọc được định dạng mới.

Xóa field

Ta cũng cần tránh xóa field, vì điều này có thể làm cho code cũ bị không tương thích. Trước khi quyết định xóa field, hãy đảm bảo rằng là field đó đã được khai báo giá trị mặc định (default value) ở code cũ.

Thêm field

Code mới ghi 1 giá trị nào đó vào DB, và sau đấy bản ghi đó lại được đọc ra từ replica vẫn đang chạy code cũ. Dữ liệu được load vào object model trong bộ nhớ, rồi cập nhật giá trị và ghi lại vào DB. Trong quá trình đó, field được thêm của code mới sẽ bị mất.

Trong trường hợp này, giải pháp đó là code cũ phải luôn giữ các field mới nguyên vẹn, mặc dù ta không dùng tới nó. Tùy vào từng ngôn ngữ lập trình và thư viện khác nhau, sẽ có những cách xử lý khác nhau để thực hiện giải pháp đó. Ở phần tiếp theo, ta sẽ minh họa với Java và Jackson ObjectMapper.

Dependency

Thêm dòng sau vào trong file pom.xml:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.11.3</version>
</dependency>

Tìm các phiên bản mới nhất của thư viện jackson-databind tại đây.

Chương trình minh họa:

Ta sẽ tạo 2 model riêng: 1 cái tượng trưng cho model cũ, còn model mới sẽ extends từ cái cũ

class OldUser {
    private String userName;
    private Long favoriteNumber;
    private List<String> interests;

    // getters and setters...
}

class User extends OldUser {
    private String photoURL;
    private String foo;

    // getters and setters...
}

Chương trình sẽ tái hiện lại quá trình được minh họa bên trên:

public static void main(String[] args) throws IOException {
    final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    User user = new User();
    user.setUserName("Martin");
    user.setFavoriteNumber(1337L);
    user.setInterests(Collections.singletonList("hacking"));
    user.setPhotoURL("http://...");

    // Data written by new version of code
    String newVersionJSON = OBJECT_MAPPER.writeValueAsString(user);
    System.out.println("before: " + newVersionJSON);

    // Read and decode into old model object
    OldUser oldUser = OBJECT_MAPPER.readValue(newVersionJSON, OldUser.class);
    oldUser.setFavoriteNumber(42L);     // Update

    // Re-encode and Write back
    System.out.println("after: " + OBJECT_MAPPER.writeValueAsString(oldUser));
}

Kết quả – giá trị photoURL đã bị mất:

before: {"userName":"Martin","favoriteNumber":1337,"interests":["hacking"],"photoURL":"http://…"}
after: {"userName":"Martin","favoriteNumber":42,"interests":["hacking"]}

Giải pháp xử lý:

Để giữ nguyên vẹn các field mới mà không xóa nó đi, ta sẽ giữ nó trong 1 field riêng tên là others:

class OldUser {

    ...

    @JsonIgnore
    private final Map<String, Object> others = new HashMap<>();

    @JsonAnyGetter
    private Map<String, Object> any() {
        return others;
    }

    @JsonAnySetter
    private void set(String name, Object value) {
        others.put(name, value);
    }
}

Giờ thì ta chạy thử lại chương trình nhé:

before: {"userName":"Martin","favoriteNumber":1337,"interests":["hacking"],"photoURL":"http://…"}
after: {"userName":"Martin","favoriteNumber":42,"interests":["hacking"],"photoURL":"http://…"}

Latest Comments:

  1. Mình đi search về storage engine thì thấy series về bài của bạn (mình đoán là bạn cũng đọc từ…

  2. Series rất hay, ủng hộ admin làm thêm về các database khác như Scylladb (discord mới migrate từ Cassandra sang)

  3. bài viết rất chất lượng, ủng hộ mạnh tác giả

  4. Mình đang làm về authentication thì phải tìm hiểu thêm về JWE (Json Web Encryption) và JWS (Json Web Signature).…

Leave a Reply

Your email address will not be published. Required fields are marked *