Blow Up by Black Swan

mysql-connector-pythonの使い方(MySQL Connector/Python Developer Guideの意訳)

今回は、PythonのMySQLドライバ、mysql-connector-pythonについて公式ドキュメントに沿って一通り学習したので、それを公開したいと思います。ただ、私もまだまだ使い始めたばかりでまとめきれていないところもあるので、今回は私の学習ノートを公開するような形にしたいと思います。このドライバは、MySQLの公式ドライバで最新のPythonバージョン(3.7.0)にも対応し、英語圏のサイトで調べてもおすすめ(というか無難なドライバとして?)で挙げられていることが多いです。

しかし、日本語サイトではmysqlcliantなどに比べると情報量が少ないのかなという印象を受けるので、英語の公式ドキュメントを意訳した私の学習ノートでも参考になる方はいるのかなと思い、公開しようと思いました。

上記のような経緯で公開しますので、基本箇条書きになっています。また、章立てについては、公式ドキュメントのまんまですが、基本的に必要だと思われるところだけしかまとめていませんので、リリースノートやリーガルノーティスなど、不必要だと判断したものについては除外しており、かなりいびつな章立てになっています。

今回の学習環境は以下になります。

ちなみに私がPythonのモジュール等を学習する場合、jupyter(labの方)を利用し、説明などの部分はマークダウンを利用しています。リンクなども簡単に挿入できるので、かなりおすすめです。

Chapter1 Introduction to MySQL Connector/Python

[MySQL Connector/Pythonの特徴]

  • PythonプログラムがMySQLデータベースすることを可能とするもの
  • DB API2.0に準拠
  • 純粋にPythonのみで書かれており、標準ライブラリ以外に依存関係を持たない(自己充足的なライブラリ)
  • PythonとMySQL間でデータ型を相互に変換する
  • 標準的なSQLシンタックスを拡張
  • データ圧縮のプロトコルを使用
  • SSLを使用して、安全なTCP/IPコネクションを確立

Chapter2 Guidelines for Python Developers

[MySQLアプリケーションの開発に関する助言]

  • セキュリティ上、データベースに接続しログインするためのデータを、メインスクリプトの中に書いてはならない。Pythonでは、config.pyという設定ファイルに設定上のコードを記載する慣習をもつ。
  • Pythonスクリプトは、大抵、利用できるRAMの限界まで、メモリー内でデータ構造の構築と破壊を行う。MySQLは、大抵、利用できるメモリー以上に大きなデータセットを何回も取り扱うので、ストレージスペースやディスクI/Oを最適化する技術は重要になる(例:IDを文字列でなく数字で設定する)
  • インプットやデータベースにアクセスするいかなるアプリケーションも、バッドデータを処理する可能性があることを予測しておかなければならない。
    • 範囲外のデータや形式の異なる文字列のようにバッドデータは偶然によるものかもしれないが、アプリケーションは、バッドデータがデータベースに到達しないようにするために、NOT NULLuniqueといった制限をかける文法などの、サーバサイドチェックの仕組みを利用することができる。クライアントサイドでは、問題をリポートし、是正処置をとるための例外処理などの対応を利用する。
    • バッドデータは、SQLインジェクションのように故意の場合もある。期待される文字列だけを持つことを確認するために、インプットデータを検証するべき。意図した振る舞いを変える文字列が投稿された時は、それを避けるようにする。検証したり、エスケープしたりせずに、SQL文にユーザーインプットデータを繋げるようなことはしない。他のプログラムによって生成されたインプットデータを受け入れる時でさえも、そのプログラムが汚染され、不正であったり悪意のあるデータを送る可能性を想定せよ。
  • SQLクエリから得られるデータはとても巨大であるため、ループなどのように結果一式からアイテムを引き出す、適切な方法を利用せよ。
    • fetchone()は、一つのアイテムを取り出すもので、帰ってくる値が一つのrowを含むことがわかっている時に使う。
    • fetchall()は、全てのアイテムを取り出し、帰ってくる値のサイズがメモリーに適切にフィットする限定された数のrowを持つ時に利用する。
    • fetchmany()は、汎用性のあるもので、帰ってくる値のサイズがわからない時に利用する(関数を呼び続け、処理する結果がなくなるまで、ひたすら繰り返す)。
  • pythonは、PicklecPickleのような、ディスク上でデータを読んだり、書いたりするための便利なモジュールを持つので、MySQLの中で保存することを選んだデータは特別な特徴を持つ可能性が高い。
    • 大きすぎてメモリーの中で一度に適合させることができない。必要とする正確なアイテムだけ要求するためにSELECT文を使い、複数のアイテムを計算するために集合機能(Group by)を使う。クエリ結果のキャッシュ用に一定量のRAMを充てるためにinnodb_buffer_pool_sizeオプションをMySQLサーバ内に設定する
    • 複雑すぎて、一つのデータ構造だけでは表すことができない。データを異なるSQLテーブルで分ける。joinクエリを使ってデータを複数のテーブルから再結合することができる。foreighn keyリレーションシップを設定することで、関連データが異なるテーブル間で同時に保持されるようにする。
    • 頻繁に、場合によっては複数のユーザによって一斉にアップデートされる。アップデートは、毎回構造全体を記述するという無駄なことを行わせ、結果的に少しのデータにだけしか影響を与えないかもしれない。異なるアイテムを同時にアップデートするために、INSERTUPDATEDELETE文を使い、ディスクには変更されるデータだけ書き込む。記載されたプログラムがお互いに反目しあわないようにし、基本的なデータがアップデートされて利用な時でさえも一貫したクエリ結果を返すようにするために、InnoDBテーブルとトランザクションを使え。
  • MySQLのベストプラクティスを実践することは、主要部分のリライトを行ったり、アーキテクチャを変更したりせずにアプリケーションがスケールするのを手助けする。ベストプラクティス関するページ
  • 一般的なオペレーション向けのMySQL文を学ぶことによって、車輪の再発明を避けることができる:クエリでつかうオペレーター、まとまったローディングデータ向けのテクニックなど。主要なステートメントクラス:データ操作データ定義SELECT文
  • PythonからSQL文を実行するには、一般的にかなり冗長で複数行に渡る文字列が伴う。SQL文内の文字列はシングルクオーテーションやダブルクオーテーションで囲んだりできるため、シンプルにするためにPythonのトリプルクオーテーションのメカニズムを利用するとよい。
  • 早く、スケーラブルなMySQLアプリケーションへの秘訣の多くは、セットアップ時点での手順の開始時に正しいシンタックスを使うことが関係している(エンジンにINNODBを使う、それぞれのテーブル用に数字のprimary keyを宣言する、絡むのデータタイプにはアプリケーションに適合する物を使用する、など)。

Chapter4 Connector/Python Installation

インストールにはpipを利用

$ pip install mysql-connector-python

Chapter5 Connector/Python Coding Examples

5.1 Connectiong to MySQL Using Connector/Python

  • ※ 利用にはimport mysql.connectorが必要
  • MySQLサーバとのコネクションを確立する方法
    1. connect()コンストラクタの使用(クラス分の__init__()のやつ)…推奨される。関連ページ:Section 7.1, “Connector/Python Connection Arguments”
    2. connection.MySQLConnection()クラスの利用。機能的に1と等しいが非推奨
  • コネクション時のエラーの取り扱い
  • 備考: コネクションの引数にアスタリスク(*)演算子2つによる辞書による定義も可能
    <コネクションの確立①(connect()コンストラクタの利用、2は省略)>
import mysql.connector
cnx = mysql.connector.connect(user='scott',  #引数は適宜設定  
                              password='password',
                              host='127.0.0.1')
cnx.close()

<引数に辞書データを利用した場合>

import mysql.connector

config = {
  'user': 'sipper',
  'password': 'sql',
  'host': '172.20.0.2',
  'raise_on_warnings': True}

cnx = mysql.connector.connect(**config)  #アスタリスク2つで辞書形式で関数に引数を渡せる
cnx.close()<エラーの取り扱い>
import mysql.connector
from mysql.connector import errorcode

try:
  cnx = mysql.connector.connect(user='scott',
                                host='127.0.0.1',
                                password='password',
                                database='employees')
except mysql.connector.Error as err:
  if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
    print("Something is wrong with your user name or password")
  elif err.errno == errorcode.ER_BAD_DB_ERROR:
    print("Database does not exist")
  else:
    print(err)
else:
  cnx.close()

5.2 Creating Tables using Connector/Python

  • 全てのデータ定義言語(DDL)は、cursorという構造を使って実行される。
  • MySQLサーバ一つで複数のデータベースを管理できる。通常、接続時にスイッチを入れるデータベースを指定する
  • 下記のコード例で示していること
    • 辞書構造の変数TABLEの中でCREATE文を保存するする方法
    • 複数のデータベースの構築
  • 下記コード例の流れ
    1. 辞書構造の変数TABLESとグローバル変数DB_NAMEを定義
    2. ConnectionオブジェクトからのCursorオブジェクトの生成
    3. 特定のデータベースに切り替え
    4. エラーが発生したら、エラーコードからデータベースが存在する確認
    5. 存在しなければデータベースを構築(create_database())
    6. 他のエラーであれば、アプリケーションを終了し、エラーメッセージを移す
    7. 辞書データのTABLESのアイテムを繰り返すことでテーブルを作成(※通常はIF NOT EXISTS文を使う)
    8. テーブルが存在するというエラーが出た場合は文字列を表示
    9. そうでない場合のエラーはエラーメッセージを表示
from __future__ import print_function

import mysql.connector
from mysql.connector import errorcode

#1.辞書構造の変数TABLESとグローバル変数DB_NAMEを定義
DB_NAME = 'employees'

TABLES = {}
TABLES['employees'] = (
    "CREATE TABLE `employees` ("
    "  `emp_no` int(11) NOT NULL AUTO_INCREMENT,"
    "  `birth_date` date NOT NULL,"
    "  `first_name` varchar(14) NOT NULL,"
    "  `last_name` varchar(16) NOT NULL,"
    "  `gender` enum('M','F') NOT NULL,"
    "  `hire_date` date NOT NULL,"
    "  PRIMARY KEY (`emp_no`)"
    ") ENGINE=InnoDB")

TABLES['departments'] = (
    "CREATE TABLE `departments` ("
    "  `dept_no` char(4) NOT NULL,"
    "  `dept_name` varchar(40) NOT NULL,"
    "  PRIMARY KEY (`dept_no`), UNIQUE KEY `dept_name` (`dept_name`)"
    ") ENGINE=InnoDB")

TABLES['salaries'] = (
    "CREATE TABLE `salaries` ("
    "  `emp_no` int(11) NOT NULL,"
    "  `salary` int(11) NOT NULL,"
    "  `from_date` date NOT NULL,"
    "  `to_date` date NOT NULL,"
    "  PRIMARY KEY (`emp_no`,`from_date`), KEY `emp_no` (`emp_no`),"
    "  CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) "
    "     REFERENCES `employees` (`emp_no`) ON DELETE CASCADE"
    ") ENGINE=InnoDB")

TABLES['dept_emp'] = (
    "CREATE TABLE `dept_emp` ("
    "  `emp_no` int(11) NOT NULL,"
    "  `dept_no` char(4) NOT NULL,"
    "  `from_date` date NOT NULL,"
    "  `to_date` date NOT NULL,"
    "  PRIMARY KEY (`emp_no`,`dept_no`), KEY `emp_no` (`emp_no`),"
    "  KEY `dept_no` (`dept_no`),"
    "  CONSTRAINT `dept_emp_ibfk_1` FOREIGN KEY (`emp_no`) "
    "     REFERENCES `employees` (`emp_no`) ON DELETE CASCADE,"
    "  CONSTRAINT `dept_emp_ibfk_2` FOREIGN KEY (`dept_no`) "
    "     REFERENCES `departments` (`dept_no`) ON DELETE CASCADE"
    ") ENGINE=InnoDB")

TABLES['dept_manager'] = (
    "  CREATE TABLE `dept_manager` ("
    "  `dept_no` char(4) NOT NULL,"
    "  `emp_no` int(11) NOT NULL,"
    "  `from_date` date NOT NULL,"
    "  `to_date` date NOT NULL,"
    "  PRIMARY KEY (`emp_no`,`dept_no`),"
    "  KEY `emp_no` (`emp_no`),"
    "  KEY `dept_no` (`dept_no`),"
    "  CONSTRAINT `dept_manager_ibfk_1` FOREIGN KEY (`emp_no`) "
    "     REFERENCES `employees` (`emp_no`) ON DELETE CASCADE,"
    "  CONSTRAINT `dept_manager_ibfk_2` FOREIGN KEY (`dept_no`) "
    "     REFERENCES `departments` (`dept_no`) ON DELETE CASCADE"
    ") ENGINE=InnoDB")

TABLES['titles'] = (
    "CREATE TABLE `titles` ("
    "  `emp_no` int(11) NOT NULL,"
    "  `title` varchar(50) NOT NULL,"
    "  `from_date` date NOT NULL,"
    "  `to_date` date DEFAULT NULL,"
    "  PRIMARY KEY (`emp_no`,`title`,`from_date`), KEY `emp_no` (`emp_no`),"
    "  CONSTRAINT `titles_ibfk_1` FOREIGN KEY (`emp_no`)"
    "     REFERENCES `employees` (`emp_no`) ON DELETE CASCADE"
    ") ENGINE=InnoDB")
    
#2.connectionオブジェクトからのcursorオブジェクトの生成
cnx = mysql.connector.connect(user='scott')
cursor = cnx.cursor()

def create_database(cursor):
    try:
        cursor.execute(
            "CREATE DATABASE {} DEFAULT CHARACTER SET 'utf8'".format(DB_NAME))
    except mysql.connector.Error as err:
        print("Failed creating database: {}".format(err))
        exit(1)

#3.特定のデータベースに切り替え
try:
    cursor.execute("USE {}".format(DB_NAME))
#3-A.エラーが発生したら、エラーコードからデータベースが存在するか確認
except mysql.connector.Error as err:
    print("Database {} does not exists.".format(DB_NAME))
    if err.errno == errorcode.ER_BAD_DB_ERROR:
       #3-B.存在しなければデータベースを構築
        create_database(cursor)
        print("Database {} created successfully.".format(DB_NAME))
        cnx.database = DB_NAME
    #3-C.他のエラーであれば、アプリケーションを終了し、エラーメッセージを移す
    else:
        print(err)
        exit(1)

#4.辞書データのTABLESのアイテムを繰り返すことでテーブルを作成
for table_name in TABLES:
    table_description = TABLES[table_name]
    try:
        print("Creating table {}: ".format(table_name), end='')
        cursor.execute(table_description)
    except mysql.connector.Error as err:
        #4-A.テーブルが存在するというエラーが出た場合は文字列を表示
        if err.errno == errorcode.ER_TABLE_EXISTS_ERROR:
            print("already exists.")
        #4-B.そうでない場合のエラーはエラーメッセージを表示
        else:
            print(err.msg)
    else:
        print("OK")

cursor.close()
cnx.close()

5.3 Inserting Data Using Connector/Python

  • データの挿入、アップデートには、ハンドラー構造のcursorを使用する
  • MySQL5.5以上からデフォルトの処理保存エンジンのInnoDBなどのエンジンを使う場合、INSERTDELETEUPDATE文の後には処理を完了させるcommit()を行う必要がある
  • このコード例が示していること
    • 新しいデータの挿入方法
    • 2番目のINSERTは新しく作られる最初のINSERT文のprimary keyの値に依存する
    • コード例内容:明日から5万のサラリーで働き始める労働者の追加
  • 下記コード例の流れ
    1. ConnectionオブジェクトからのCursorオブジェクトの生成
    2. INSERT文を変数add_employeeadd_salaryに格納(※新しいろう者情報はdata_employee変数に格納されている)
    3. 労働者情報の挿入
    4. Cursorオブジェクトのlastrowid属性を使い、emp_no変数に追加した労働者のIDを格納
    5. 新しい労働者のサラリー情報を挿入
    6. 挿入内容をcommit()
from __future__ import print_function
from datetime import date, datetime, timedelta
import mysql.connector

#1.ConnectionオブジェクトからのCursorオブジェクトの生成
cnx = mysql.connector.connect(user='scott', database='employees')
cursor = cnx.cursor()

tomorrow = datetime.now().date() + timedelta(days=1)

#2.INSERT文を変数add_employeeとadd_salaryに格納
add_employee = ("INSERT INTO employees "
               "(first_name, last_name, hire_date, gender, birth_date) "
               "VALUES (%s, %s, %s, %s, %s)")
add_salary = ("INSERT INTO salaries "
              "(emp_no, salary, from_date, to_date) "
              "VALUES (%(emp_no)s, %(salary)s, %(from_date)s, %(to_date)s)")

data_employee = ('Geert', 'Vanderkelen', tomorrow, 'M', date(1977, 6, 14))

#3.労働者情報の挿入
cursor.execute(add_employee, data_employee)

#4.Cursorオブジェクトのlastrowid属性を使い、emp_no変数に追加した従業員のIDを格納
emp_no = cursor.lastrowid

#5.新しい労働者のサラリー情報を挿入
data_salary = {
  'emp_no': emp_no,
  'salary': 50000,
  'from_date': tomorrow,
  'to_date': date(9999, 1, 1),
}
cursor.execute(add_salary, data_salary)

#6.挿入内容をcommit()
cnx.commit()

cursor.close()
cnx.close()

5.4 Querying Data using Connector/Python

  • このコード例で示していること
    • Cursorオブジェクトを使ってデータを要求する方法
    • タスク:1999年に雇用された労働者の名前、雇用データをコンソールに表示する
  • 下記コード例の流れ
    1. ConeectionオブジェクトからCursorオブジェクトを生成
    2. 変数querySELECT文を格納
    • mysql-connector-pythonでPythonのデータ型をMySQLが理解できるデータ型に変換し、要求された箇所にその値を加えている(この場合では、PythonのdatetimeオブジェクトをMySQLのdate型に変換し、一つ目の%s1999-01-01を、二つ目の%s1999-12-31を挿入している)
    1. クエリの実行
    • %sマーカーに挿入する値はタプルで渡される
    1. 結果の出力
    • MySQLのdate型がPythonのdatetime型に自動で変換されている
import datetime
import mysql.connector

#1.ConeectionオブジェクトからCursorオブジェクトを生成
cnx = mysql.connector.connect(user='scott', database='employees')
cursor = cnx.cursor()

#2.変数queryにSELECT文を格納
query = ("SELECT first_name, last_name, hire_date FROM employees "
         "WHERE hire_date BETWEEN %s AND %s")
hire_start = datetime.date(1999, 1, 1)
hire_end = datetime.date(1999, 12, 31)

#3.クエリの実行
cursor.execute(query, (hire_start, hire_end))

#4.結果の出力
for (first_name, last_name, hire_date) in cursor:
  print("{}, {} was hired on {:%d %b %Y}".format(
    last_name, first_name, hire_date))

cursor.close()
cnx.close()

Chapter6 Connector/Python Tutorials

6.1 Tutorial: Raise Employee’s Salary Using a Buffered Cursor

  • 2000年に雇用され、まだ勤めている、全ての労働者の給料を明日付で15%昇級する
  • 緩衝材となるcursorを利用し、これで新しい変数でロウを取得する必要がなくなる。このcursorはイテレーターとして利用される
    • buffered rowによってサーバから直接ロウを取得するのでなく、一旦Cursorオブジェクトの中にデータが格納され、そこから取り出される形になる(buffered row出ない、通常の場合、ロウを取得するメソッド(fetchone()など)は、実行されるごとにサーバにアクセスし、情報を取得する)
    • 関連ページ: Section 10.6.1, “cursor.MySQLCursorBuffered Class”
from __future__ import print_function

from decimal import Decimal
from datetime import datetime, date, timedelta

import mysql.connector

# MySQLサーバに接続
cnx = mysql.connector.connect(user='scott', database='employees')

# buffered cursorの取得
curA = cnx.cursor(buffered=True)
curB = cnx.cursor(buffered=True)

# 特定の期間に加入した労働者情報取得のためのクエリ
query = (
  "SELECT s.emp_no, salary, from_date, to_date FROM employees AS e "
  "LEFT JOIN salaries AS s USING (emp_no) "
  "WHERE to_date = DATE('9999-01-01')"
  "AND e.hire_date BETWEEN DATE(%s) AND DATE(%s)")

# 新旧の給料用のUPDATEとINSERT文
update_old_salary = (
  "UPDATE salaries SET to_date = %s "
  "WHERE emp_no = %s AND from_date = %s")
insert_new_salary = (
  "INSERT INTO salaries (emp_no, from_date, to_date, salary) "
  "VALUES (%s, %s, %s, %s)")

# 昇級する労働者の選択
curA.execute(query, (date(2000, 1, 1), date(2000, 12, 31)))

# CurAの結果からループを実行
for (emp_no, salary, from_date, to_date) in curA:

  # Update the old and insert the new salary
  new_salary = int(round(salary * Decimal('1.15')))
  curB.execute(update_old_salary, (tomorrow, emp_no, from_date))
  curB.execute(insert_new_salary,
               (emp_no, tomorrow, date(9999, 1, 1,), new_salary))

  # 変更をコミット
  cnx.commit()

cnx.close()