File I/O in Apps (App တွေထဲမှာ file ဖတ်ခြင်း၊ ရေးခြင်း)

Syllabus ထဲက file processing ကို အခြေခံထားတဲ့ IA အတွက် tool ပါ: text file (စာသားပဲ ပါတဲ့ file) တွေကို ဖတ်တာနဲ့ ရေးတာက IB syllabus ထဲမှာ ပါပါတယ် (B2.5.1၊ Reading Files (file ထဲက ဒေတာကို ဖတ်ခြင်း) နဲ့ Writing Files (file ထဲကို ဒေတာ ရေးခြင်း) ကို ကြည့်ပါ)။ ဒီစာမျက်နှာက အဲဒီ အယူအဆကို window ပါတဲ့ app တစ်ခုထဲမှာ သုံးပါတယ် (Java ဥပမာတွေမှာ JavaFX၊ Python ဥပမာတွေမှာ tkinter ကို သုံးပါတယ်၊ နှစ်ခုလုံးက desktop app တည်ဆောက်တဲ့ library တွေပါ)၊ ဒါမှ user ထည့်ထားတဲ့ ဒေတာက နောက်တစ်ကြိမ် app ကို ဖွင့်တဲ့အခါ ရှိနေဦးမှာပါ။ IA (IB သင်တန်းမှာ ကျောင်းသားတစ်ယောက်ချင်း ကိုယ်တိုင် တည်ဆောက်ရပြီး ကျောင်းက ဆရာက အမှတ်ပေးတဲ့ အပိုင်း) အတွက် IB က file ဒါမှမဟုတ် တခြား ဒေတာ သိမ်းနည်း တစ်ခုခု သုံးဖို့ မတောင်းဆိုပါဘူး။ ကိုယ့် product (program ဒါမှမဟုတ် app) က run တစ်ကြိမ်နဲ့ တစ်ကြိမ်ကြား ဒေတာကို သိမ်းထားရင် ဘယ်လို သိမ်းထားလဲ ဆိုတာက Criterion D (Development) မှာ ရှင်းပြနိုင်တဲ့ ရွေးချယ်မှု တစ်ခုပါ။ Criterion D ဆိုတာ IA ကို အမှတ်ပေးတဲ့ စံနှုန်းတွေထဲက product တည်ဆောက်ပုံကို အမှတ်ပေးတဲ့ စံနှုန်းပါ။


ခြုံငုံ မိတ်ဆက်

IA product တော်တော်များများက program ပိတ်သွားပြီးနောက်မှာလည်း သူတို့ ဒေတာကို ဆက်ထားဖို့ လိုပါတယ်၊ ဒါမှ user က app ကို ပြန်ဖွင့်တဲ့အခါ ဒေတာ ရှိနေမှာပါ။ ဒါကို များသောအားဖြင့် persistence (program ပိတ်သွားပြီးနောက်မှာလည်း ဒေတာ ဆက်ရှိနေအောင် သိမ်းထားခြင်း) လို့ ခေါ်ပါတယ်။ ရိုးရှင်းတဲ့ နည်းတစ်ခုက CSV (comma နဲ့ ခြားထားတဲ့ စာသား file) ပုံစံနဲ့ text file တစ်ခုပါ၊ ဒီစာမျက်နှာမှာ အဲဒီနည်းကို သုံးပါတယ်။ နောက်တစ်နည်းက database (ဒေတာ သိမ်းဆည်းတဲ့ စနစ်) အသေး တစ်ခုပါ: JavaFX Cookbook ရဲ့ Step up to a database when a list outgrows a file recipe (ချက်ချင်း ယူသုံးလို့ရတဲ့ code နမူနာ) မှာ ဥပမာ တစ်ခု ပြထားပါတယ်။

ဒီစာမျက်နှာက MVC Architecture (MVC ဖွဲ့စည်းပုံ) နဲ့ FXML and Controllers (FXML နဲ့ controller များ) ထဲက Student Manager ကို ဆက်တည်ဆောက်ပါတယ် (Python မှာဆိုရင် MVC with tkinter (tkinter နဲ့ MVC) ထဲက Student Manager ပါ)။ ဒီအကြောင်းအရာတွေ ပါပါတယ်:

  • Object (class တစ်ခုကနေ ဖန်တီးထားတဲ့ instance) တွေကို CSV file ထဲ ရေးခြင်း
  • CSV file ထဲကနေ object တွေကို ပြန်ဖတ်ခြင်း
  • MVC (app ကို Model၊ View၊ Controller ဆိုပြီး အပိုင်းသုံးပိုင်း ခွဲရေးတဲ့ ပုံစံ) application တစ်ခုထဲကို file I/O (file ဖတ်ခြင်း၊ ရေးခြင်း) ထည့်ခြင်း
  • File နဲ့ အလုပ်လုပ်နေစဉ် ဖြစ်တဲ့ error တွေကို ကိုင်တွယ်ခြင်း

အဓိက အယူအဆများ

CSV Format (comma နဲ့ ခြားထားတဲ့ စာသား ပုံစံ)

CSV (comma-separated values) က record (အချက်အလက် အများကြီး စုထားတဲ့ ဒေတာ တစ်ခု) တစ်ခုကို line တစ်ကြောင်းမှာ သိမ်းပြီး record ရဲ့ field (record တစ်ခုထဲက အချက်အလက် တစ်ကွက်) တွေကို comma နဲ့ ခြားထားတဲ့ ရိုးရိုး စာသား ပုံစံ တစ်ခုပါ။ ကိုယ့် app က ဘာတွေ save လုပ်ထားလဲ စစ်ကြည့်ဖို့ CSV file ကို text editor (စာရိုက်ပြီး ပြင်တဲ့ program) ဒါမှမဟုတ် spreadsheet (Excel လို ဇယားကွက် program) နဲ့ ဖွင့်ကြည့်နိုင်ပါတယ်။

Alice,85
Bob,92
Charlie,78

Line တစ်ကြောင်းစီမှာ Student object တစ်ခုစီ ပါပါတယ်: ပထမ field က နာမည်၊ ဒုတိယ field က grade (အမှတ်) ပါ။ Object တစ်ခုစီကို ပြန်တည်ဆောက်ဖို့ code က line ကို comma နေရာမှာ ခွဲပါတယ်။

try-with-resources (file ကို အလိုအလျောက် ပိတ်ပေးတဲ့ try ပုံစံ)

Java ရဲ့ try-with-resources ပုံစံက try နောက်က ကွင်းထဲမှာ reader ဒါမှမဟုတ် writer ကို ဖွင့်ပြီး block ပြီးဆုံးတဲ့အခါ ပိတ်ပေးပါတယ်၊ block ထဲမှာ exception (program run နေစဉ် ဖြစ်တဲ့ အမှား) throw (exception ဖြစ်ကြောင်း အချက်ပြ) ဖြစ်ခဲ့ရင်တောင် ပိတ်ပေးပါတယ်။ Writer ကို ပိတ်လိုက်တာက သူ့ရဲ့ buffer (file ထဲ မရေးရသေးတဲ့ ဒေတာကို ခဏ သိမ်းထားတဲ့ memory နေရာ) ထဲမှာ စောင့်နေတဲ့ စာသားတွေကိုလည်း file ထဲ ရေးထုတ်ပေးပါတယ်။ ဘယ်တော့မှ မပိတ်တဲ့ writer က file ကို မပြည့်စုံဘဲ ဒါမှမဟုတ် ဗလာ ဖြစ်နေအောင် ချန်ထားခဲ့နိုင်ပါတယ်။

// Inside a method that declares throws IOException
try (BufferedWriter writer = new BufferedWriter(new FileWriter("data.csv"))) {
    writer.write("Alice,85");
    writer.newLine();
}  // writer is automatically closed here
# Python closes the file in a finally block, which runs even if the write fails
f = open("data.csv", "w", encoding="utf-8")
try:
    f.write("Alice,85\n")
finally:
    f.close()

ဒီ block မှာ catch မပါလို့ IOException (file ဖတ်တာ၊ ရေးတာ မအောင်မြင်တဲ့အခါ ဖြစ်တဲ့ exception) ဖြစ်ရင် method (သီးခြား အလုပ်တစ်ခု လုပ်ပေးတဲ့ code အပိုင်း) ကို ခေါ်တဲ့ code ဆီ ဆက်ပို့ပါတယ်၊ ဒါကြောင့် method က throws IOException လို့ ကြေညာထားရတာပါ။ Reading Files မှာ ဖတ်ဖို့အတွက် ဒီပုံစံကိုပဲ ပြထားပါတယ်။

Python မှာ try/finally က ဒီအလုပ်ကို လုပ်ပါတယ်: ရေးတာ အောင်မြင်သည်ဖြစ်စေ မအောင်မြင်သည်ဖြစ်စေ finally block က file ကို ပိတ်ပေးပြီး error (OSError) ဖြစ်ရင် ခေါ်တဲ့ code ဆီ ဆက်ပို့ပါတယ်။ Python မှာ throws clause မရှိပါဘူး။ Tutorial (အဆင့်ဆင့် သင်ပြတဲ့ လမ်းညွှန်) တွေမှာ with open("data.csv", "w") as f: ကို မကြာခဏ သုံးကြပါတယ်၊ ဒါကလည်း သူ့ block ပြီးဆုံးတဲ့အခါ file ကို ပိတ်ပေးပါတယ်။

ဖွင့်ထားတဲ့ file တိုင်းကို ပိတ်ပါ။ try-with-resources ကို သုံးပါ၊ ဒါမှမဟုတ် finally block ထဲမှာ close() ကို ကိုယ်တိုင် ခေါ်ပါ။ Writer ကို မပိတ်ရင် buffer ထဲမှာ ကျန်နေတဲ့ စာသားတွေက file ထဲ ရောက်မသွားဘဲ ကျန်ခဲ့နိုင်ပါတယ်။


File ထဲကို save လုပ်ခြင်း

ArrayList (ကြီးလာ၊ သေးသွားနိုင်တဲ့ list) ကို CSV file ထဲ ရေးခြင်း

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

public class StudentFileHandler {
    private static final String FILE_PATH = "students.csv";

    public static void save(ArrayList<Student> students) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_PATH))) {
            for (int i = 0; i < students.size(); i++) {
                Student s = students.get(i);
                writer.write(s.getName() + "," + s.getGrade());
                writer.newLine();
            }
        }
    }
}
# file_handler.py: in Python, the file handler is a file of functions
FILE_PATH = "students.csv"


def save(students):
    f = open(FILE_PATH, "w", encoding="utf-8")
    try:
        for i in range(len(students)):
            s = students[i]
            f.write(s.get_name() + "," + str(s.get_grade()) + "\n")
    finally:
        f.close()

အဆင့်ဆင့် ဖြစ်ပုံ:

  1. new FileWriter(FILE_PATH) က file ကို ရေးဖို့ ဖွင့်ပါတယ် (FileWriter က file ထဲ စာရေးတဲ့ Java class ပါ)။ File မရှိရင် အသစ် ဖန်တီးပြီး ရှိရင် ထဲက content တွေကို ရှင်းပစ်ပါတယ်။ မရှိတဲ့ folder ကိုတော့ မဖန်တီးပေးပါဘူး: data folder မရှိဘဲ data/students.csv လို path (file ရှိရာ လမ်းကြောင်း) ကို ပေးရင် FileNotFoundException (file ကို ရှာမတွေ့လို့ ဖြစ်တဲ့ exception) ကို throw လုပ်ပါတယ်။
  2. BufferedWriter (file ထဲ ထိရောက်စွာ ရေးတဲ့ Java class) က FileWriter ကို အပြင်ကနေ ထုပ်ထားပြီး စာသားတွေကို စုကာ အပိုင်း ပိုကြီးကြီးနဲ့ file ထဲ ရေးပါတယ်၊ ဒါက ပိုမြန်ပါတယ်
  3. Loop (ထပ်ခါထပ်ခါ run တဲ့ code အပိုင်း) က ကျောင်းသား တစ်ယောက်စီကို CSV line တစ်ကြောင်းအဖြစ် ရေးပါတယ်: နာမည်၊ comma နဲ့ grade ပါ
  4. writer.newLine() က record တစ်ခုစီရဲ့ နောက်မှာ line ခွဲပေးပါတယ်
  5. try block ပြီးဆုံးတဲ့အခါ writer ကို ပိတ်ပြီး ကျန်နေတဲ့ နောက်ဆုံး စာသားတွေကို file ထဲ ရေးထုတ်ပါတယ်

Python မှာ open(FILE_PATH, "w", encoding="utf-8") က အဆင့် 1 နဲ့ 2 ကို လုပ်ပါတယ်: file ကို ဖန်တီးတာ ဒါမှမဟုတ် ရှင်းတာ လုပ်ပြီး စာသားတွေကို buffer ထဲ စုထားကာ folder မရှိရင် FileNotFoundError ကို ပေးပါတယ်။ "\n" က line တစ်ကြောင်းစီကို အဆုံးသတ်ပြီး finally block ထဲက close() က နောက်ဆုံး စာသားတွေကို file ထဲ ရေးထုတ်ပါတယ်။ တခြား file တွေက import file_handler လုပ်ပြီးနောက် ဒီ function (သီးခြား အလုပ်တစ်ခု လုပ်ပေးတဲ့ code အပိုင်း၊ Java မှာ method လို့ ခေါ်ပါတယ်) တွေကို file_handler.save(...) လို့ ခေါ်သုံးပါတယ်။ encoding="utf-8" က ဘယ်ကွန်ပျူတာမှာမဆို Python က file ကို text encoding (စာလုံးတွေကို byte တွေအဖြစ် သိမ်းတဲ့ စနစ်) တစ်မျိုးတည်းနဲ့ ရေးပြီး ဖတ်အောင် လုပ်ပေးပါတယ်။ ဒါ မပါရင် encoding က system ပေါ် မူတည်သွားပြီး Zoë လို နာမည်က ကွန်ပျူတာ တစ်လုံးမှာ error ဖြစ်နိုင်သလို နောက်တစ်လုံးမှာ မှားပြီး ပြန်ဖတ်မိနိုင်ပါတယ်။

Overwrite နဲ့ Append ကွာခြားချက်

new FileWriter(path) က file ကို overwrite (ရှိပြီးသား content ကို ဖျက်ပြီး အသစ် ရေးခြင်း) လုပ်ပါတယ်: ပြီးတဲ့အခါ file ထဲမှာ ဒီ save က ရေးလိုက်တာပဲ ရှိပါတယ်။ Memory (မှတ်ဉာဏ်) ထဲက list က ပြည့်စုံပြီး နောက်ဆုံး ဒေတာ ဖြစ်လို့ app ရဲ့ Save အတွက် ဒါကို လိုတာပါ။

new FileWriter(path, true) က append (ရှိပြီးသား content ရဲ့ နောက်မှာ ဆက်ရေးခြင်း) လုပ်ပါတယ်: file ရဲ့ အဆုံးမှာ ထပ်ထည့်ပါတယ်။ Run တိုင်း line အသစ်တွေ ထပ်ထည့်တဲ့ log (ဖြစ်ခဲ့တာတွေကို အစဉ်လိုက် မှတ်ထားတဲ့ မှတ်တမ်း) အတွက် သုံးပါ။ App ရဲ့ list ကို save လုပ်ဖို့တော့ မသင့်တော်ပါဘူး: save လုပ်တိုင်း record အားလုံးကို ထပ်ထည့်မှာဖြစ်ပြီး နောက်တစ်ကြိမ် load လုပ်တဲ့အခါ ကျောင်းသား တစ်ယောက်စီ နှစ်ခါ ပြန်ပါလာပါလိမ့်မယ်။ Python မှာ mode "w" က overwrite လုပ်ပြီး mode "a" က append လုပ်ပါတယ်။

File ကို ဘယ်မှာ save လုပ်လဲ

"students.csv" မှာ folder နာမည် မပါလို့ Java က အဲဒီ file ကို program ရဲ့ working directory (program ကို စ run ခဲ့တဲ့ folder) ထဲမှာ ရှာပါတယ်။ App ကို IDE (code ရေးပြီး run ဖို့ သုံးတဲ့ software) ကနေ run ရင် ဒီ folder ကို IDE က ရွေးပါတယ်။ များသောအားဖြင့် project folder ဖြစ်ပြီး အဲဒါက ကိုယ့် .java file တွေ ရှိတဲ့ folder နဲ့ အမြဲ တူမနေပါဘူး။ File ဘယ်မှာ ရှိလဲ သိချင်ရင် new File("students.csv").getAbsolutePath() ကို print ထုတ်ကြည့်ပါ။ App ကို တခြား folder ကနေ စ run ရင် တခြား students.csv ကို ဖတ်ပြီး ရေးမှာဖြစ်လို့ save လုပ်ထားတဲ့ ဒေတာ ပျောက်သွားသလို ဖြစ်နေပါလိမ့်မယ်။ Python လည်း ဒီအတိုင်းပါပဲ: path အပြည့်ကို သိဖို့ os.path.abspath("students.csv") ကို print ထုတ်ကြည့်ပါ။ App ကို ဘယ်ကနေ စ run ရ run ရ file ကို ကိုယ့် Python file တွေ ဘေးမှာပဲ ထားချင်ရင် code file ရဲ့ ကိုယ်ပိုင် folder ကနေ path ကို တည်ဆောက်ပါ: os.path.join(os.path.dirname(os.path.abspath(__file__)), "students.csv")။


File ထဲကနေ load လုပ်ခြင်း

CSV ကို ArrayList ထဲ ဖတ်ထည့်ခြင်း

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.File;
import java.util.ArrayList;

public class StudentFileHandler {
    private static final String FILE_PATH = "students.csv";

    public static ArrayList<Student> load() throws IOException {
        ArrayList<Student> students = new ArrayList<>();
        File file = new File(FILE_PATH);

        if (!file.exists()) {
            return students;  // first run: there is no file yet
        }

        try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
            String line = reader.readLine();
            while (line != null) {
                String[] parts = line.split(",", -1);   // -1 keeps empty fields at the end
                if (parts.length == 2) {
                    String name = parts[0].trim();
                    int grade = Integer.parseInt(parts[1].trim());
                    students.add(new Student(name, grade));
                }
                line = reader.readLine();
            }
        }
        return students;
    }
}
# file_handler.py
import os
import models

FILE_PATH = "students.csv"


def load():
    students = []
    if os.path.exists(FILE_PATH) == False:
        return students    # first run: there is no file yet

    f = open(FILE_PATH, "r", encoding="utf-8")
    try:
        line = f.readline()
        while line != "":
            parts = line.strip().split(",")
            if len(parts) == 2:
                name = parts[0].strip()
                grade = int(parts[1].strip())
                students.append(models.Student(name, grade))
            line = f.readline()
    finally:
        f.close()
    return students

အဆင့်ဆင့် ဖြစ်ပုံ:

  1. File ရှိမရှိ စစ်ပါ။ မရှိရင် (app ကို ပထမဆုံး run တဲ့အခါ) list အလွတ် တစ်ခုကို return (ရလဒ်အဖြစ် ပြန်ပေး) လုပ်ပါ
  2. BufferedReader (file ကို ထိရောက်စွာ ဖတ်တဲ့ Java class) နဲ့ file ကို ဖွင့်ပါ
  3. readLine() နဲ့ တစ်ကြိမ်မှာ line တစ်ကြောင်းစီ ဖတ်ပါ၊ file ရဲ့ အဆုံး ရောက်ရင် readLine() က null (ဘာ object ကိုမှ မညွှန်းတဲ့ တန်ဖိုး) ကို ပြန်ပေးပါတယ်
  4. Line တစ်ကြောင်းစီကို comma တွေ နေရာမှာ ခွဲပြီး field တွေကို ယူပါ
  5. Field တစ်ခုစီကို လိုအပ်တဲ့ type ကို ပြောင်းပါ (grade အတွက် Integer.parseInt)၊ ပြီးရင် Student object တစ်ခု ဖန်တီးပါ
  6. ကျောင်းသားကို list ထဲ ထည့်ပါ
  7. try block ပြီးဆုံးတဲ့အခါ reader ကို ပိတ်ပါတယ်

Python မှာ file ရဲ့ အဆုံးမှာ readline() က string (စာသား) အလွတ် "" ကို ပြန်ပေးလို့ loop က while line != "" နဲ့ run ပါတယ်။ File အလယ်က line အလွတ်က "\n" အဖြစ် ပြန်ရလို့ loop ကို စောစော မရပ်စေပါဘူး။ Line တစ်ကြောင်းစီမှာ သူ့ "\n" ပါနေပြီး split မလုပ်ခင် strip() က အဲဒါကို ဖယ်ပေးပါတယ်။

from models import Student အစား import models ကို ဘာကြောင့် သုံးတာလဲ။ models.py က file_handler ကို import လုပ်ပါတယ် (အောက်က save နဲ့ load method တွေအတွက်)၊ file_handler.py ကလည်း models.py ထဲက Student class ကို လိုပါတယ်: file တစ်ခုစီက နောက်တစ်ခုကို import လုပ်နေတာကြောင့် circular import (file နှစ်ခု အပြန်အလှန် import လုပ်နေခြင်း) ဖြစ်ပါတယ်။ models ကို အရင် import လုပ်တဲ့ main.py ရဲ့ စတင်ပုံ အစဉ်နဲ့ဆိုရင် file_handler.py က Student ကို တောင်းတဲ့အချိန်မှာ models.py က load လုပ်လို့ မပြီးသေးတာကြောင့် from models import Student က Python ကို ImportError နဲ့ ရပ်သွားစေပါတယ်။ Function ထဲမှာ models.Student(...) လို့ ရေးပြီး ရိုးရိုး import models ကို သုံးရင်တော့ အလုပ်လုပ်ပါတယ်: နာမည်ကို load() run မှပဲ ရှာတာဖြစ်ပြီး အဲဒီအချိန်မှာ file နှစ်ခုလုံး load လုပ်ပြီးသား ဖြစ်နေလို့ပါ။

မဖတ်ခင် file ရှိမရှိ စစ်ပါ။ App ကို ပထမဆုံး run တဲ့အခါ file မရှိသေးလို့ new FileReader(FILE_PATH) က FileNotFoundException ကို throw လုပ်ပါလိမ့်မယ်။ List အလွတ်ကို return လုပ်တာက app ကို ဒေတာ မပါဘဲ စခွင့်ပေးပါတယ်၊ ပထမဆုံး save က file ကို ဖန်တီးပါတယ်။

Scanner နဲ့ PrintWriter (file ဖတ်၊ ရေးတဲ့ တခြား Java class များ)

Reading Files နဲ့ Writing Files ထဲက တခြား class တွေကလည်း ဒီအလုပ်တွေကိုပဲ လုပ်ပါတယ်:

  • Scanner reader = new Scanner(new File(FILE_PATH)) က hasNextLine() နဲ့ nextLine() ကို သုံးပြီး line တွေကို ဖတ်ပါတယ်။ File မရှိရင် သူ့ constructor (object ဖန်တီးချိန် attribute တွေကို စတင် သတ်မှတ်ပေးတဲ့ method) က FileNotFoundException ကို throw လုပ်ပါတယ်။
  • PrintWriter writer = new PrintWriter(new FileWriter(FILE_PATH)) က line တစ်ကြောင်းနဲ့ သူ့ line break ကို ခေါ်မှု တစ်ခုတည်းနဲ့ ရေးပါတယ်: writer.println(s.getName() + "," + s.getGrade());။ BufferedWriter နဲ့ မတူတာက သူ့ရဲ့ print နဲ့ println method တွေက ရေးတာ မအောင်မြင်ရင် IOException ကို throw မလုပ်ဘဲ error ကို မှတ်ထားပါတယ်။ ဒေတာ save ပြီးပြီလို့ user ကို မပြောခင် တစ်ခုခု မှားခဲ့ရင် true ကို ပြန်ပေးတဲ့ writer.checkError() ကို ခေါ်ပါ။

Python မှာ file ဖွင့်ဖို့ အသင့်ပါတဲ့ နည်း တစ်ခုတည်း (open()) ရှိလို့ ဒီရွေးချယ်မှုက Java မှာပဲ ကြုံရပါတယ်။


Comma ပါတဲ့ field တွေကို ကိုင်တွယ်ခြင်း

split(",") က line ကို comma တိုင်းမှာ ဖြတ်ပါတယ်။ Field တစ်ခုထဲမှာ comma ပါနိုင်ရင် (ဥပမာ 12 Hill Road, Ayr လို လိပ်စာ ဒါမှမဟုတ် Lee, Jo လို့ ရိုက်ထည့်ထားတဲ့ နာမည်) line က အပိုင်း အရမ်းများပြီး ကွဲသွားလို့ load method က အဲဒီ line ကို ကျော်သွားပါတယ်။ IA အတွက် ရိုးရှင်းတဲ့ ပြင်နည်း နှစ်ခု ရှိပါတယ်:

  • တခြား delimiter (အပိုင်းတွေကို ခြားပေးတဲ့ character) တစ်ခုကို သုံးပါ၊ ဥပမာ | (pipe) ဒါမှမဟုတ် tab (\t) ပါ။ ပြီးရင် Model (app ရဲ့ ဒေတာနဲ့ စည်းမျဉ်းတွေကို ထိန်းထားတဲ့ class တွေ) က ဒေတာထဲမှာ အဲဒီ character ကို လက်မခံအောင် လုပ်ပါ၊ ဒါမှ field ထဲမှာ ပေါ်မလာနိုင်မှာပါ။
  • Comma ပါနိုင်တဲ့ field ကို နောက်ဆုံးမှာ ထားပါ၊ ပြီးရင် limit နဲ့ split လုပ်ပါ: line.split(",", 3) က ပထမ comma နှစ်ခု နေရာမှာပဲ ဖြတ်လို့ တတိယ အပိုင်းက သူ့ထဲက comma တွေကို ဆက်ထိန်းထားပါတယ်။ Python မှာတော့ ဂဏန်းက အပိုင်း အရေအတွက် မဟုတ်ဘဲ ဖြတ်မယ့် အကြိမ်ရေကို ရေတွက်ပါတယ်: line.split(",", 2) က အပိုင်း သုံးပိုင်း အတူတူကို ပေးပါတယ်။
// Writing with a pipe delimiter (if Student also had an address field)
writer.write(s.getName() + "|" + s.getAddress() + "|" + s.getGrade());

// Reading with a pipe delimiter
String[] parts = line.split("\\|");
# Writing with a pipe delimiter (if Student also had an address field)
f.write(s.get_name() + "|" + s.get_address() + "|" + str(s.get_grade()) + "\n")

# Reading with a pipe delimiter
parts = line.strip().split("|")

Java ရဲ့ split() က သူ့ argument (method ကို ခေါ်တဲ့အခါ တကယ် ပေးလိုက်တဲ့ တန်ဖိုး) ကို pattern အဖြစ် (regular expression၊ စာသား ပုံစံကို ဖော်ပြတဲ့ သင်္ကေတ စနစ် အဖြစ်) သဘောထားပါတယ်။ အဲဒီ pattern ထဲမှာ | က အထူး အဓိပ္ပာယ် ရှိလို့ "\\|" လို့ ရေးရပါတယ်။ ရိုးရိုး split("|") က line ကို character (စာလုံး ဒါမှမဟုတ် သင်္ကေတ) တစ်လုံးချင်းစီ ဖြတ်ပစ်ပါတယ်။ Python ရဲ့ split() ကတော့ pattern မဟုတ်ဘဲ ရိုးရိုး စာသားကို လက်ခံလို့ split("|") က ရေးထားတဲ့အတိုင်း အလုပ်လုပ်ပါတယ်။

Java ရဲ့ split() က line ရဲ့ အဆုံးက field အလွတ်တွေကိုလည်း ဖယ်ပစ်ပါတယ်၊ ဒါကြောင့် "Ana,91,,".split(",") က အပိုင်း လေးပိုင်း မဟုတ်ဘဲ နှစ်ပိုင်းပဲ ပေးပါတယ်။ Line ရဲ့ နောက်ဆုံး field တွေ အလွတ် ဖြစ်နိုင်ရင်၊ ဒါမှမဟုတ် load method က field အရေအတွက်ကို စစ်ရင် field အလွတ်တွေကို ထိန်းထားပေးတဲ့ line.split(",", -1) ကို သုံးပါ။ ဒီစာမျက်နှာက load method တွေ ဒီလို သုံးထားလို့ Ana,91, လို line ကို field သုံးခုလို့ ရေတွက်ပြီး ကျော်သွားပါတယ်။ Python ရဲ့ split() ကတော့ field အလွတ်တွေကို ထိန်းထားပြီးသားပါ: "Ana,91,,".split(",") က အပိုင်း လေးပိုင်း ပေးပါတယ်။


MVC ထဲကို File I/O ပေါင်းထည့်ခြင်း

File I/O က ဘယ်မှာ ရှိရမလဲ

ဒီစာမျက်နှာရဲ့ ဒီဇိုင်းမှာ file ကို ဖတ်တဲ့၊ ရေးတဲ့ code က Model နဲ့ အတူ ရှိပါတယ်: သီးခြား “file handler” (file ဖတ်တာ၊ ရေးတာကို တာဝန်ယူတဲ့ class) class တစ်ခု၊ ဒါမှမဟုတ် collection (ဒေတာအစု) class ထဲက load နဲ့ save method တွေပါ။ File က Model ရဲ့ ဒေတာကို သိမ်းထားလို့ Model ကို သုံးတဲ့ screen တိုင်းက ဒေတာကို နည်းတူ save လုပ်နိုင်ပါတယ်။

အသုံးများတဲ့ နောက်ထပ် ဒီဇိုင်း တစ်ခုက ဒေတာ အမျိုးအစား တစ်ခုစီအတွက် file code ကို အဲဒီ ဒေတာကို သုံးတဲ့ controller (user လုပ်တာတွေကို ကိုင်တွယ်ပြီး Model နဲ့ screen တွေကို ချိတ်ပေးတဲ့ class) ထဲမှာ ထားတာပါ။ Screen တစ်ခုပဲ ပါတဲ့ app မှာ ရေးရ ပိုမြန်ပေမယ့် controller က file ပုံစံပေါ် မူတည်သွားပြီး ဒေတာ အတူတူ လိုတဲ့ ဒုတိယ screen က code ကို ထပ်ရေးရမယ်၊ ဒါမှမဟုတ် အဲဒီ controller ထဲကို လှမ်းယူရပါမယ်။ အကြောင်းပြချက် ပေးနိုင်ရင် ဒီဇိုင်း နှစ်ခုထဲက ဘယ်ဟာကိုမဆို ကိုယ့် IA မှာ ရွေးသုံးလို့ ရပါတယ်။ ဘယ်ဒီဇိုင်းကမှ file code ကို View ထဲမှာ မထားပါဘူး။

flowchart LR
    V["View<br>(screens)"] --- C["Controller<br>(handlers)"]
    C --- M["Model<br>StudentList +<br>StudentFileHandler"]
    M --> F[("students.csv")]

ပုံထဲမှာ View (screen တွေ) က Controller (handler တွေ) နဲ့ ချိတ်ထားပြီး Controller က Model (StudentList နဲ့ StudentFileHandler) နဲ့ ချိတ်ထားပါတယ်။ students.csv file ကို Model ကပဲ ဖတ်၊ ရေးပါတယ်။

Model ထဲကို Load/Save ထည့်ခြင်း

MVC Architecture ထဲက StudentList class ထဲကို method နှစ်ခု ထည့်ပြီး သူ့ file ရဲ့ ထိပ်မှာ import java.io.IOException; ကို ထည့်ပါ (Python မှာဆိုရင် MVC with tkinter ထဲက StudentList ကို သုံးပြီး models.py ရဲ့ ထိပ်မှာ import file_handler ကို ထည့်ပါ):

// In StudentList
public void saveToFile() throws IOException {
    StudentFileHandler.save(students);
}

public void loadFromFile() throws IOException {
    students = StudentFileHandler.load();
}
# In StudentList (models.py)
def save_to_file(self):
    file_handler.save(self._students)

def load_from_file(self):
    self._students = file_handler.load()

Controller: စတင်ချိန်မှာ load လုပ်ပြီး button နှိပ်ရင် save လုပ်ခြင်း

FXML and Controllers ထဲက StudentController မှာ load လုပ်တဲ့ code ကို setModel() ထဲမှာ ထားပါတယ်၊ setModel() က controller ဆီ Model ရောက်တာနဲ့ run ပါတယ်။ Save button အသစ် တစ်ခုက handleSave() ကို ခေါ်ပါတယ်။ student-view.fxml ထဲက button တွေကြားမှာ <Button text="Save" onAction="#handleSave"/> ကို ထည့်ပြီး controller ထဲမှာ import java.io.IOException; ကို ထည့်ပါ။ Python version မှာတော့ load လုပ်တဲ့ code ကို controller ရဲ့ constructor ထဲမှာ ထားပါတယ်။ View မှာ Save button တစ်ခု တိုးလာပြီး set_handlers က handle_save ကိုပါ လက်ခံပါတယ်။ တခြား program တစ်ခုက မတူတဲ့ text encoding နဲ့ save လုပ်ထားတဲ့ file ကို ဖတ်ရင် UnicodeError (ValueError အမျိုးအစား တစ်ခု) ဖြစ်နိုင်လို့ constructor က OSError အပြင် အဲဒါကိုပါ catch လုပ်ပါတယ်။

// In StudentController
public void setModel(StudentList model) {
    this.model = model;
    try {
        model.loadFromFile();
        status.setText("Loaded " + model.size() + " students.");
    } catch (IOException e) {
        status.setText("Could not load data: " + e.getMessage());
    }
    refreshDisplay();
}

@FXML
private void handleSave() {
    try {
        model.saveToFile();
        status.setText("Saved " + model.size() + " students.");
    } catch (IOException e) {
        status.setText("Error saving: " + e.getMessage());
    }
}
# In StudentController (controller.py)
def __init__(self, model, view):
    self._model = model
    self._view = view
    view.set_handlers(self.handle_add, self.handle_remove, self.handle_search, self.handle_save)
    try:
        model.load_from_file()
        view.show_status("Loaded " + str(model.size()) + " students.")
    except (OSError, UnicodeError) as error:    # UnicodeError: a file saved in another text encoding
        view.show_status("Could not load data: " + str(error))
    self.refresh_display()

def handle_save(self):
    try:
        self._model.save_to_file()
        self._view.show_status("Saved " + str(self._model.size()) + " students.")
    except OSError as error:
        self._view.show_status("Error saving: " + str(error))

ပထမဆုံး run မှာ စတင် ဒေတာ ထည့်ခြင်း

တချို့ app တွေမှာ user က ဘာမှ မထည့်ရသေးခင် record အနည်းငယ် လိုပါတယ်၊ ဥပမာ login screen အတွက် default admin account (မူလ စီမံခန့်ခွဲသူ account) တစ်ခုပါ။ File ရှိမရှိ စစ်ပါ၊ load လုပ်ပါ၊ ပြီးရင် file မရှိသေးတဲ့ ပထမဆုံး run မှာပဲ စတင် record တွေကို ထည့်ပါ:

// Inside the try block in setModel(), in place of model.loadFromFile();
// (needs import java.io.File; at the top of the controller)
boolean firstRun = !new File("students.csv").exists();
model.loadFromFile();
if (firstRun) {
    model.addStudent(new Student("Example Student", 50));
    model.saveToFile();
}
# Inside the try block in the constructor, in place of model.load_from_file()
# (needs import os at the top of controller.py)
first_run = os.path.exists(file_handler.FILE_PATH) == False
model.load_from_file()
if first_run == True:
    model.add_student(Student("Example Student", 50))
    model.save_to_file()

List အလွတ် ဟုတ်မဟုတ် စစ်တာ မဟုတ်ဘဲ file ရှိမရှိ စစ်တာက အရေးကြီးပါတယ်: ရှိပြီးသား file ထဲက line တိုင်းကို လက်မခံခဲ့ရင် list က အလွတ် ဖြစ်နေမှာဖြစ်ပြီး save က user ရဲ့ ဒေတာကို စတင် record နဲ့ အစားထိုးပစ်ပါလိမ့်မယ်။ တခြား အစဉ်နဲ့ဆိုရင် (စတင် record တွေ ထည့်၊ save လုပ်၊ ပြီးမှ load လုပ်ရင်) app စတိုင်း save က file ကို overwrite လုပ်ပြီး user ထည့်ခဲ့တာ အားလုံး ပျောက်သွားပါတယ်။

Window ပိတ်ချိန်မှာ အလိုအလျောက် save လုပ်ခြင်း

User က window ကို ပိတ်တဲ့အခါ save လုပ်ဖို့ Application class ထဲမှာ window ကို close-request handler (user က window ပိတ်ဖို့ တောင်းဆိုတဲ့အခါ run တဲ့ code) တစ်ခု ပေးပါ။ ဒါက FXML and Controllers ထဲက StudentApp ပါ၊ handler က Model ကို သုံးနိုင်အောင် Model ကို variable တစ်ခုထဲမှာ ထားထားပါတယ်:

import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Stage;

public class StudentApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        FXMLLoader loader = new FXMLLoader(
            getClass().getResource("student-view.fxml")
        );
        Parent root = loader.load();

        StudentList model = new StudentList();
        StudentController controller = loader.getController();
        controller.setModel(model);   // loads the file

        stage.setOnCloseRequest(event -> {
            try {
                model.saveToFile();
            } catch (IOException e) {
                Alert ask = new Alert(Alert.AlertType.CONFIRMATION);
                ask.setHeaderText("Could not save: " + e.getMessage());
                ask.setContentText("Close anyway? Changes since the last save will be lost.");
                if (ask.showAndWait().get() != ButtonType.OK) {
                    event.consume();   // cancels the close: the window stays open
                }
            }
        });

        stage.setTitle("Student Manager");
        stage.setScene(new Scene(root, 450, 450));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
# main.py
import tkinter as tk
from tkinter import messagebox
from models import StudentList
from views import StudentView
from controller import StudentController

window = tk.Tk()
window.title("Student Manager")
model = StudentList()
view = StudentView(window)
controller = StudentController(model, view)    # loads the file


def handle_close():
    try:
        model.save_to_file()
        window.destroy()
    except OSError as error:
        if messagebox.askokcancel("Could not save", str(error) + "\n\nClose anyway? Changes since the last save will be lost.") == True:
            window.destroy()    # the user chose to close without saving


window.protocol("WM_DELETE_WINDOW", handle_close)    # runs when the user clicks the close button
window.mainloop()

Handler က JavaFX Basics မှာလို lambda (နာမည်မရှိတဲ့ method အတိုလေး) တစ်ခုပါ။ Lambda တွေက စာမေးပွဲမှာ မေးတဲ့ အကြောင်းအရာ မဟုတ်ပါဘူး။ Save မအောင်မြင်ရင် user က ရွေးရပါတယ်: OK က save မလုပ်ဘဲ window ကို ပိတ်ပြီး Cancel က window ကို ဆက်ဖွင့်ထားပါတယ်၊ ဘာလို့လဲဆိုတော့ event.consume() က ပိတ်တာကို ပယ်ဖျက်လိုက်လို့ပါ။ Python မှာ user က close button ကို နှိပ်တဲ့အခါ protocol("WM_DELETE_WINDOW", handle_close) က handle_close ကို run ပြီး handle_close က window.destroy() ကို ခေါ်မှပဲ window ပိတ်ပါတယ်။

Close request က user က window ကို ပိတ်ဖို့ တောင်းဆိုမှပဲ run ပါတယ်၊ ဥပမာ close button နဲ့ပါ။ ကိုယ့် code က stage.close() ဒါမှမဟုတ် Platform.exit() (Python မှာ window.destroy()) ကို ခေါ်တဲ့အခါ မ run ပါဘူး၊ ဒါကြောင့် Quit button ဒါမှမဟုတ် menu item တစ်ခုက app ကို မပိတ်ခင် save လုပ်ရပါမယ်။ Extension 3 (လေ့ကျင့်ခန်း 3) မှာ မေးထားသလို ပြောင်းလဲမှု တစ်ခုပြီးတိုင်း save လုပ်ရင် app ရုတ်တရက် ရပ်သွားတဲ့အခါ ပျောက်တာ ပိုနည်းပါတယ်။ ဒါပေမယ့် save လုပ်နေတုန်း တစ်ဝက်တစ်ပျက်မှာ ရပ်သွားတဲ့ app က file ကို မပြည့်စုံဘဲ ချန်ထားခဲ့နိုင်ပါသေးတယ်၊ ဘာလို့လဲဆိုတော့ ရေးဖို့ ဖွင့်လိုက်တာနဲ့ file ကို အရင် ရှင်းပစ်လို့ပါ။


Object အမျိုးအစား အများကို ကိုင်တွယ်ခြင်း

ကိုယ့် IA မှာ object အမျိုးအစား တစ်ခုထက် ပိုရှိရင် အမျိုးအစား တစ်ခုစီကို သူ့ရဲ့ ကိုယ်ပိုင် file ထဲမှာ ကိုယ်ပိုင် file handler နဲ့ save လုပ်ပါ။ ဥပမာ TeacherList ကိုပါ ထားတဲ့ app က အဲဒါကို StudentFileHandler လိုပဲ ရေးထားတဲ့ TeacherFileHandler ကနေ တစ်ဆင့် teachers.csv ထဲ save လုပ်ပါလိမ့်မယ်:

import java.io.IOException;

public class DataManager {

    public static void saveAll(StudentList students, TeacherList teachers)
            throws IOException {
        students.saveToFile();
        teachers.saveToFile();
    }

    public static void loadAll(StudentList students, TeacherList teachers)
            throws IOException {
        students.loadFromFile();
        teachers.loadFromFile();
    }
}
# data_manager.py
def save_all(students, teachers):
    students.save_to_file()
    teachers.save_to_file()


def load_all(students, teachers):
    students.load_from_file()
    teachers.load_from_file()

Model class တစ်ခုအတွက် file တစ်ခု ထားရင် file တစ်ခုထဲက line တိုင်း ပုံစံ တူနေလို့ load method တစ်ခုစီက line အမျိုးအစား တစ်မျိုးတည်းကိုပဲ ဖတ်ရပါတယ်။ Record အမျိုးအစား တစ်ခုက နောက်တစ်မျိုးကို ညွှန်းနေရင် ချိတ်ဆက်မှု တစ်ခုစီကို စစ်နိုင်အောင် ညွှန်းခံရတဲ့ record တွေကို အရင် load လုပ်ပါ။


Error ကိုင်တွယ်ပုံ ပုံစံများ

Model ထဲမှာ မဟုတ်ဘဲ Controller ထဲမှာ catch လုပ်ပါ

ဒီစာမျက်နှာရဲ့ ဒီဇိုင်းမှာ Model ရဲ့ saveToFile() နဲ့ loadFromFile() က exception တွေကို throw လုပ်ပါတယ်။ Controller က အဲဒါတွေကို catch လုပ်ပြီး user ကို ရှင်းလင်းတဲ့ message တစ်ခု ပြပါတယ်။

// Model: throws the exception
public void saveToFile() throws IOException {
    StudentFileHandler.save(students);
}

// Controller: catches it and shows feedback
@FXML
private void handleSave() {
    try {
        model.saveToFile();
        status.setText("Saved successfully.");
    } catch (IOException e) {
        status.setText("Error: " + e.getMessage());
    }
}
# Model: does not catch the error, so it passes to the code that called it
def save_to_file(self):
    file_handler.save(self._students)

# Controller: catches it and shows feedback
def handle_save(self):
    try:
        self._model.save_to_file()
        self._view.show_status("Saved successfully.")
    except OSError as error:
        self._view.show_status("Error: " + str(error))

Model ထဲမှာ ဘာကြောင့် catch မလုပ်တာလဲ။ Model က UI (user interface၊ user နဲ့ program ကြားက မြင်ကွင်း) အကြောင်း မသိပါဘူး။ Error message တွေကို မပြနိုင်သလို label တွေကိုလည်း update မလုပ်နိုင်ပါဘူး။ Exception ကို throw လုပ်လိုက်တာနဲ့ user ကို ဘယ်လို အသိပေးမလဲ ဆိုတာ Controller ကို ဆုံးဖြတ်ခွင့် ပေးပါတယ်။

ပျက်စီးနေတဲ့ ဒေတာကို ကိုင်တွယ်ခြင်း

တစ်ယောက်ယောက်က text editor နဲ့ file ကို ပြင်လိုက်ပြီးတဲ့နောက်လို အခြေအနေမျိုးမှာ file ထဲမှာ load method က သုံးလို့ မရတဲ့ line တွေ ပါလာနိုင်ပါတယ်။ အပေါ်က load method က field အရေအတွက် မှားနေတဲ့ line ကို ကျော်ပြီးသားပါ၊ ဒါပေမယ့် ကိန်းပြည့် မဟုတ်တဲ့ grade၊ ဒါမှမဟုတ် Student class က လက်မခံတဲ့ နာမည် ဒါမှမဟုတ် grade တစ်ခုက load တစ်ခုလုံးကို ရပ်သွားစေပါတယ်: Integer.parseInt က NumberFormatException (ဂဏန်း မဟုတ်တဲ့ စာသားကို ဂဏန်း ပြောင်းလို့ ဖြစ်တဲ့ exception) ကို throw လုပ်တာ၊ ဒါမှမဟုတ် Student constructor က IllegalArgumentException (method ကို ပေးတဲ့ တန်ဖိုးကို လက်မခံလို့ ဖြစ်တဲ့ exception) ကို throw လုပ်တာ ဖြစ်ပါတယ်။ နှစ်ခုလုံး IOException မဟုတ်လို့ controller ရဲ့ catch က မကိုင်တွယ်ပါဘူး၊ ဒါကြောင့် app က window မပွင့်ခင်မှာပဲ ရပ်သွားပါတယ်။ Python မှာလည်း ဒီအတိုင်းပါပဲ: int() နဲ့ Student constructor က ValueError ကို raise (exception ဖြစ်ကြောင်း အချက်ပြ) လုပ်ပြီး except OSError က အဲဒါကို catch မလုပ်ပါဘူး။ အဲဒီအစား line မကောင်းတိုင်းကို ကျော်ပြီး ရေတွက်ထားပါ:

// Inside the try block of load(); skippedLines is a static int field
String line = reader.readLine();
while (line != null) {
    String[] parts = line.split(",", -1);   // -1 keeps empty fields at the end
    if (parts.length == 2) {
        try {
            String name = parts[0].trim();
            int grade = Integer.parseInt(parts[1].trim());
            students.add(new Student(name, grade));
        } catch (NumberFormatException e) {
            skippedLines++;   // the grade is not a whole number
        } catch (IllegalArgumentException e) {
            skippedLines++;   // Student refused the name or the grade
        }
    } else if (!line.trim().isEmpty()) {
        skippedLines++;       // the wrong number of fields
    }
    line = reader.readLine();
}
# Inside the try block of load(); skipped_lines is a variable at the top of
# file_handler.py, and load() starts with: global skipped_lines
line = f.readline()
while line != "":
    parts = line.strip().split(",")
    if len(parts) == 2:
        try:
            name = parts[0].strip()
            grade = int(parts[1].strip())
            students.append(models.Student(name, grade))
        except ValueError:
            skipped_lines = skipped_lines + 1    # not a whole number, or refused by Student
    elif line.strip() != "":
        skipped_lines = skipped_lines + 1        # the wrong number of fields
    line = f.readline()

NumberFormatException က IllegalArgumentException အမျိုးအစား တစ်ခု ဖြစ်လို့ သူ့ catch ကို အရင် ရေးရပါတယ်။ အစဉ် ပြောင်းပြန် ရေးရင် code က compile မဖြစ်ပါဘူး။ Python မှာ int() နဲ့ Student constructor နှစ်ခုလုံးက ValueError ကို raise လုပ်လို့ except တစ်ခုတည်းက နှစ်ခုလုံးကို ကိုင်တွယ်ပေးပါတယ်။ global skipped_lines က load() ကို local variable (method ထဲမှာပဲ ရှိတဲ့ variable) အသစ် တစ်ခု မဖန်တီးဘဲ file ရဲ့ ထိပ်က variable ကို ပြောင်းခွင့် ပေးပါတယ်။

Line တွေ ကျော်သွားရင် user ကို ပြောပါ။ ကျော်သွားတဲ့ line က list ထဲမှာ မပါလို့ နောက်တစ်ကြိမ် save လုပ်တဲ့အခါ အဲဒီ line မပါဘဲ file ကို ရေးမှာဖြစ်ပြီး အဲဒီ record ပျောက်သွားပါတယ်။ အောက်က handler အပြည့်အစုံက ကျော်သွားတဲ့ line တွေကို ရေတွက်ထားလို့ controller က အရေအတွက်ကို ပြနိုင်ပါတယ်။ မပြည့်စုံဘဲ load ဖြစ်ခဲ့တဲ့ file ကို save လုပ်ပြီး မဖုံးပါနဲ့ အပိုင်းကလည်း file ကို ပြင်ပြီးတဲ့အထိ save လုပ်တာကို ပိတ်ထားပါတယ်။


File Handler ဥပမာ အပြည့်အစုံ

ဒါက IA project အတွက် သင့်တော်တဲ့ save နဲ့ load နှစ်ခုလုံး ပါတဲ့ StudentFileHandler အပြည့်အစုံပါ။

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

public class StudentFileHandler {
    private static final String FILE_PATH = "students.csv";
    private static int skippedLines = 0;   // lines the last load could not use

    public static void save(ArrayList<Student> students) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_PATH))) {
            for (int i = 0; i < students.size(); i++) {
                Student s = students.get(i);
                writer.write(s.getName() + "," + s.getGrade());
                writer.newLine();
            }
        }
    }

    public static ArrayList<Student> load() throws IOException {
        ArrayList<Student> students = new ArrayList<>();
        skippedLines = 0;
        File file = new File(FILE_PATH);

        if (!file.exists()) {
            return students;   // first run: there is no file yet
        }

        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line = reader.readLine();
            while (line != null) {
                String[] parts = line.split(",", -1);   // -1 keeps empty fields at the end
                if (parts.length == 2) {
                    try {
                        String name = parts[0].trim();
                        int grade = Integer.parseInt(parts[1].trim());
                        students.add(new Student(name, grade));
                    } catch (NumberFormatException e) {
                        skippedLines++;   // the grade is not a whole number
                    } catch (IllegalArgumentException e) {
                        skippedLines++;   // Student refused the name or the grade
                    }
                } else if (!line.trim().isEmpty()) {
                    skippedLines++;       // the wrong number of fields
                }
                line = reader.readLine();
            }
        }
        return students;
    }

    public static int getSkippedLines() {
        return skippedLines;
    }
}
# file_handler.py
import os
import models

FILE_PATH = "students.csv"
skipped_lines = 0    # lines the last load could not use


def save(students):
    f = open(FILE_PATH, "w", encoding="utf-8")
    try:
        for i in range(len(students)):
            s = students[i]
            f.write(s.get_name() + "," + str(s.get_grade()) + "\n")
    finally:
        f.close()


def load():
    global skipped_lines    # load() changes the variable at the top of this file
    students = []
    skipped_lines = 0
    if os.path.exists(FILE_PATH) == False:
        return students    # first run: there is no file yet

    f = open(FILE_PATH, "r", encoding="utf-8")
    try:
        line = f.readline()
        while line != "":
            parts = line.strip().split(",")
            if len(parts) == 2:
                try:
                    name = parts[0].strip()
                    grade = int(parts[1].strip())
                    students.append(models.Student(name, grade))
                except ValueError:
                    skipped_lines = skipped_lines + 1    # not a whole number, or refused by Student
            elif line.strip() != "":
                skipped_lines = skipped_lines + 1        # the wrong number of fields
            line = f.readline()
    finally:
        f.close()
    return students


def get_skipped_lines():
    return skipped_lines

getSkippedLines() က နောက်ဆုံး load က ကျော်ခဲ့တဲ့ line အရေအတွက်ကို ပြန်ပေးပါတယ်။ setModel() ထဲမှာ (Python မှာဆိုရင် controller ရဲ့ constructor ထဲမှာ) ပုံမှန် “Loaded” message အစား အဲဒီ အရေအတွက်ကို ပြပါ၊ ဒါမှ သတိပေးချက်ကို ချက်ချင်း အစားထိုးမခံရမှာပါ:

// In StudentController's setModel(), in place of the "Loaded ..." status line
int skipped = StudentFileHandler.getSkippedLines();
if (skipped > 0) {
    status.setText("Loaded " + model.size() + " students. Lines that could not be read: "
        + skipped + ". Fix students.csv, then restart the app.");
} else {
    status.setText("Loaded " + model.size() + " students.");
}
# In StudentController's constructor, in place of the "Loaded ..." status line,
# with import file_handler at the top of controller.py
skipped = file_handler.get_skipped_lines()
if skipped > 0:
    view.show_status("Loaded " + str(model.size()) + " students. Lines that could not be read: "
                     + str(skipped) + ". Fix students.csv, then restart the app.")
else:
    view.show_status("Loaded " + str(model.size()) + " students.")

မပြည့်စုံဘဲ load ဖြစ်ခဲ့တဲ့ file ကို save လုပ်ပြီး မဖုံးပါနဲ့

Load မအောင်မြင်ရင် (file ကို ဖတ်လို့ မရရင်) ဒါမှမဟုတ် line တွေ ကျော်ခဲ့ရင် memory ထဲက list က file တစ်ခုလုံး မဟုတ်ပါဘူး။ အဲဒီ list ကို save လုပ်ရင် file ကို overwrite လုပ်ပြီး load မဖြစ်ခဲ့တဲ့ record တွေ ပျောက်သွားပါတယ်: နောက်တစ်ကြိမ် Save နှိပ်တဲ့အခါ၊ close request မှာ၊ ဒါမှမဟုတ် app က စတင် ဒေတာ ထည့်ရင် ချက်ချင်းပဲ ပျောက်ပါတယ်။ လုံခြုံတဲ့ စည်းမျဉ်း တစ်ခုက load မအောင်မြင်ခဲ့ရင် ဒါမှမဟုတ် line တွေ ကျော်ခဲ့ရင် နောက်ထပ် load တစ်ခုက file တစ်ခုလုံးကို ဖတ်နိုင်တဲ့အထိ Model က save လုပ်တာကို ပိတ်ထားတာပါ။ User က file ကို ပြင်ပြီး app ကို ပြန်စပါတယ်။

// In StudentList: saving stays off until a load has read the whole file
private boolean canSave = false;

public void loadFromFile() throws IOException {
    canSave = false;
    students = StudentFileHandler.load();   // if this throws, canSave stays false
    canSave = StudentFileHandler.getSkippedLines() == 0;
}

public void saveToFile() throws IOException {
    if (!canSave) {
        throw new IOException("Saving is switched off because students.csv did not load completely. "
            + "Fix or remove the lines it could not read, then restart the app.");
    }
    StudentFileHandler.save(students);
}
# In StudentList (models.py): saving stays off until a load has read the whole file
def __init__(self):
    self._students = []
    self._can_save = False

def load_from_file(self):
    self._can_save = False
    self._students = file_handler.load()    # if this raises an error, saving stays off
    self._can_save = file_handler.get_skipped_lines() == 0

def save_to_file(self):
    if self._can_save == False:
        raise OSError("Saving is switched off because students.csv did not load completely. "
                      + "Fix or remove the lines it could not read, then restart the app.")
    file_handler.save(self._students)

လက်မခံကြောင်း ပြတာက IOException (Python မှာ OSError) ဖြစ်လို့ controller ရဲ့ ရှိပြီးသား catch က အဲဒါကို status label (အခြေအနေ ပြတဲ့ label) မှာ ပြပြီး close request ကလည်း မပိတ်ခင် မေးပါတယ်။ ပထမဆုံး run မှာ save လုပ်ထားတဲ့ file မရှိသေးလို့ save လုပ်ခွင့် ပွင့်နေပါတယ်။


Quick Check (အမြန် စစ်ဆေးမေးခွန်း)

Q1. File နဲ့ အလုပ်လုပ်တဲ့အခါ try-with-resources ကို သုံးတာရဲ့ အဓိက အကျိုးကျေးဇူးက ဘာလဲ။

Q2. File ရဲ့ အဆုံး ရောက်တဲ့အခါ reader.readLine() က ဘာကို ပြန်ပေးသလဲ။

Q3. ဒီစာမျက်နှာရဲ့ ဒီဇိုင်းမှာ ဒေတာ file ကို ဖတ်တဲ့၊ ရေးတဲ့ code က ဘယ်မှာ ရှိသလဲ။

Q4. App ကို ပထမဆုံး run တဲ့အခါ students.csv မရှိသေးပါဘူး။ StudentFileHandler.load() က ဘာလုပ်သင့်သလဲ။

Q5. "Alice,85".split(",") က ဘာကို ပြန်ပေးသလဲ။


Code Completion (code ကွက်လပ် ဖြည့်ခြင်း)

ကျောင်းသားတွေကို CSV file ထဲ ရေးပေးတဲ့ save method ကို ဖြည့်ပါ။

ကွက်လပ် ဖြည့်ပါ: Student တွေပါတဲ့ ArrayList တစ်ခုကို file ထဲ save လုပ်ပေးတဲ့ ဒီ method ကို ဖြည့်ပါ။

public static void save(ArrayList<Student> students) throws IOException {
    try (BufferedWriter writer = new BufferedWriter(
            new ("students.csv"))) {
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            writer.(s.getName() + "," + s.getGrade());
            writer.;
        }
    }
}

Spot the Error (အမှား ရှာခြင်း)

ဒီ load method က file ထဲက ကျောင်းသား အားလုံးကို ဖတ်ရမှာပါ၊ ဒါပေမယ့် ဘယ်တော့မှ မပြီးပါဘူး။

အမှား ရှာပါ: ဒီ method က CSV file ကနေ ဖတ်ပေမယ့် ဘယ်တော့မှ မပြီးပါဘူး: program ရဲ့ memory ကုန်သွားတဲ့အထိ ပထမ ကျောင်းသားရဲ့ copy တွေကို ဆက်ထည့်နေပါတယ်။ (Line 7 က comment ဖြစ်ပြီး "ဒီနေရာမှာ တစ်ခုခု ကျန်နေတယ်" လို့ ဆိုလိုပါတယ်။)

1ArrayList<Student> students = new ArrayList<>(); 2BufferedReader reader = new BufferedReader(new FileReader("data.csv")); 3String line = reader.readLine(); 4while (line != null) { 5 String[] parts = line.split(","); 6 students.add(new Student(parts[0], Integer.parseInt(parts[1]))); 7 // missing something here 8}

Line 7 အတွက် မှန်ကန်တဲ့ ပြင်နည်းကို ရွေးပါ:


Predict the Output (output ကို ကြိုခန့်မှန်းခြင်း)

ကြိုခန့်မှန်းပါ: CSV line ကို အလုပ်လုပ်ပြီးနောက် ဒီ code က ဘာကို print ထုတ်သလဲ။

String line = "Alice,85";
String[] parts = line.split(",");
Student s = new Student(parts[0], Integer.parseInt(parts[1]));
System.out.println(s);
// Student.toString() returns: name + " (" + grade + ")"

Practice Exercises (လေ့ကျင့်ခန်းများ)

(4 marks) စတာတွေက လေ့ကျင့်ခန်း တစ်ခုစီအတွက် ရနိုင်တဲ့ အမှတ်ပါ။ IB စာမေးပွဲ မေးခွန်းတွေမှာ အမှတ်ကို ဒီလို ပြလေ့ ရှိပါတယ်။

Core (အခြေခံ အဆင့်)

  1. နာမည်တွေကို save လုပ်ပြီး ပြန် load လုပ်ခြင်း (4 marks): User က list (Java မှာ ArrayList<String>) ထဲကို နာမည်တွေ ထည့်နိုင်တဲ့ ရိုးရှင်းတဲ့ app တစ်ခုကို Construct (ကိုယ်တိုင် တည်ဆောက်ပါ)။ နာမည် အားလုံးကို line တစ်ကြောင်းမှာ တစ်ခုစီ names.txt ထဲ ရေးပေးတဲ့ Save button တစ်ခုနဲ့ အဲဒီ နာမည်တွေကို list ထဲ ပြန်ဖတ်ထည့်ပြီး ပြပေးတဲ့ Load button တစ်ခု ထည့်ပါ။ App ကို ပိတ်၊ ပြန်ဖွင့်ပြီး Load ကို နှိပ်တဲ့အခါ နာမည်တွေ ရှိနေသေးလား စစ်ကြည့်ပါ။

  2. Field သုံးခုပါ CSV (4 marks): ကျောင်းသား တစ်ယောက်စီမှာ တတိယ field အဖြစ် save လုပ်မယ့် house (ကျောင်းမှာ ကျောင်းသားတွေကို ခွဲထားတဲ့ အသင်း) လည်း ပါတဲ့ Student Manager version တစ်ခုကို Construct လုပ်ပါ (ဥပမာ Alice,85,Blue)။ တတိယ field အတွက် Student class နဲ့ save၊ load method တွေကို update လုပ်ပြီး field အတိအကျ 3 ခု မပါတဲ့ line တွေကို load method က ကျော်သွားအောင် လုပ်ပါ။

Extension (တိုးချဲ့ အဆင့်)

  1. အလိုအလျောက် save လုပ်ခြင်း (6 marks): Student Manager အတွက် အလိုအလျောက် save လုပ်တာကို Construct လုပ်ပါ: user က ကျောင်းသား တစ်ယောက်ကို ထည့်တိုင်း ဒါမှမဟုတ် ဖယ်တိုင်း list ကို save လုပ်ပြီး app စတဲ့အခါ load လုပ်ပါ။ Load လုပ်ခဲ့တဲ့ ကျောင်းသား အရေအတွက်ကို status label မှာ ပြပြီး save မအောင်မြင်ရင် အဲဒီမှာပဲ message တစ်ခု ပြပါ။

  2. လူဖတ်လို့ ရတဲ့ ပုံစံနဲ့ export လုပ်ခြင်း (6 marks): ကျောင်းသား list ကို report.txt ထဲ လူဖတ်ဖို့ report တစ်ခုအဖြစ် (app က load လုပ်ဖို့ CSV file မဟုတ်ဘဲ) ရေးပေးတဲ့ Export button တစ်ခုကို Construct လုပ်ပါ၊ ဒီလို ပုံစံပါ:

    Student Report
    ==============
    1. Alice - Grade: 85
    2. Bob - Grade: 92
    Total students: 2
    

Challenge (စိန်ခေါ်မှု အဆင့်)

  1. Merit (ချီးကျူးမှတ်) မှတ်တမ်း (8 marks): Student Manager အတွက် merit မှတ်တမ်း တစ်ခုကို Construct လုပ်ပါ။ Merit တစ်ခုက အဲဒီ merit ကို ရခဲ့တဲ့ ကျောင်းသားရဲ့ နာမည်၊ စာသားအဖြစ် ရက်စွဲ (ဥပမာ 2026-10-20) နဲ့ အကြောင်းရင်း တစ်ခုကို သိမ်းပါတယ်။ Merit တွေကို Model ထဲက MeritList တစ်ခုမှာ ထားပြီး students.csv နဲ့ သီးခြား merits.csv ထဲမှာ save လုပ်ပါ:
    • User က ကျောင်းသား နာမည်နဲ့ အကြောင်းရင်းကို ရိုက်ထည့်ပြီး Add Merit ကို နှိပ်ကာ merit တစ်ခု ထည့်ပါတယ်။ ရက်စွဲက ဒီနေ့ရက်စွဲ ဖြစ်ပြီး java.time.LocalDate.now().toString() ကနေ ယူပါတယ် (Python မှာ import datetime လုပ်ပြီးနောက် str(datetime.date.today()))။ List ထဲမှာ မရှိတဲ့ နာမည်အတွက် merit ကို message တစ်ခု ပြပြီး လက်မခံရပါဘူး။
    • Save button က file နှစ်ခုလုံးကို save လုပ်ပါတယ်။
    • App စတဲ့အခါ ကျောင်းသားတွေကို အရင် load လုပ်ပြီးမှ merit တွေကို load လုပ်ပါတယ်။ students.csv ထဲမှာ ကျောင်းသား မရှိတဲ့ merit ကို ချန်ထားခဲ့ပြီး ဘယ်နှစ်ခု ချန်ခဲ့လဲ ဆိုတာကို status label မှာ ပြပါတယ်။
    • Display က ကျောင်းသား တစ်ယောက်စီကို သူတို့ရဲ့ merit အရေအတွက်နဲ့ အတူ ပြပါတယ်၊ ဥပမာ 1. Ana (91): 2 merits။
    • အကြောင်းရင်းမှာ comma တွေ ပါနိုင်လို့ (Set up, and packed away, the science fair၊ “သိပ္ပံပြပွဲကို ပြင်ဆင်ပေးပြီး ပြန်သိမ်းပေးခဲ့တယ်”) အဲဒီလို အကြောင်းရင်းကို မှန်မှန်ကန်ကန် ပြန်ဖတ်နိုင်တဲ့ file ပုံစံကို ရွေးပါ။
  2. ပျောက်သွားတဲ့ record ကို ရှင်းပြခြင်း (6 marks): အတန်းဖော် တစ်ယောက်ရဲ့ app က ပျက်စီးနေတဲ့ ဒေတာကို ကိုင်တွယ်ခြင်း ထဲက load method ကို သုံးပြီး window ပိတ်တဲ့အခါ list တစ်ခုလုံးကို overwrite mode နဲ့ save လုပ်ပါတယ်။ App စတဲ့အခါ students.csv ထဲမှာ line သုံးကြောင်း ရှိပါတယ်: Ana,91၊ Lee, Jo,85 နဲ့ Ben,78 ပါ။ App စတဲ့အခါ ဘယ်ကျောင်းသားတွေကို ပြမလဲ၊ user က grade 88 နဲ့ Cara ကို ထည့်ပြီး window ကို ပိတ်လိုက်တဲ့နောက် students.csv ထဲမှာ ဘာတွေ ရှိမလဲ ဆိုတာ Explain (ရှင်းပြပါ)။ ပြီးရင် record တစ်ခု ဒီလို မပျောက်အောင် app ကို ပြောင်းနိုင်တဲ့ မတူတဲ့ နည်း နှစ်ခုကို Describe (ဖော်ပြရှင်းပြပါ)။ Code ရေးဖို့ မလိုပါဘူး။

ဆက်စပ် ခေါင်းစဉ်များ

  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ်: Reading Files (file ထဲက ဒေတာကို ဖတ်ခြင်း) နဲ့ Writing Files (file ထဲကို ဒေတာ ရေးခြင်း) (B2.5.1): text file တွေကို ဖွင့်ခြင်း၊ ဖတ်ခြင်း၊ ရေးခြင်းနဲ့ ပိတ်ခြင်း
  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ်: MVC Architecture (MVC ဖွဲ့စည်းပုံ): ဒီစာမျက်နှာက save၊ load လုပ်တဲ့ Student နဲ့ StudentList Model class တွေ
  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ်: FXML and Controllers (FXML နဲ့ controller များ): ဒီစာမျက်နှာက load နဲ့ save ထည့်ပေးတဲ့ StudentController နဲ့ StudentApp
  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ် (Python): MVC with tkinter (tkinter နဲ့ MVC): Python ဥပမာတွေက load နဲ့ save ထည့်ပေးတဲ့ Model၊ View နဲ့ Controller file တွေ
  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ်: Encapsulation (ဒေတာနဲ့ method ကို တစ်စုတည်း ထုပ်ပိုးခြင်း): CSV line တွေ တည်ဆောက်ဖို့ getter (private attribute ရဲ့ တန်ဖိုးကို ပြန်ပေးတဲ့ method) တွေ
  • ဆက်စပ် ခေါင်းစဉ်: Exception Handling (program run နေစဉ် ဖြစ်တဲ့ အမှားတွေကို ကိုင်တွယ်ခြင်း): IOException တွေအတွက် try-catch
  • ဆက်စပ် ခေါင်းစဉ်: Multi-Window Apps (Window အများ ပါတဲ့ app များ) နဲ့ Multi-Window Apps with tkinter (tkinter နဲ့ window အများ ပါတဲ့ app များ): screen အများက Model တစ်ခုတည်းကို မျှသုံးလို့ ဘယ် screen ကမဆို Model ရဲ့ method တွေကနေ တစ်ဆင့် save လုပ်နိုင်ပါတယ်
  • ဆက်စပ် ခေါင်းစဉ်: JavaFX Cookbook: Files and persistence (file တွေနဲ့ ဒေတာ သိမ်းဆည်းခြင်း): dialog နဲ့ file ရွေးခြင်း၊ စာသားကို save၊ load လုပ်ခြင်းနဲ့ database ကို ပြောင်းသုံးခြင်း
  • နောက်ထပ် လေ့လာမယ့် ခေါင်းစဉ်: Structuring Your IA (IA ကို ဖွဲ့စည်း တည်ဆောက်ခြင်း): အစိတ်အပိုင်း အားလုံးကို ပေါင်းစပ်ခြင်း

© EduCS.me. ကွန်ပျူတာသိပ္ပံ ပညာရေးအတွက် အရင်းအမြစ်တွေ စုထားတဲ့ နေရာပါ။ EduCS is an independent educational resource. It is not affiliated with, or endorsed by, the International Baccalaureate Organization. (EduCS ဟာ သီးခြား ပညာရေး အရင်းအမြစ်တစ်ခုပါ။ International Baccalaureate Organization နဲ့ ဆက်စပ်မှု မရှိသလို သူတို့ရဲ့ ထောက်ခံချက်လည်း မရှိပါဘူး။) ဆက်သွယ်ရန်