SQLAlchemyの基本的な使い方【理解度チェックテスト付き】

SQLAlchemyの基本的な使い方についてこの記事で丁寧に解説していきます。
なお進め方は公式のチュートリアルに即しており、SQLAlchemyについて説明したのち、インストール手順と基本的な使い方を解説します。
最後に理解度チェックテストも準備してるので、説明を読み終えたら是非。
著者環境
OS : Ubuntu 18.04.2 LTS
python : python 3.6.8
SQLAlchemy : 1.3.7
SQLAlchemyとは
そもそもSQLAlchemyとは何でしょう。
公式サイトには以下のように説明されています。
The Python SQL Toolkit and Object Relational Mapper
https://www.sqlalchemy.org/
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper that gives application developers the full power and flexibility of SQL.
It provides a full suite of well known enterprise-level persistence patterns, designed for efficient and high-performing database access, adapted into a simple and Pythonic domain language.
つまり、SQLAlchemyとはPythonを使ってDB操作を簡単に行うためのツールです。
SQLAlchemyを使うことで、PythonのクラスとDB内のテーブルを関連づけられます。
その結果、インスタンス生成をするようにレコードを登録したり、そのクラスから生成されたインスタンスが持つ変数に値を再代入することで、レコードの情報を更新することが出来ます。
ちなみにこのようにクラスとテーブルを関連づける機能のことをORM(Object-relational mapping)と呼びます。
インストール方法
pipがインストールされている環境であればのコマンドを実行。
参考: 【Ubuntu】Pythonの仮想環境をvenvで作る方法
$ pip install sqlalchemy
pipをまだインストールしていない環境であれば以下のコマンドを実行。
# pip3コマンドを使うために必要なものをインストール
$ sudo apt update
$ sudo apt install python3-pip
# SQLAlchemyのインストール
$ pip3 install sqlalchemy
上記の手順でインストールは完了。
それでは使い方の説明に移ります。
DBへの足がかり作成
まず最初に「どのデータベース」に「どうやって接続するか」の設定を行います。
その設定内容を保持したものはエンジンと呼ばれ、このエンジンが全てのDB操作の足がかりとなります。
注意: >>>で始まっている行はpythonコマンドラインから実行していることを表しています。コマンドライン実行するためにはターミナル上で「python」もしくは「python3」と入力後、Enterキーを押下してください。するとpythonをコマンドライン実行できるようになります。
# エンジン作成のための関数をimport
>>> from sqlalchemy import create_engine
# エンジンオブジェクトの作成
>>> engine = create_engine('sqlite:///:memory:', echo=True)
create_engineのパラメータについて解説します。
第一引数
create_engineの第一引数には「どのDB」に「どうやって接続するか」を示します。今回の場合はUbuntu18.04に標準で入っている”sqlite”というDB管理システムを使用します。sqlite:///:memory:と指定した場合にはsqliteのインメモリデータベース内にアクセスすることになります。
インメモリデータベースはデータをHDDやSSDに保存するのではなく、メモリ上に記録します。そのため、ファイル実行が完了した時や、Pythonコマンドラインを抜けるなど、一連の処理が完了したら記録されていたデータは消えてしまいます。簡単に操作を試すときなどに有用です。
ちなみにpostgresqlデータベースが対象の場合、以下のようになります。
>>> engine = create_engine('postgresql://username:password@hostname/database_name', echo=True)
hostnameの箇所は「IPアドレス:port番号」という書き方でも可。postgresqlにアクセスする場合はPsycopg2のようなpythonとPostgreSQLを結びつけるアダプターが必要なので注意です。
第2引数
echoの値がTrueの場合、SQLAlchemyが発行したSQL文がログとして出力されるようになります。最初のうちはTrueにして、SQLAlchemyによってどのようなSQL文が発行されているのか確認しましょう。
テーブル作成
テーブル作成をする方法は2つあります。1つが”MetaData”を使う方法、もう一つが”declarative_base”を使う方法です。それぞれ詳しく説明します。
MetaDataを使う場合
“MetaData”はテーブルオブジェクト(テーブルと紐づいたクラス)をまとめて管理するためのものです。実際のコードを見てみましょう。
from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
# MetaDataをインスタンス化
metadata = MetaData()
# Tableインスタンスを作成(第一引数:テーブル名,第二引数:metadata、第三引数以降:カラム)
# これによってmetadataの管理対象として登録
users_table = Table('users',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String),
Column('fullname', String),
Column('password', String))
# engineで設定されているDBにmetadataの管理化にあるテーブルをすべて作成
metadata.create_all(engine)
“metadata.create_all”メソッドは同名のテーブルがあるか確認されたのちに行われるので、複数回実行しても同じテーブルは複数作成されることはありません。
カラム等のテーブル情報を変更したTableインスタンスを再度生成してからcreate_allしても、同名のテーブルがすでにある場合、新しくテーブルは作成されないので注意しましょう。あくまでテーブル名のみをみて作成するかしないかを決めています。
テーブルの情報を受け取る用のクラスを作る
上記で作成した”users”と名付けたTableインスタンスはあくまでもDBにテーブルを作成するためのものであり、DBの”users”テーブルからレコード情報を受け取るための、ものではありません。
テーブル作成用のクラスとは別に、レコード情報を受け取るためのクラスを作成する必要があります。
作り方は以下の通りです。
class User:
def __init__(self, name, fullname, password):
self.name = name
self.fullname = fullname
self.password = password
# インスタンスを出力しようとすると
# __repr__メソッドは"print(Userインスタンス)"した時に出力する値を決めることができる。
def __repr__(self):
return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)
カラム名がそのままインスタンス変数となります。
“__repr__”メソッドは”print(Userインスタンス)”を実行した時に出力する値を決めることが出来ます。
# __repr__がない場合
>>> class User:
... def __init__(self, name, fullname, password):
... self.name = name
... self.fullname = fullname
... self.password = password
...
>>> ed_user = User('ed', 'Ed Jones', 'edspassword')
>>> print(ed_user)
<__main__.User object at 0x7fb07929c320>
# この出力結果ではインスタンスがどんな値を持っているか分からない。
# __repr__がある場合
>>> class User:
... def __init__(self, name, fullname, password):
... self.name = name
... self.fullname = fullname
... self.password = password
... def __repr__(self):
... return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)
...
>>> ed_user = User('ed', 'Ed Jones', 'edspassword')
>>> print(ed_user)
<User('ed','Ed Jones', 'edspassword')>
# インスタンス変数の中身が見れる!
マッピング
Tableの情報とそれをPythonで扱うためのクラスが出来ました。
次にこの2つを関連付ける必要があります。この関連付けのことをマッピングと呼びます。
コード例は以下。
>>> from sqlalchemy.orm import mapper
>>> mapper(User, users_table)
<Mapper at 0x7fb07531ce10; User>
mapper関数を使って’users’テーブルと紐づけています。Mapper at ~~~の~~~はメモリ上のどこに紐付け情報が保存されているかを示しています。
これでSQLAlchemyのORM機能を使う準備は出来ました。それでは実際に動かしてみましょう。
# マッピング完了後
>>> ed_user = User('ed', 'Ed Jones', 'edspassword')
>>> ed_user.name
'ed'
>>> ed_user.password
'edspassword'
>>> print(ed_user.id)
None
もともとUserというクラスにはidという変数がないはず。
本来そのようなクラスから生成されたインスタンスでidを参照しようとしたら以下のようにエラーが発生します。
>>> class Dog:
... def __init__(self, name):
... self.name = name
...
>>> pochi = Dog("pochi")
>>> pochi.name
'pochi'
>>> pochi.id
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Dog' object has no attribute 'id'
しかし、Userクラスはすでにusersテーブルにマッピングされています。
テーブルにマッピングされたクラスは、そのテーブルのカラムと同じ属性を持つようになります。Userクラスから生成されたインスタンスにはidという属性が追加され、参照をしようとしてもAttributionErrorは発生しなくなります。
idの値はまだNoneですが、後述するcommitという作業を行うと値が入ることになります。
declarative_baseを使う場合
テーブル定義、およびテーブル生成をする方法の2つ目がdeclarative_baseを使う方法です。
この方法はmetadataの作成もtableオブジェクトの作成もクラスとのマッピングも同時に行うことが出来、便利です。
以下がdeclarative_baseを使った場合のコード例になります。
# declarative_base関数のインポート
from sqlalchemy.ext.declarative import declarative_base
# MetaDataの機能や関連付けの機能を持つBaseオブジェクトを作成
Base = declarative_base()
# Baseを継承させることでBaseのもつMetaDataにこのクラスが登録される
class User(Base):
# 関連づけるテーブル名を指定
__tablename__ = 'users'
# テーブルのカラム情報をクラス変数として持たせる
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
password = Column(String)
# id以外のカラム名のインスタンス変数を作成(idはDBに登録される際に自動で割り振られるので不要)
def __init__(self, name, fullname, password):
self.name = name
self.fullname = fullname
self.password = password
# Userクラスのインスタンスの出力結果を設定
def __repr__(self):
return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)
declarative_base関数で作成されたBaseはmetadataを属性として持っているので、以下のようにしてmetadataに登録されているクラス(上のコード例でいうとUserクラス)のテーブルを作成することができます。
>>> Base.metadata.create_all(engine)
~~出力一部省略~~
CREATE TABLE users (
id SERIAL NOT NULL,
name VARCHAR,
fullname VARCHAR,
password VARCHAR,
PRIMARY KEY (id)
)
~~出力一部省略~~
# テーブル名"users"はUserクラスのクラス変数__tablename__に格納された文字列を取ってきている
セッションを作る
セッションとはDBとPythonコードを結びつける紐のようなものだと簡単に覚えてください。このセッションを使ってDBに実際にデータを追加したり、編集したりなど、DB操作をすることになります。
作り方は以下の2通りです。
# sessionmakerのimport
# 小文字で始まっているから関数だと思いきや、実はクラス
>>> from sqlalchemy.orm import sessionmaker
# ここまでの手順は同じ
## セッションの作り方1つ目:sessionmakerを呼ぶ時にengineと結びつける。
# sessionmakerはクラスなので出来上がるSessionはsessionmakerのインスタンス
>>> Session = sessionmaker(bind=engine)
# sessionmakerクラスから生成されるインスタンスは
# 呼ばれた時に設定がちゃんと反映されたインスタンスを返すような作りになっている。
>>> session = Session()
# このsessionが実際に使うsession
## セッションの作り方2つ目:sessionmakerでインスタンスを作った後にengineと結びつける。
>>> Session = sessionmaker()
>>> Session.configure(bind=engine)
>>> session = Session()
# sessionを作ってからengineを作りたい時に使う。
# 基本は簡単に作成できる1つ目の方法を使う
カラムの追加
エンジン、マッピング、セッション作成が完了し、ついにDB操作を始める時が来ました。そこでまずは最も簡単なカラムの追加の流れを説明します。
# usersテーブルとマッピングされているUserクラスからインスタンス化
>>> ed_user = User('ed', 'Ed Jones', 'edspassword')
# セッションに乗せる
# あくまでここではセッションに乗せているだけで、DBの方には反映されていないことに注意
# Amazonでカートには入れたけど、まだ購入は完了していない感じ
>>> session.add(ed_user)
# DBに反映させる
# Amazonでいうと購入を確定させた感じ
>>> session.commit()
これで追加の流れは完了です。
インスタンス化→add→commitの流れ。
テーブル情報の読み取り
カラムの読み取りは基本以下のような構文で行います。
変数 = session.query(~~~).all()
~~~の箇所には”欲しいテーブル情報とマッピングしているクラス名.カラム名”で持ってきます。クラス名だけで指定した場合には指定したクラスのインスタンスが返ってきます。つまり全カラムの情報をまとめて取得できます。
all()で取得した場合には条件に合致したカラム全てがリストに格納されて返ってきます。
条件の指定方法にはfilterメソッドがあります。これはSQL文で言うwhere文と同じです。
first()というメソッドもあり、これは条件に合致したカラムの中で先頭のカラムだけ返します。
ここでの一番気を付けないといけない点は、session.queryはDBだけでなくsessionにも問い合わせを行なっている点です。「DBの情報」+「セッションにaddはしたがcommitはしていないデータ」がquery対象となります。実行例は以下。
# 一人目はcommitまでする
>>> ed_user = User('ed', 'Ed Jones', 'edspassword')
>>> session.add(ed_user)
>>> session.commit()
# 二人目はcommitせずaddだけ
>>> ed_user2 = User('ed2', 'Ed2 Jones', 'ed2spassword')
>>> session.add(ed_user2)
>>> all_users = session.query(User).all()
>>> all_users
[<User('ed','Ed Jones', 'edspassword')>, <User('ed2','Ed2 Jones', 'ed2spassword')>]
# commitしていないed2のインスタンスまで持ってこれた
# ついでにカラムを指定した場合
>>> users_id_name = session.query(User.id, User.name).all()
>>> users_id_name
[(1, 'ed'), (2, 'ed2')]
>>> type(users_id_name[0])
<class 'sqlalchemy.util._collections.result'>
# さらについでにfilterを使った場合
>>> filter_users = session.query(User).filter(User.name=='ed').all()
>>> filter_users
[<User('ed','Ed Jones', 'edspassword')>]
# filter条件に合致したedを取得し、合致しなかったed2は取得しなかった。
カラムの更新
カラムの更新は以下の手順で更新します。
- 更新したいレコードの情報をインスタンスの形でqueryで取得(カラムの指定はしない)
- 取得したインスタンスの変数に再代入
- commit
ちなみに1で取得したものはsessionに問い合わせて取得したものであり、すでにsessionに紐づいているためaddする必要がないことを覚えておきましょう。
# all()ではなくfirst()で取得するのでリストではなくインスタンスの形で返ってくる
>>> ed_user = session.query(User).filter(User.name=='ed').first()
>>> ed_user
<User('ed','Ed Jones', 'edspassword')>
>>> ed_user.name = 'edy'
>>> ed_user
<User('edy','Ed Jones', 'edspassword')
>>>> session.commit()
2019-08-21 22:52:04,419 INFO sqlalchemy.engine.base.Engine UPDATE users SET name=? WHERE users.id = ?
2019-08-21 22:52:04,419 INFO sqlalchemy.engine.base.Engine ('edy', 1)
2019-08-21 22:52:04,419 INFO sqlalchemy.engine.base.Engine COMMIT
# ログ出力されているSQL文からレコードの更新(UPDATEが行われていることが分かる)
カラムの削除
削除は簡単です。以下のように行えばOK
>>> session.query(User).filter(User.name=='edy').delete()
>>> session.commit()
# 消えたか確認してみる
>>> ed_user = session.query(User).filter(User.name=='edy').first()
>>> ed_user
>>>
# ed_userを出力しようとしてみたが、何も出力されない
# これはqueryの結果が空であったことを示す。ちゃんと削除されたようだ。
まとめ
この記事でのSQLAlchemyの説明は以上です。
テーブル同士にリレーションを張ったり、queryを投げる際にjoinを使ったりなんてこともSQLAlchemyではできますが、その説明は別の記事で説明しようと思います。
最後に”全部読めたら基本はSQLAlchemyの基本はバッチリ”というコードを準備したので、知識の定着度合いを確認してみてください。
変数にはどんな値が入っているのか、DBはどんな状態なのか考えながら読み進めてみましょう。
最後に出力されるall_usersの値が分かるでしょうか?参考サイトの下に答えを載せてるので、想像と合っているか確認してみましょう。
from sqlalchemy import create_engine
from sqlalchemy import Table, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
engine = create_engine('sqlite:///:memory:', echo=True)
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
password = Column(String)
def __init__(self, name, fullname, password):
self.name = name
self.fullname = fullname
self.password = password
def __repr__(self):
return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
ed_user = User('ed', 'Ed Jones', 'edspassword')
session.add(ed_user)
session.commit()
ed_user2 = User('ed2', 'Ed2 Jones', 'ed2spassword')
session.add(ed_user2)
ed_user = session.query(User).filter(User.name=='ed').first()
ed_user.name = 'edy'
session.query(User).filter(User.name=='ed').delete()
session.commit()
all_users = session.query(User).all()
print(all_users) # 参考サイトの下に答え書いてあるから見ないよう気をつけてください。
参考サイト
- SQLAlchemy公式サイト
- Object Relational Tutorial
- SQLAlchemy 0.6.5 ドキュメント (和訳)
- とりあえず使えそうな SQLAlchemy 入門(※ ORM機能は使いません)
- 「インメモリデータベース」ってなんだ?!
最後のall_usersの値
ちゃんと分かったかな?
外れてたらもう一回説明読み直してみてね。
[<User(‘edy’,’Ed Jones’, ‘edspassword’)>, <User(‘ed2′,’Ed2 Jones’, ‘ed2spassword’)>]