アジョブジ星通信

日常系バンザイ。

MySQL-Pythonの正しい使い方

img.azyobuzi.net の開発が予想以上に遅れてます、 azyobuzin です。本当は、 img.azyobuzi.net の新バージョンを完成させてから書こうと思っていた記事ですが、諦めて書いてしまいます。

さて、今日は個人的に今まで間違った(?)使い方をしていた MySQL-Python(以下 MySQLdb)のお話をしようと思います。

前提事項

  • Python 2.7
  • MySQLdb 1.2.4(?)

ここに書いたコードたちは未テストです。感覚だけで書いたので、臨機応変にお願いします。

今までの書き方

昔の img.azyobuzi.net や TbrFeed のソースコードを見ればわかると思いますが、こんな書き方をしていました。

import MySQLdb

#接続
db = MySQLdb.connect(user="hoge", passwd="bar", db="booboo", charset="utf8")
c = db.cursor()

#INSERT
c.execute("INSERT INTO %s (id, username, token, secret, created, lastaccess) VALUES (%s, %s, %s, %s, NOW(), NOW()) ON DUPLICATE KEY UPDATE token = %s, secret = %s, created = NOW(), lastaccess = NOW()"
    % (usersTable, db.literal(id), db.literal(username), db.literal(oauthToken), db.literal(oauthTokenSecret), db.literal(oauthToken), db.literal(oauthTokenSecret)))

db.commit()

c.close()
db.close()

db.literal が大量にあってウザいですね。さて、ここから無駄なところを削り、清潔な Python コードにしていきます。

1. with で接続を管理

まず、 with の挙動を確認していきます。 with 文は __enter__() を呼び出して、その引数を as のあとの変数にいれ、ブロックの最後に __exit__(self, exc, value, tb) を呼び出して終了します。そして、 MySQLdb の Connection の実装では、

def __enter__(self): return self.cursor()
    
def __exit__(self, exc, value, tb):
   if exc:
        self.rollback()
   else:
        self.commit()

となっています。
つまり、

with MySQLdb.connect(user="hoge", passwd="bar", db="booboo", charset="utf8") as c:
    #ポイント: c は Cursor のインスタンスです。間違えやすい。
    pass

としてやれば、接続後、勝手に Cursor を取得し、 with 文終了時点で勝手にコミットしてくれるので、 close()commit() も必要ないわけですね!*1

2. Connection.literal にさよならバイバイ

最初のコードに、 db.literal(o) がたくさん出て来ましたが、そもそもこれ、ドキュメントには

Non-standard. For internal use; do not use this in your applications.

MySQLdb API Documentation

と書かれています。

では、どうやってエスケープするのか?答えは Cursor.execute(query, args) です。内部には

query = query % db.literal(args)

というコードがあります。つまり、 query文字列フォーマットで指定しておき、 args にタプルを指定すれば、自動的にエスケープされた文字列が挿入されるようです。

まとめ

最初のコードはこんな感じに書きなおすことができます。

import MySQLdb

#接続
with MySQLdb.connect(user="hoge", passwd="bar", db="booboo", charset="utf8") as c
    #INSERT
    c.execute("INSERT INTO " + usersTable + " (id, username, token, secret, created, lastaccess) VALUES (%s, %s, %s, %s, NOW(), NOW()) ON DUPLICATE KEY UPDATE token = %s, secret = %s, created = NOW(), lastaccess = NOW()",
        (id, username, oauthToken, oauthTokenSecret, oauthToken, oauthTokenSecret))

ちょっと MySQLdb を見直してしまった。

*1:close() に関して:Connection.close() はそもそも API ドキュメントに載っていないですし、 Cursor.close() も内部では Connection のインスタンスを捨てているだけなので、たぶん必要ないと思われます。