SQLAlchemyの基本的な使い方を超丁寧に解説【理解度チェックテスト付き】

2019年8月21日

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を結びつけるアダプターが必要なので注意です。

  • 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インスタンスはあくまでもDBにテーブルを作成するためのクラスであり、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オブジェクトの作成もクラスとのマッピングも同時に行うことが出来、便利です。

以下が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は取得しなかった。

カラムの更新

カラムの更新は以下の手順で更新します。

  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_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) # 参考サイトの下に答え書いてあるから見ないよう気をつけてください。

参考サイト

最後のall_usersの値

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

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