ざっくりPythonのおさらい その2。おもにデータ保存


前回に引き続いて、データ保存周りのおさらいです。
その他のおさらい事項にについては、以下ご参照ください。

[ざっくりPythonのおさらい その1。基本文法など]
https://qiita.com/48hands/items/31b7ac2b49addbb8658c

[ざっくりPythonのおさらい その2。おもにデータ保存]
https://qiita.com/48hands/items/ab86b7463268e669d216

[ざっくりPythonのおさらい その3。Numpy, Pandas]
https://qiita.com/48hands/items/b1a71000533e129b27f2

## PythonからRDBを扱う

事前に`mysql-connector-python`, `sqlalchemy`, `pymysql`をパッケージインストールしておく。

```
pip install mysql-connector-python sqlalchemy pymysql
```

### ORMを使わない実装例

#### SQLite3

```python
import sqlite3

# コネクション生成 ファイルに永続化したい場合
conn = sqlite3.connect('my_sqlite.db')

# コネクション生成 メモリ上で完結させる場合はこっちを使う。
# conn = sqlite3.connect(':memory:')

# カーソルの生成
cursor = conn.cursor()

# テーブルの作成
sql = """
CREATE TABLE IF NOT EXISTS persons(
  id INTEGER PRIMARY KEY AUTOINCREMENT
 ,name STRING
)
"""
cursor.execute(sql)
conn.commit()

# データのINSERT
cursor.execute("INSERT INTO persons(name) VALUES('Hanako')")
cursor.execute("INSERT INTO persons(name) VALUES('Taro')")
cursor.execute("INSERT INTO persons(name) VALUES('Yuichi')")
cursor.execute("INSERT INTO persons(name) VALUES('Makio')")
conn.commit()

# データのSELECT
sql = "SELECT * from persons"
cursor.execute(sql)

# カーソルに結果が入っているので受け取る
# fetchall()メソッドを使う。
result = cursor.fetchall()
for user_id, name in result:
    print(user_id, name)

# コネクションのクローズ
conn.close()
```

#### MySQL

ほとんどSQLiteと同じ。

```python
import mysql.connector

# コネクションの設定
conn = mysql.connector.connect(host='127.0.0.1', port=3306, user='root',
    # password='password',
    # database='my_db'
)

# カーソルの生成
cursor = conn.cursor()

# データベース作成
cursor.execute('CREATE DATABASE IF NOT EXISTS my_db')

# テーブル作成
cursor.execute('CREATE TABLE IF NOT EXISTS my_db.persons('
               'id int NOT NULL AUTO_INCREMENT,'
               'name varchar(20) NOT NULL,'
               'PRIMARY KEY(id)'
               ')')


# データのINSERT
for i in range(10):
    cursor.execute(
        'INSERT INTO my_db.persons(name) values("Robot-{}")'.format(str(i + 1)))

# コミット
conn.commit()


# データのSELECT
cursor.execute('SELECT * FROM my_db.persons')

for user_id, name in cursor.fetchall():
    print(user_id, name)


# カーソルのクローズ
cursor.close()

# コネクションクローズ
conn.close()
```

### SQLAlchemyを使った実装

データベースをSQLiteのメモリに設定する場合は以下。
personsテーブルを作成してPersonオブジェクトとして扱えるようにしている。

```python
import sqlalchemy
import sqlalchemy.ext.declarative
import sqlalchemy.orm

# sqliteを使ってメモリで完結させる。
# echoをTrueにすると、実際に発行したSQLを確認できる
engine = sqlalchemy.create_engine('sqlite:///:memory:', echo=True)

# おまじない
Base = sqlalchemy.ext.declarative.declarative_base()


# Personモデルの定義
class Person(Base):
    __tablename__ = 'persons'

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True,
        autoincrement=True)

    name = sqlalchemy.Column(sqlalchemy.String(20))


# これを実行するとメモリ上にpersonsテーブルが作成される。
Base.metadata.create_all(engine)

# セッションの作成
Session = sqlalchemy.orm.sessionmaker(bind=engine)
session = Session()

# データの登録
person_list = [Person(name='Hanako'), Person(name='Taro'), Person(name='Makio')]
for p in person_list:
    # addメソッドを使って登録する
    session.add(p)
    session.commit()

# データを取得してnameを変更してupdateする。
p1 = session.query(Person).filter_by(name='Taro').first()
p1.name = 'Mike'
session.add(p1)
session.commit()

# データを削除する
p1 = session.query(Person).filter_by(name='Hanako').first()
session.delete(p1)
session.commit()

# 一覧を取得する
persons = session.query(Person).all()
for person in persons:
    print(person.id, person.name)
```

データベースをSQLiteのファイルに永続化する場合は`engine`を以下のように設定する。

```python
engine = sqlalchemy.create_engine('sqlite:///dev_db_sqlalchemy', echo=True)
```

データベースをMySQLにしたい場合は、以下のように設定する。


```python
# mysql+pymysqlを指定する
url = 'mysql+pymysql://username:password@127.0.0.1/my_db'
engine = sqlalchemy.create_engine(url, echo=True)
```

## PythonのDBMを扱う

DBMはPythonの標準ライブラリ。
簡単にキャッシュの保存などを使いたい場合に利用するらしい。

```python
import dbm


with dbm.open('cache', 'c') as db:
    # 文字列をキャッシュとして保存する
    db['key1'] = 'value1'
    db['key2'] = 'value2'

    # NOTE: 数値はそのまま保存するとエラーになる。文字列のみ対応している。
    # db['key3'] = 123


with dbm.open('cache', 'r') as db:
    print(db.get('key1')) # b'value1'となり、バイナリ型になって表示される
    print(db.get('key2').decode('utf-8')) # utf-8でデコードするとvalue2として表示される
```

```python
b'value1'
value2
```

## PythonからMemchachedを扱う

Memchachedは、データをメモリ上に保存しておいてデーベースへのアクセスの前に参照してデータベースにアクセスする回数を減らしたりするために使ったりするソフトウェア。

[Memcheched]
http://memcached.org/

### Memchachedのインストール

```
# Linuxの場合
wget https://www.memcached.org/files/memcached-1.5.10.tar.gz
tar -zxvf memcached-1.5.10.tar.gz
cd memcached-1.5.10
./configure && make && make test && sudo make install

# Macの場合はbrew installでできる。
brew install memcached
brew services start memcached


# python-memchacheパッケージをインストールしておく
pip install python-memcached
```

Pythonから扱うためのコードは以下。

```python
import memcache

# memcacheのクライアント設定
db = memcache.Client(['127.0.0.1:11211'])

# キーバリューの形式でセットする。
db.set('key1', 'value1')
db.set('key2', 'value2')

# データの取得
print(db.get('key1'))
```

保持する時間を設定できる。
以下のように`time`に値をセットする。

```python
import time

import memcache

# memcacheのクライアント設定
db = memcache.Client(['127.0.0.1:11211'])

# キーバリューの形式でセットする。
db.set('key1', 'value1', time=1)

# データの取得
print(db.get('key1'))

# sleepする
time.sleep(2)

# データ取得できないことを確認する。
# 値が取れない場合はNoneが返却される
print(db.get('key1'))
```

実行すると、以下のようになる。

```
value1
None
```
### RDBとMemcacheの併用

準備として、SQLiteにデータを入れておく。

```python
import sqlite3


cursor.execute('CREATE TABLE IF NOT EXISTS persons'
               '(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING)')
cursor.execute('INSERT INTO persons(name) VALUES("Makio")')
conn.commit()
conn.close()
```

この状態でMemchacheから値が取れなかった場合に、DBアクセスする。

```python
import sqlite3
import memcache

# memcacheのクライアント設定
mem = memcache.Client(['127.0.0.1:11211'])

# SQLiteのコネクション、カーソル設定
conn = sqlite3.connect('test_sqlite_memcached.db')
cursor = conn.cursor()


def get_person_id(name):
    """
    nameからidを返却するメソッド
    """
    # Memcachedからidがとれればそのまま返却
    # DBにアクセスしない。
    person_id = mem.get(name)
    if person_id:
        return person_id

    # 以下はMemchachedにデータがない場合
    # DBに対して検索処理
    sql= 'SELECT * FROM persons WHERE name = "{}"'.format(name)
    cursor.execute(sql)

    person = cursor.fetchone()
    if not person:
        raise Exception('No person data')

    person_id, name = person # アンパッキング

    # 60秒以内だったらMemchacheから値を参照できるように
    # Memchachedに値を設定しておく。
    mem.set(name, person_id, time=60)

    return person_id

print(get_person_id("Makio"))
```

## Pickleを使ったオブジェクトの保存

Pythonのデータをそのまま保存するライブラリ

```python
import pickle

class Person(object):
    def __init__(self, name):
        self.name = name

obj = {
    'key1': [1,2,3,4,5],
    'key2': ['dev', 'prod', 'test'],
    'key3': Person('Makio')
}

# データの保存処理。
# バイナリ形式でオブジェクトを保存する。
with open('obj.pickle', 'wb') as f:
    pickle.dump(obj, f)

# pickleファイルからデータを読み込む。
with open('obj.pickle', 'rb') as f:
    read_obj = pickle.load(f)

# 読込できたか確認する。
print(read_obj)
print(read_obj['key1'])
print(read_obj['key2'])
print(read_obj['key3'].name)
```

実行結果。

```
{'key1': [1, 2, 3, 4, 5], 'key2': ['dev', 'prod', 'test'], 'key3': <__main__.Person object at 0x10e328588>}
[1, 2, 3, 4, 5]
['dev', 'prod', 'test']
Makio
```