Python+MySQL で dict で結果が返ってくるカーソルを使う

（この記事は私の blog の http://umezawa.dyndns.info/wordpress/?p=7308 の転載です）

ちょっと Python + MySQL をいじってるんですが、デフォルトではクエリの結果はタプルorリストで返ってくるので、雑に `SELECT * FROM table` とかやるとどんな順番で列が返ってくるのか SQL 文を見ただけでは不明で扱いづらいため、 dict （連想配列）で取れると嬉しいです。

<!--more-->

何とかならないのかなぁと思いつつ [PyMySQL](https://pymysql.readthedocs.io/en/latest/modules/index.html) や [MySQL Connector/Python](https://dev.mysql.com/doc/connector-python/en/connector-python-reference.html) や [mysqlclient](https://mysqlclient.readthedocs.io/index.html) のドキュメントを読むと、 `connection.cursor()` の引数になんか渡すと結果を dict で返してくるカーソルが得られるようなので、これを使ってみます。

## 準備

まず、以下のようなテーブルを作っておきます。

```
mysql> SELECT * FROM table1;
+------+------+
| hoge | fuga |
+------+------+
|    1 |    2 |
|    3 |    4 |
+------+------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM table2;
+------+------+
| fuga | piyo |
+------+------+
|    4 |    5 |
+------+------+
1 row in set (0.00 sec)
```

## PyMySQL の場合

そして以下のように実行してみます。

```python
#!/usr/bin/python3

mysql_kwargs = {
	"host":     "localhost",
	"port":     3306,
	"user":     "test",
	"password": "hogehoge",
	"database": "testdb",
}


import pymysql
print("PyMySQL")
print()

conn = pymysql.connect(**mysql_kwargs)

def get_cursor():
	return conn.cursor()

def get_dict_cursor():
	return conn.cursor(pymysql.cursors.DictCursor)

cur = get_cursor()
cur.execute("SELECT * FROM table1")
print(cur.description)
print(cur.fetchall())
cur.close()

cur = get_dict_cursor()
cur.execute("SELECT * FROM table1")
print(cur.fetchall())
cur.close()

print()

cur = get_cursor()
cur.execute("SELECT * FROM table1, table2 WHERE table1.fuga = table2.fuga")
print(cur.description)
print(cur.fetchall())
cur.close()

cur = get_dict_cursor()
cur.execute("SELECT * FROM table1, table2 WHERE table1.fuga = table2.fuga")
print(cur.fetchall())
cur.close()

print()

cur = get_cursor()
cur.execute("SELECT * FROM table1 INNER JOIN table2 USING(fuga)")
print(cur.description)
print(cur.fetchall())
cur.close()

cur = get_dict_cursor()
cur.execute("SELECT * FROM table1 INNER JOIN table2 USING(fuga)")
print(cur.fetchall())
cur.close()
```

```
[umezawa@devubuntu:pts/1 ~]$ ./cursortest.py
PyMySQL

(('hoge', 3, None, 11, 11, 0, False), ('fuga', 3, None, 11, 11, 0, False))
((1, 2), (3, 4))
[{'hoge': 1, 'fuga': 2}, {'hoge': 3, 'fuga': 4}]

(('hoge', 3, None, 11, 11, 0, False), ('fuga', 3, None, 11, 11, 0, False), ('fuga', 3, None, 11, 11, 0, False), ('piyo', 3, None, 11, 11, 0, False))
((3, 4, 4, 5),)
[{'hoge': 3, 'fuga': 4, 'table2.fuga': 4, 'piyo': 5}]

(('fuga', 3, None, 11, 11, 0, False), ('hoge', 3, None, 11, 11, 0, False), ('piyo', 3, None, 11, 11, 0, False))
((4, 3, 5),)
[{'fuga': 4, 'hoge': 3, 'piyo': 5}]
```

## MySQL Connector/Python の場合

スクリプトの先頭の部分を以下のように変えて MySQL Connector/Python を使ってみます。

```python
import mysql.connector
print("MySQL Connector/Python")
print()

conn = mysql.connector.connect(**mysql_kwargs)

def get_cursor():
	return conn.cursor()

def get_dict_cursor():
	return conn.cursor(dictionary=True)
```

```
[umezawa@devubuntu:pts/1 ~]$ ./cursortest.py
MySQL Connector/Python

[('hoge', 3, None, None, None, None, 0, 4097), ('fuga', 3, None, None, None, None, 0, 4097)]
[(1, 2), (3, 4)]
[{'hoge': 1, 'fuga': 2}, {'hoge': 3, 'fuga': 4}]

[('hoge', 3, None, None, None, None, 0, 4097), ('fuga', 3, None, None, None, None, 0, 4097), ('fuga', 3, None, None, None, None, 0, 4097), ('piyo', 3, None, None, None, None, 0, 4097)]
[(3, 4, 4, 5)]
[{'hoge': 3, 'fuga': 4, 'piyo': 5}]

[('fuga', 3, None, None, None, None, 0, 4097), ('hoge', 3, None, None, None, None, 0, 4097), ('piyo', 3, None, None, None, None, 0, 4097)]
[(4, 3, 5)]
[{'fuga': 4, 'hoge': 3, 'piyo': 5}]
```

## mysqlclient の場合

今度はこう書き換えて mysqlclient を使います。

```python
import MySQLdb
print("mysqlclient")
print()

conn = MySQLdb.connect(**mysql_kwargs)

def get_cursor():
	return conn.cursor()

def get_dict_cursor():
	return conn.cursor(MySQLdb.cursors.DictCursor)
```

```
[umezawa@devubuntu:pts/1 ~]$ ./cursortest.py
mysqlclient

(('hoge', 3, 1, 11, 11, 0, 0), ('fuga', 3, 1, 11, 11, 0, 0))
((1, 2), (3, 4))
({'hoge': 1, 'fuga': 2}, {'hoge': 3, 'fuga': 4})

(('hoge', 3, 1, 11, 11, 0, 0), ('fuga', 3, 1, 11, 11, 0, 0), ('fuga', 3, 1, 11, 11, 0, 0), ('piyo', 3, 1, 11, 11, 0, 0))
((3, 4, 4, 5),)
({'hoge': 3, 'fuga': 4, 'table2.fuga': 4, 'piyo': 5},)

(('fuga', 3, 1, 11, 11, 0, 0), ('hoge', 3, 1, 11, 11, 0, 0), ('piyo', 3, 1, 11, 11, 0, 0))
((4, 3, 5),)
({'fuga': 4, 'hoge': 3, 'piyo': 5},)
```

というわけで何となく期待したような結果になっています。

## 注意点とか

MySQL Connector/Python は、同じ名前の列が複数出現するときの挙動が他の2つと異なる（他の2つは2つ目の出現にはテーブル名が付くので見分けがつくが、MySQL Connector/Python はテーブル名は付かずに上書きされるっぽい？）ので、その点だけ注意でしょうか。

ちなみに dict なカーソルが使えない場合は、以下のような idiom で同様の結果を得ることができます。（最初は全部こう書いててダサいなぁと思ってた）

```python
dict_results= [dict((cursor.description[i][0], value) for i, value in enumerate(row)) for row in cursor.fetchall()]
```

## 使った環境

- Ubuntu 18.04 LTS
- Python 3.6.8
- PyMySQL 0.9.3
- MySQL Connector/Python 2.1.6
- mysqlclient 1.3.10