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
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.

https://www.sqlalchemy.org/

つまり、SQLAlchemyとはPythonというプログラミング言語を使ってDB操作を簡単に行うためのツールである。

SQLAlchemyを使うことで、PythonのクラスとDB内のテーブルを関連づけられるので、インスタンス生成をするようにレコードを登録したり、そのクラスから生成されたインスタンスが持つ変数に値を再代入することで、レコードの情報を更新することが出来る。

ちなみにこのようにクラスとテーブルを関連づける機能のことをORM(Object-relational mapping)と呼ぶ。

インストール方法

venvで仮想環境を作成しているなら、その仮想環境を有効化して以下のコマンドを実行。
参考: 【Ubuntu】Pythonの仮想環境をvenvで作る方法

$ pip install sqlalchemy

仮想環境は使わないという方は以下のコマンドを実行。

# pip3コマンドを使うために必要なものをインストール
$ sudo apt update
$ sudo apt install python3-pip
# SQLAlchemyのインストール
$ pip3 install sqlalchemy

上記の手順でインストールは完了。
それでは使い方の説明に移る。

DBへの足がかり作成

まず最初にどのデータベースにどうやって接続するか、設定を行う。
その設定内容を保持したものはエンジンと呼ばれる。
このエンジンが全てのDB操作の足がかりになる。

注意: >>>で始まっている行は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:と指定した場合にはインメモリデータベース内にアクセスすることになる。

インメモリデータベースは実際にデータをDBに保存するのではなく、メモリ上に記録する。そのため、ファイル実行が完了した時や、Pythonコマンドラインを抜けるなど、一連の処理が完了したら記録されていたデータは消える。

postgresqlデータベースが対象の場合、以下のようになる

>>> engine = create_engine('postgresql://username:password@hostname/database_name', echo=True)

hostnameの箇所は「IPアドレス:port番号」という書き方でも可能。postgresqlにアクセスする場合はPsycopg2のようなpythonとPostgreSQLを結びつけるアダプターが必要なので注意
【Ubuntu18】Psycopg2のインストール手順

echo=True

echoの値がTrueの場合、SQLAlchemyが発行したSQL文がログとして出力されるようになる。最初のうちはTrueにしておいて、どんなSQL文が発行されているのか確認しよう。

テーブル定義&テーブル作成

テーブル定義、およびテーブル生成をする方法は2つある。1つがMetaDataを使う方法、もう一つがdeclarative_baseを使う方法。

MetaDataを使う場合

MetaDataはテーブルオブジェクト(テーブルと紐づいたクラス)をまとめて管理するためのもの。この説明だけだとよく分からないと思うから、実際のコードを見てみよう。

from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
# MetaDataをインスタンス化
metadata = MetaData()
# DBのテーブルと紐づけるTableインスタンスを作成(第一引数がテーブル名)&
# 第二引数にmetadataを指定して、このTableインスタンスを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インスタンスはあくまでもTable情報として作成したものであり、DBからusersテーブルからレコード情報を受け取るための箱(クラス)ではない。そこでそのDBからレコード情報を受け取るための箱となるクラスを作成する必要が生まれる。
作り方は以下の通り。

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オブジェクトの作成もクラスとの関連付け(マッピング)も一気にやることができ、かなり便利。上で説明したMetaDataクラスからインスタンスを作成する方法よりもこちらのやり方の方が簡単で楽なのでおすすめ。

# 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と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は取得しなかった。

カラムの更新

カラムの更新は以下の手順で更新するのが基本

  1. 更新したいレコードの情報をクラスのままの形でqueryで取得(カラムの指定はしない)
  2. 取得したクラスの変数に再代入
  3. 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_user = session.query(User).filter_by(name='ed').first()
ed_user2 = User('ed2', 'Ed2 Jones', 'ed2spassword')
session.add(ed_user2)
all_users = session.query(User).all()
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) # 参考サイトの下に答え書いてあるから見ないよう気をつけて。

参考サイト

最後のall_usersの値

ちゃんと分かったかな?
外れてたらもう一回説明読み直してみてね。

[<User(‘edy’,’Ed Jones’, ‘edspassword’)>, <User(‘ed2′,’Ed2 Jones’, ‘ed2spassword’)>]