CodeGym /Kurslar /JAVA 25 SELF /Serializasiya olunmuş məlumatların miqrasiyası və versiya...

Serializasiya olunmuş məlumatların miqrasiyası və versiyalaşdırılması

JAVA 25 SELF
Səviyyə, Dərs
Mövcuddur

1. Problem: serializasiya olunan sinif dəyişdirildikdə nə baş verir?

Real layihələrdə obyektlər tez-tez serializasiya olunur — fayllara, verilənlər bazasına və ya keşə yazılır ki, sonra bərpa edilsin. Amma sinifdə sahələr əlavə edib-çıxarsanız və ya tipləri dəyişsəniz, halbuki production mühitində artıq köhnə serializasiya olunmuş obyektlər saxlanılırsa, nə baş verəcək?

Məsələn, production-da User sinfinin saxlanmış obyektləri olan bir fayl var. Siz tətbiqin yeni versiyasını buraxırsınız, burada User-ə yeni sahə əlavə olunub və ya mövcud sahənin tipi dəyişdirilib. Proqram köhnə məlumatları deserializasiya etməyə cəhd etdikdə, çox vaxt InvalidClassException kimi səhvlə və ya məlumat itkisi ilə nəticələnir, çünki obyektin strukturu artıq JVM-in gözləntilərinə uyğun gəlmir.

Məhz buna görə siniflərin versiyaları və serializasiya olunmuş məlumatlar arasında uyğunluğu öncədən planlaşdırmaq vacibdir. Production mühitində köhnə faylları sadəcə “silə” bilməzsiniz — ya geriyə uyğunluğu təmin etməli, ya da məlumatların miqrasiyasını həyata keçirməlisiniz ki, sinfin yeni versiyaları artıq saxlanılmış obyektlərlə düzgün işləsin.

2. serialVersionUID ilə həll

serialVersionUID nədir?

Bu, serializasiya olunan sinifin “versiyasını” müəyyən edən xüsusi sahədir.

private static final long serialVersionUID = 1L;
  • Əgər sahə göstərilməyibsə, Java onu sinfin strukturuna əsasən avtomatik hesablayır.
  • Deserializasiya zamanı sinifdəki və serializasiya olunmuş məlumatlardakı serialVersionUID müqayisə olunur.
  • Uyğun gəlmirsə — InvalidClassException atılır.

Avtomatik generasiya və əl ilə idarəetmə

Avtomatik: əgər açıq göstərməsəniz, kompilyator dəyəri sinfin strukturuna (ad, sahələr, metodlar və s.) əsasən özü hesablayacaq.

Əl ilə idarəetmə: uyğunluğa nəzarət etmək üçün serializasiya olunan siniflərdə serialVersionUID-u həmişə açıq şəkildə göstərmək tövsiyə olunur.

Nümunə:

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ...
}

Nə zaman dəyişmək, nə zaman əvvəlki kimi saxlamaq?

  • Eyni saxlamaq: dəyişikliklər uyğunluğu pozmursa (məsələn, susmaya görə ilkinləşdirilə bilən yeni sahə əlavə etmisiniz).
  • Dəyişmək: sahəni sildinizsə, sahənin tipini dəyişmisinizsə, sinif iyerarxiyasını dəyişmisinizsə və ya başqa uyğunsuz dəyişikliklər etmisinizsə.

Qayda:

- Yeni sinif versiyasının köhnə serializasiya olunmuş obyektləri oxuya bilməsini istəyirsinizsə — serialVersionUID-u dəyişməyin.
- Uyğunsuzluq kritikdirsə (səhv almaq “əyri” məlumatlardan daha yaxşıdır) — serialVersionUID-u artırın.

3. Məlumat miqrasiyası strategiyaları

Rahat yanaşmalardan biri — “tənbəl” miqrasiya. Məqsəd ondan ibarətdir ki, bütün köhnə məlumatları dərhal çevirmirsiniz, bunu obyekt ilk dəfə oxunanda tədricən edirsiniz.

Məsələn, yeni sahə əlavə etmisinizsə, köhnə obyekt deserializasiya olunanda o, tipindən asılı olaraq susmaya görə 0, null və ya false dəyərini alacaq. Sahəni sildinizsə, deserializasiya onu sadəcə nəzərə almır. Bu zaman JVM sahələri ad və tip üzrə özü uyğunlaşdırır, buna görə də bir çox dəyişikliklər “öz-özünə” işləyir.

Sahənin tipini dəyişmək daha çətindir, məsələn əvvəllər int olub, indi String olub. Standart deserializasiya artıq bunun öhdəsindən gəlməyəcək. Həll — çevrilməni əl ilə işləyən readObject metodunu reallaşdırmaqdır:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField fields = in.readFields();
    // Köhnə sahə: int age
    int age = fields.get("age", -1);
    // Yeni sahə: String ageStr
    this.ageStr = String.valueOf(age);
}

Beləliklə, köhnə obyektlər ilk oxunma anında sinfin yeni versiyasına düzgün şəkildə adaptasiya olunur.

Şablon “yerində çevirmə” (in-place conversion)

Bu yanaşma “tənbəl” miqrasiyadan fərqli olaraq bütün məlumatları dərhal çevirir. İdeya sadədir: hər bir serializasiya olunmuş obyektə — fayllarda və ya verilənlər bazasında — keçirsiniz, onu sinfin köhnə versiyası ilə oxuyursunuz, sinfin yeni versiyasından obyekt yaradırsınız və yenilənmiş formatda geri yazırsınız.

Bu üsul, “tənbəl” miqrasiyaya güvənmək mümkün olmadıqda rahatdır. Məsələn, böyük həcmli məlumatlarda və ya obyektlərə nadir hallarda müraciət olunduqda, elə etmək lazımdır ki, onların hamısı tətbiqin yeni versiyası ilə işləməyə hazır olsun. Praktikada bunu tez-tez ayrıca skript və ya utilit vasitəsilə edirlər. Məsələn, proses belə görünə bilər:

// Sadə in-place çevirmə nümunəsi
List<File> files = getSerializedFiles(); // köhnə obyektləri olan faylların siyahısı
for (File file : files) {
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
        OldUser oldUser = (OldUser) ois.readObject(); // köhnə obyekti oxuyuruq
        NewUser newUser = new NewUser(oldUser); // köhnə obyekt əsasında yeni obyekt yaradırıq
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
            oos.writeObject(newUser); // faylı yeni versiya ilə yenidən yazırıq
        }
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

Beləcə bütün obyektlər dərhal yeni versiyaya gətirilir və production mühitində istifadə üçün təhlükəsiz olur.

4. Köhnəlmiş versiyalarla iş: advanced fəndlər

ObjectInputStream.readClassDescriptor() və readFields()

  • readClassDescriptor() — sinifin meta-məlumatlarının oxunmasını tutmağa və lazım gələrsə onları əvəz etməyə imkan verir, yəni serializasiyanı “aldatmaq” mümkün olur.
  • readFields() — sinfin strukturu dəyişsə belə sahələri ada görə oxumağa imkan verir.

Nümunə:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField fields = in.readFields();
    String name = (String) fields.get("name", "unknown");
    int age = fields.defaulted("age") ? 0 : fields.get("age", 0);
    // ... yeni sahələrin ilkinləşdirilməsi
}

5. Təcrübə: sinifin iki versiyası, serializasiya və miqrasiya

Addım 1. Sinifin köhnə versiyası

// OldUser.java
import java.io.Serializable;

public class OldUser implements Serializable {
    private static final long serialVersionUID = 1L;
    public String name;
    public int age;

    public OldUser(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Addım 2. Köhnə versiyanın obyektini serializasiya edirik

OldUser user = new OldUser("Vasya", 30);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
    out.writeObject(user);
}

Addım 3. Sinifin yeni versiyası (email sahəsi əlavə olunub, age-in tipi dəyişdirilib)

// User.java
import java.io.*;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    public String name;
    public String age; // tip dəyişdirilib!
    public String email; // yeni sahə

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = in.readFields();
        this.name = (String) fields.get("name", "unknown");
        // Köhnə age sahəsini (int) sətirə çeviririk
        if (!fields.defaulted("age")) {
            int oldAge = fields.get("age", 0);
            this.age = String.valueOf(oldAge);
        } else {
            this.age = "unknown";
        }
        // Yeni email sahəsi — susmaya görə null
        this.email = (String) fields.get("email", null);
    }
}

Addım 4. Köhnə obyektin yeni siniflə deserializasiyası

try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.dat"))) {
    User user = (User) in.readObject();
    System.out.println(user.name + ", " + user.age + ", " + user.email);
}

Nəticə:

  • Köhnə age sahəsi sətirə çevrildi.
  • Yeni email sahəsi — null.
  • InvalidClassException xətası yoxdur, çünki serialVersionUID eynidir və tip uyğunsuzluğunu əl ilə işlədik.

Uyğunsuzluğu işləməsək nə baş verəcək?

Sahənin tipini sadəcə dəyişib readObject reallaşdırmasanız, deserializasiya zamanı belə bir səhv alacaqsınız:

java.io.InvalidClassException: User; incompatible types for field age

6. Serializasiya olunmuş məlumatların miqrasiyasında tipik səhvlər

Xəta №1: serialVersionUID göstərilməyib — sinifdə cüzi dəyişikliklərdə belə, hətta əhəmiyyətsiz dəyişikliklərdə də InvalidClassException alırsınız.

Xəta №2: readObject daxilində işləmədən sahənin tipini dəyişmisiniz — tip uyğunsuzluğu xətası alacaqsınız.

Xəta №3: Sahəni sildiniz, lakin köhnə məlumatlar onu hələ də saxlayır — Java bu sahəni sadəcə görməzlikdən gələcək, amma o kritik idisə, məlumatlar itəcək.

Xəta №4: Test etmədən bütün məlumatları əl ilə miqrasiya etməyə çalışdınız — məlumatların bir hissəsini itirə və ya inkonsistent obyektlər əldə edə bilərsiniz.

Xəta №5: Obyektin serializasiyası/deserializasiyası aparılan bütün yerləri yeniləmədiniz — kodun bir hissəsi yeni versiya ilə, bir hissəsi köhnə ilə işləyir, nəticədə “xəyali” xətalar yaranır.

Xəta №6: Böyük həcmli məlumatlar üçün miqrasiya strategiyası nəzərdə tutulmayıb — “tənbəl” miqrasiyada istifadəçilər köhnəlmiş məlumatlara ilk girişdə gözlənilməz xətalarla üzləşə bilər.

Xəta №7: Miqrasiyadan əvvəl ehtiyat nüsxə yaratmadınız — yeniləmədən əvvəl serializasiya olunmuş məlumatların mütləq ehtiyat nüsxəsini yaradın!

1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Heyvan uçotu sistemi: köhnə məlumatlar itirilmədən rəng əlavə etmək
Heyvan uçotu sistemi: köhnə məlumatlar itirilmədən rəng əlavə etmək
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
İstifadəçi profillərinin yenilənməsi: ağıllı yaş miqrasiyası
İstifadəçi profillərinin yenilənməsi: ağıllı yaş miqrasiyası
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION