見出し画像

機能追加(4)データベースのバックアップ

(Python学習初心者の試行錯誤・備忘録です)
 ここまでのところ、新しい教材(単語リスト)をCSVで読み込むと、テーブルにあるそれ以前のデータを削除してから新しい単語リストを読み込む処理にしています。

mvctest_controller.pyより

#抜粋
class Controller:
    def loadcsv(self, csvfilename:str) -> None:
        self.mycards.clear_table()
        self.mycards.loadfromcsv(csvfilename)

mvctest_model.pyより

#抜粋
class Cards:
    def clear_table(self)-> None:
        with sqlite3.connect(self.mydb) as con:
            cur = con.cursor()
            cur.execute(f"DELETE FROM {self.table}")
            con.commit()

    def loadfromcsv(self, csvfilename:str) -> None:
        with sqlite3.connect(self.mydb) as con:
            cur = con.cursor()
            with open(csvfilename, newline='', encoding='utf-8') as csvfile:
                csvreader = csv.reader(csvfile, delimiter = ",")
                for row in csvreader:
                    con.execute(f"INSERT INTO {self.table}(face, back) VALUES(?,?)",
                                 (row[0],row[1]))
            con.commit()

これだと、複数の教材を切り替えて利用することができない。もちろん、CSVファイルの「読み直し」はできますが、暗記学習の進捗状況が失われて、全部レベル0に戻ってしまいますから、学習者の「やる気」を削いでしまいます。
 教材が第一課、第二課・・・とあるときに第二課の読み込みをすると第一課の進捗が消える。このため「100%記憶定着」と確信するまで、次の課に、進めない。まあ「100%記憶定着」なんて理想論ですから、結局、躊躇してしまって次の課の教材に進めない。これじゃあ、学習アプリとしては致命的です。
 「教材と進捗状況」を保持しているDBを簡単に切り替えられるような仕組みを用意したい。公式のDBバックアップの説明を見ると

(データベースでの)ページ数って何だ?まあ、気にせずデフォルト(-1)のままで、ワンステップコピーにしておけばいいか。

まず、見た目から

 アプリの機能をイメージするためにも、まずは「VIEW」をいじる。
mvctest_view.py の メニュー設定部分で

        menu_def = [
            #['ファイル',['読み込み',['CSV::LoadCSV'],  '---', 'Exit']],
            #上の記法はTkEasyGUI用
            ['ファイル',['読み込み',['LoadCSV'],  '---', 'Exit']],
            ['データベース',['新規DB作成',['NewDB'], 
                       '名前を付けて保存',['SaveDB'],
                       '接続先を変更',['ConnectDB']]],
         ]

今接続しているDB教材と別教材を用意するには「新規DB作成」。
今DB教材の内容を別名で保存するには「名前を付けて保存」。
複数あるDBを切り替える際は「接続先を変更」

としました。機能はまだ実装されていませんが、どんな操作感になるのか、まず見た目から作ってイメージします。

機能の実装

DBのSAVE機能

VIEWクラスに popup_dbsave()という関数を追加、ポップアップでファイル保存ダイアログを開くようにします。

    def popup_dbsave(self):
        # ファイル保存ダイアログを開く
        dbfilename = eg.popup_get_file(
            '保存するファイル名を入力してください',
            save_as=True,
            default_extension='.sqlite3',
            file_types=(('sqlite3 Files', '*.sqlite3'), ('db Files', '*.db'), ('All Files', '*.*')),
        )
        if dbfilename:
    # ファイル名を表示
            eg.popup(f'保存するファイル名: {dbfilename}')
            return(dbfilename)
        else:
            eg.popup('DBファイル保存がキャンセルされました')
            return(None)

Controllerクラスにdbsave()と言う関数を追加

    def dbsave(self,filename):
        with sqlite3.connect(self.mycards.mydb) as src:
            with sqlite3.connect(filename) as dst:
                src.backup(dst)

メインのイベント処理部にSaveDBのイベント追加

    elif event == "SaveDB":
        popup_result = myview.popup_dbsave()
        if popup_result is not None:
            mycontroller.dbsave(popup_result)

テストしてみると、無事今のDB内容、複製保存に成功しました。

DBファイルの切り替え

DBファイルを複数作れるようになったので、「切り替え」ができるようにする必要があります。今は、メインのコードで
mycontroller = Controller()
と、コントローラーのインスタンスを作成したときにイニシャライザで
self.mycards = Cards()
とCardsモデルのインスタンスが作成され、そちらのイニシャライザで

class Cards:
    #データベースのt_cardテーブル
    def __init__(self, mydb="mydb.sqlite3",table="t_cards") -> None:
        self.mydb = mydb
        self.table = table
        #テーブル t_cards がなければ作成する。
        self.createsql = f""" CREATE TABLE IF NOT EXISTS {self.table} (
                    id INTEGER PRIMARY KEY,
                    face TEXT NOT NULL,
                    back TEXT,
                    level INTEGER DEFAULT 0,
                    timestamp TEXT DEFAULT CURRENT_TIMESTAMP)
                    """
        self._create_table()

    def _create_table(self):
        with sqlite3.connect(self.mydb) as con:
            cur = con.cursor()
            cur.execute(self.createsql)
            con.commit()

と、DBファイル"mydb.sqlite3" の中のテーブル"t_cards" をデフォルトとする流れになっています。
 別のDBと接続しなおせるようにCardsクラスに次の関数を追加します。

    def reinitialize(self,mydb="mydb.sqlite3",table="t_cards") -> None:
        self.__init__(mydb,table)

VIEWクラスに popup_dbconnect()という関数を追加

    def popup_dbconnect(self):
        # ファイル接続ダイアログを開く
        dbfilename = eg.popup_get_file(
            '接続するDBファイル名を入力してください',
            default_extension='.sqlite3',
            file_types=(('sqlite3 Files', '*.sqlite3'), ('db Files', '*.db'), ('All Files', '*.*')),
        )
        if dbfilename:
    # ファイル名を表示
            eg.popup(f'接続するDBファイル名: {dbfilename}')
            return(dbfilename)
        else:
            eg.popup('DBファイル切替がキャンセルされました')
            return(None)   

Controllerクラスにdbconnect()と言う関数を追加

    def dbconnect(self,filename):
        self.mycards.reinitialize(filename)

メインのイベント処理部にConnectDBのイベント追加

    elif event == "ConnectDB":
        popup_result = myview.popup_dbconnect()
        if popup_result is not None:
            mycontroller.dbconnect(popup_result)
        mycontroller.pickupcard(value["-LEVEL-"])    
        mycontroller.refresh_view(myview)

テストしてみると、無事DBの切り替えに成功しました。ただ、こうなると、今どのデータベースファイルを対象としているのかが分かりにくいので、画面に表示するようにしましょう。
Viewでレイアウトに

[eg.Text("接続DB:"), eg.Text("dbname",font = btfont,key="-TEXT_DBNAME-")],

を追加、Controllerの refresh_view関数に

 myview.window["-TEXT_DBNAME-"].update(self.mycards.mydb)

を追加。

新規DB作成

VIEWクラスに popup_dbnew()という関数を追加

    def popup_dbnew(self):
        # ファイル保存ダイアログを開く
        dbfilename = eg.popup_get_file(
            '新しいDBファイル名を入力してください',
            save_as=True,
            default_extension='.sqlite3',
            file_types=(('sqlite3 Files', '*.sqlite3'), ('db Files', '*.db'), ('All Files', '*.*')),
        )
        if dbfilename:
    # ファイル名を表示
            eg.popup(f'新しいDBファイル名: {dbfilename}')
            return(dbfilename)
        else:
            eg.popup('DBファイル作成がキャンセルされました')
            return(None)    

Controllerクラスにdbnew()と言う関数を追加しようと思ったけどdbconnect と同じなので要らなかったです。

メインのイベント処理部にNewDBのイベント追加

    elif event == "NewDB":
        popup_result = myview.popup_dbnew()
        if popup_result is not None:
            mycontroller.dbconnect(popup_result)
        mycontroller.pickupcard(value["-LEVEL-"])    
        mycontroller.refresh_view(myview) 

一発で期待通りの動作をしてくれました。

その他試用した人の要望に合わせた機能も付けました。
 学習がいったん終了した後でまた復習したい場合など、「CSVファイルを読み直す」のは操作として、おかしいという指摘あり。
 それに、2回目はまた「習熟レベル0」からではなくて、レベル1とか2とかあたりからはじめたい。と言う要望もあり。
 そういう要望にもこたえて、レベルを好きな段階でリセットできる機能をつけました。

あと、単語を詳しく調べたいとき、外部のブラウザでウェブ辞書サイトを開くような設定も付けました。

 英単語学習用途以外にも、そのまま、私自身の台湾華語発音暗記ツールとしても使えています!

 教材としてはTOCFLのサイトで公開されているレベル別単語集を使ってみました。

 辞書サイト選択肢にあるmoeは台湾教育部の公開している《重編國語辭典修訂本》のサイトです。Web辞書ボタンをクリックすると、別ブラウザでその単語の説明が開きます。

感想

 今まで、コードが少し長くなると、新しい機能を追加するのは、どうにもしんどかったのです。MVCモデル(?)でリファクタリングしてみると、どこいじれば、どうなるかが明確になり、すごく楽だなと実感しました。
 ここまでの機能拡張、もし、最初に作った1ファイル中「一緒くた」のコードに書き足していったらきっと、ごちゃごちゃになって収拾がつかなくなっていたと思います。

この「最初」の時に期待していた以上の成果(経験値)だったと自分では思っています。


この記事が気に入ったらサポートをしてみませんか?