今さらPython3 (57) - SQLALchemy

第8章を引き続き。

入門 Python 3

入門 Python 3

SQLAlchemy

本を読みながら、まだピンと来ないけど、DB-APIは共通APIまでしかカバーされてない一方、RDMSによって異なるドライバを使うのも面倒なので、それらの違いを意識しなくても使えるようなAPIがあれば良いよねということで、SQLAlchemyが紹介されているんだね。

SQLAlchemy - The Database Toolkit for Python

ということで、ちょいと試してみますか。

$ pip3 install sqlalchemy
Collecting sqlalchemy
  Downloading SQLAlchemy-1.0.11.tar.gz (4.7MB)
    100% |████████████████████████████████| 4.7MB 121kB/s 
Installing collected packages: sqlalchemy
  Running setup.py install for sqlalchemy
Successfully installed sqlalchemy-1.0.11

エンジンレイヤ

sqliteを使った例が紹介されているけど、ひねくれ者なのでMariaDBで行きますw。たまたま入っていただけなんだけど。まずは、ターミナルをもう1つ開いて、MariaDBを起動。

$ mysqld
151223 21:04:12 [Note] mysqld (mysqld 10.0.19-MariaDB) starting as process 10625 ...
151223 21:04:12 [Warning] Setting lower_case_table_names=2 because file system for /usr/local/var/mysql/ is case insensitive
151223 21:04:12 [Note] InnoDB: Using mutexes to ref count buffer pool pages
151223 21:04:12 [Note] InnoDB: The InnoDB memory heap is disabled
151223 21:04:12 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
151223 21:04:12 [Note] InnoDB: Memory barrier is not used
151223 21:04:12 [Note] InnoDB: Compressed tables use zlib 1.2.5
151223 21:04:12 [Note] InnoDB: Using CPU crc32 instructions
151223 21:04:12 [Note] InnoDB: Initializing buffer pool, size = 128.0M
151223 21:04:12 [Note] InnoDB: Completed initialization of buffer pool
151223 21:04:12 [Note] InnoDB: Highest supported file format is Barracuda.
151223 21:04:12 [Note] InnoDB: 128 rollback segment(s) are active.
151223 21:04:12 [Note] InnoDB: Waiting for purge to start
151223 21:04:12 [Note] InnoDB:  Percona XtraDB (http://www.percona.com) 5.6.23-72.1 started; log sequence number 2209817
151223 21:04:12 [Note] Server socket created on IP: '::'.
151223 21:04:12 [Note] Event Scheduler: Loaded 0 events
151223 21:04:12 [Note] mysqld: ready for connections.
Version: '10.0.19-MariaDB'  socket: '/tmp/mysql.sock'  port: 3306  Homebrew

これを起動しておかないと、ここから先が実行できないからね。testというデータベースを作っておいた状態から、

>>> import sqlalchemy as sa
>>> conn = sa.create_engine('mysql+pymysql://localhost/test')

実は結構ハマったけど、この文書を見ながらやれば大丈夫でした。てか、pymysqlの助けが結局必要なのね。

http://docs.sqlalchemy.org/en/latest/core/engines.html

>>> conn.execute('''CREATE TABLE zoo 
... (critter VARCHAR(20) PRIMARY KEY,
...  count INT,
...  damages FLOAT)''')
<sqlalchemy.engine.result.ResultProxy object at 0x10374d940>
>>> 

これでテーブル作成。

>>> ins = 'INSERT INTO zoo (critter, count, damages) VALUES (%s, %s, %s)'
>>> conn.execute(ins, 'duck', 10, 0.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1038302e8>
>>> conn.execute(ins, 'bear', 2, 1000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x10374d7f0>
>>> conn.execute(ins, 'weasel', 1, 2000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x103830320>
>>>

エントリ追加。sqliteのときはVALUES(?, ?, ?)だったけど、MySQLのときは(%s, %s, %s)に置き換えないとエラー(TypeError: not all arguments converted during string formatting)になった。てか方言の隙間を埋めてくれるんじゃなかったの?

>>> rows = conn.execute('SELECT * FROM zoo')
>>> print(rows)
<sqlalchemy.engine.result.ResultProxy object at 0x10374dd68>
>>> for row in rows:
...     print(row)
... 
('bear', 2, 1000.0)
('duck', 10, 0.0)
('weasel', 1, 2000.0)
>>> 

これでテーブルの内容を確認。ここらへんは、sqliteと同じ。

SQL表現言語

さっきのエンジンレイヤから1レベル上がったレイヤということなのね。やっと、このあたりの意図が分かってきた。

>>> import sqlalchemy as sa
>>> conn = sa.create_engine('mysql+pymysql://localhost/test')

ここは、前回と同じというか続けてやっているなら別に実行する必要ないやね。

>>> zoo = sa.Table('zoo3', meta,
...    sa.Column('critter', sa.String(20), primary_key=True),
...    sa.Column('count', sa.Integer),
...    sa.Column('damages', sa.Float)
... )
>>> meta.create_all(conn)

これはテーブルを定義しているところ。さっきは方言の対応出来てないじゃん的なツッコミしていたけど、このレベルだとTableメソッドを呼んで作るので、バックエンドで動いているDBが何であっても同じ構文で行けるということだね。

>>> conn.execute(zoo.insert(('bear', 2, 1000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x103842e48>
>>> conn.execute(zoo.insert(('weasel', 1, 2000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x103842ac8>
>>> conn.execute(zoo.insert(('duck', 10, 0)))
<sqlalchemy.engine.result.ResultProxy object at 0x103842c88>
>>> result = conn.execute(zoo.select())
>>> rows = result.fetchall()
>>> print(rows)
[('bear', 2, 1000.0), ('duck', 10, 0.0), ('weasel', 1, 2000.0)]
>>> 

エントリを追加したあと、テーブル内容を選択してrowsに格納していると。conn.execute()と使っているものは一緒に見えて、zooインスタンスから渡している。念のため、タイプを確認。

>>> type(zoo)
<class 'sqlalchemy.sql.schema.Table'>

ORM

Object Relation Mappingになると、さらに1レイヤ上がってくるわけですね。

>>> import sqlalchemy as sa
>>> from sqlalchemy.ext.declarative import declarative_base
>>> conn = sa.create_engine('mysql+pymysql://localhost/test')

インポートするモノが1つ増えてる。

>>> Base = declarative_base()
>>> class Zoo(Base):
...     __tablename__ = 'zoo4'
...     critter = sa.Column('critter', sa.String(20), primary_key=True)
...     count = sa.Column('count', sa.Integer)
...     damages = sa.Column('damages', sa.Float)
...     def __init__(self, critter, count, damages):
...         self.critter = critter
...         self.count = count
...         self.damages = damages
...     def __repr__(self):
...         return "<Zoo4>({}, {}, {})".format(self.critter, self.count, self.damages)
... 
>>> Base.metadata.create_all(conn)

これがテーブル定義をしているところ。いちおうテーブルが出来ているか確認。

f:id:deutschina:20151224065440p:plain
(※Sequel Proを使ってます)

実際にテーブルが出来ていることも確認できるんだけど、なんかスッキリしない。Zooクラスのインスタンスを作る部分が見えないから。Zooクラスの定義をした後にやっているのは、declarative_base()をインスタンス化したBaseの中でmetadata.create_all()を呼んでいるだけ。つまりこの中で、BaseもしくはBaseを継承したクラスのインスタンスを作っているってこと?

>>> first = Zoo('duck', 10, 0.0)
>>> second = Zoo('bear', 2, 1000.0)
>>> third = Zoo('weasel', 1, 2000.0)
>>> first
<Zoo4>(duck, 10, 0.0)
>>> type(first)
<class '__main__.Zoo'>

今度はエントリを作るところ。これは、見るからにZoo.__init__()が実行されたのが分かる。となると、metadata.create_all()でインスタンスが作られているというさっきの予想はハズレだね。

f:id:deutschina:20151224071330p:plain

ちなみに、この時点ではまだレコードは出来ていない模様。

>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=conn)
>>> session = Session()
>>> session.add(first)
>>> session.add_all([second, third])
>>> session.commit()

実際にレコードを登録しているのがこの部分。今回は、きちんとコミット打たないとダメなのね。(逆にこれまで打つ必要がなかった理由が分からん)

f:id:deutschina:20151224071930p:plain

レコードも登録されてます。

本の中でも議論されているけど、どうやってDBとコミュニケーションするかというのは、アプリを作るというか設計する上で、考えなくてはいけないポイントであるのは確かだよね。自分が特定のRDBMSSQLの扱いに慣れていればいるほど、低いレベルで直接いじりたいという衝動に駆られるけど、アプリを配布するとか異なるDB上で動かす必要があるなら、高いレベルのORMとかで作っておいた方が、扱いは楽のような気がするし。

このORMよりもっとシンプルにやるには、こういうものもあるらしい。

dataset: databases for lazy people — dataset 0.6.0 documentation

(つづく)