見出し画像

リファクタリング的な作業(2)

(Python学習初心者の試行錯誤・備忘録です)
 素人が「MVCモデル」などと、知ったようなことを言うと、笑われてしまうかもしれません。ただ、基本的な考え方は、「DBの操作」と「画面の操作」と「このアプリ自体のロジック」とは分離する、と言うことだろうと解釈しています。それを意識しつつソースコードをバラしてみます。

MVC分離

 メインプログラムmvctest.py の外に、自作モジュールのファイル
M: mvctest_model.py
V: mvctest_view.py
C: mvctest_control.py
を用意して、sqlite3 などのDB操作はModelに、
GUIなど画面表示はViewに、
このアプリのロジックはControlの方に、と移していく。
メインからは、この3つのmoduleだけを経由する。原則それ以外のパッケージをimportせずに(つまりメインのコードでは import sqlite3 といったことをせず)操作することを念頭に整理してみます。

Model

Modelでは、データベースとのやり取りをまとめる。改めて見直すとテーブル設計の問題が気になってきました。そもそもは、これ

当初英単語学習ツールに発展させることは考えておらず、自分自身の中文発音暗記ツールとして構想し始めました。なので、テーブルが t_shengci となっています。(中文学習で新登場の単語を示す生詞(shēng cí)より)要素名も、hanzi (漢字)とpinyin(ピンイン)だったりします。

CREATE TABLE "t_shengci" (
	"id"	INTEGER,
	"hanzi"	TEXT NOT NULL,
	"pinyin" TEXT,
	"level"	INTEGER DEFAULT 0,
	"timestamp"	TEXT DEFAULT CURRENT_TIMESTAMP,
	PRIMARY KEY("id" AUTOINCREMENT)
);

あとから「これ、漢字⇒英単語、ピンイン⇒訳語、に置き換えてもアプリとして成り立つよね!」と思いついて、そちらを主にしましたが、要素名まで、hanzi、pinyinそのまま。
 今回、「モデル」を切り分けることを意識したところで、テーブル名も要素名も「直さなきゃ」という気になってきました。
 「汎用性」のある名前ならどういうのが良いでしょう? 中文とか英文とか関係なく、「単語カード」みたいなものだとすると、単語(word) と訳(translation)でしょうか。でも「クイズ」と「答え」でもいいし・・。
 まあ、「暗記カード」をイメージしているから「表:face」と「裏:back」が良いかな。

 CREATE TABLE IF NOT EXISTS t_cards (
        id INTEGER PRIMARY KEY,
        face TEXT NOT NULL,
        back TEXT,
        level INTEGER DEFAULT 0,
        timestamp TEXT DEFAULT CURRENT_TIMESTAMP)

これに決めました。

クラス

今回、Pythonでのクラス・オブジェクト指向初挑戦です。自分は、オブジェクト指向の考え方自体は、昔 Delphi(ObjectPascal)で使ったことはありますが、Pythonでははじめて。Youtube でPythonVtuberサプーさんの動画を見て概要把握しました。

結構、Delphiとは流儀が違うんだなぁ・・。定義していないメンバ変数を、実行時にインスタンス側で追加できちゃう(というか、イニシャライザでメンバ変数を作っているのでそれしかない。)点、面白いです。

 では、カードクラスというのを作ってみます。
 つい数日前知ったばかりのノウハウ「テーブルが無い時には自動作成する」というのを組み込む。あとデータベース名、テーブル名、もイニシャライズ時に変えられるようにしておく。

class Cards:
    #データベースのt_cardテーブル
    def __init__(self, mydb="mydb.sqlite3", table="t_card") -> None:
        self.mydb = mydb
        self.table =table
        createsql = """ CREATE TABLE IF NOT EXISTS ? (
                    id INTEGER PRIMARY KEY,
                    face TEXT NOT NULL,
                    back TEXT,
                    level INTEGER DEFAULT 0,
                    timestamp TEXT DEFAULT CURRENT_TIMESTAMP)
                    """
        with sqlite3.connect(mydb) as con:
            cur = con.cursor()
            cur.execute(createsql,table)
            con.commit()

と思ったらエラー。例によってCopilot先生に教えてもらいます。

SQLiteでは、SQL文の中でテーブル名やカラム名にプレースホルダ(?)を使用することはできません。プレースホルダは主にデータ値(例えば、INSERTUPDATE文の中の値)に使用されます。テーブル名やカラム名に動的な値を使用したい場合は、文字列のフォーマット機能や文字列結合を使ってSQL文を作成する必要があります。

以下のようにコードを修正してみてください:
(コード例:略)
この修正では、f-stringを使ってテーブル名をSQL文に直接埋め込んでいます。これにより、動的にテーブル名を変更することができます。ただし、この方法はSQLインジェクションのリスクを高める可能性があるため、table変数に信頼できる値のみが入ることを確認するか、適切なサニタイズ処理を行うことが重要です。安全なコードを書くためには、ユーザーからの入力を直接SQL文に組み込むことは避け、事前に定義された値や安全な方法でのみテーブル名を指定するようにしてください。

Copilotの回答より

なるほど、書き方を変えればできることはできるが、リスクを高める可能性があると。ならば無理せず、テーブル名は固定でいいです。

class Cards:
    #データベースのt_cardテーブル
    def __init__(self, mydb="mydb.sqlite3") -> None:
        self.mydb = mydb
        self.table ="t_cards"
        createsql = """ CREATE TABLE IF NOT EXISTS t_cards (
                    id INTEGER PRIMARY KEY,
                    face TEXT NOT NULL,
                    back TEXT,
                    level INTEGER DEFAULT 0,
                    timestamp TEXT DEFAULT CURRENT_TIMESTAMP)
                    """
        with sqlite3.connect(mydb) as con:
            cur = con.cursor()
            cur.execute(createsql)
            con.commit()

テストコードで

#テストコード            
def main():
    mycards1 = Cards()
    mycards2 = Cards("db2.sqlite3")

if __name__  == "__main__":
    main()

これをやってみたところ、無事 mydb.sqlite3 とdb2.sqlite3 が自動作成され、テーブルもできていました。

ひとまず一段落。

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