MENU

【超便利】SQLAlchemyでデータベースを操作しよう(基礎編)

データベースを扱うとき、SQLを直接書くのは面倒だと感じたことはありませんか?

SQLAlchemy は、Pythonでデータベースを操作するための強力なライブラリで、特に ORM(オブジェクトリレーショナルマッピング) を使えば、SQLを書かずにPythonのクラスとメソッドでデータを操作できます。

本記事では、SQLAlchemyのORMを使って データベースの作成、テーブルの定義、データの追加・取得 までを 基礎から解説 します。これを読めば、SQLAlchemyの基本が理解でき、スムーズにデータベース操作を行えるようになります。

目次

SQLAlchemyとは

SQLAlchemyは、Pythonでデータベースを操作するための強力なSQLツールキットであり、ORM(Object-Relational Mapper)機能も提供するライブラリです。データベースとのやり取りを効率化し、開発者の生産性を向上させるために広く利用されています。

あわせて読みたい
SQLAlchemy The Database Toolkit for Python

SQLAlchemyの特徴

  • ORM(Object-Relational Mapper)
    データベースのテーブルをPythonのオブジェクトとして扱い、オブジェクト指向のプログラミングでデータベースを操作できます。
  • SQL Expression Language
    Pythonのコード上で生のSQLクエリを実行できるため、複雑なクエリやデータベース固有の機能の柔軟な対応が可能です。
  • クロスデータベース対応
    PostgreSQL、MySQL、SQLite、Oracle、SQL Serverなど、幅広いデータベースに対応し、データベースの種類を意識せず、共通のインターフェースで操作できます。
  • 接続管理の簡便性
    データベースごとに接続URIを指定することで、簡単に切り替えが可能です。
  • 安全性と可読性
    生のSQL文を直接実行できる一方で、text()を用いたパラメータバインディングが可能であり、SQLインジェクションのリスクを軽減できます。
  • メタデータとユーティリティ
    データベースのメタ情報(テーブル一覧やスキーマ情報など)をプログラムで取得できる

データベース(DB)個別ライブラリとの違い

Python からDBに接続する場合、DBごとに用意されたライブラリを用いる方法もよく用いられますので、SQLAlchemyとDBごとの専用ライブラリの違いを簡単にまとめておきます。

特性SQLAlchemyDBごとの個別ライブラリ
対応データベース複数のDBに対応(汎用的)特定のDBに特化
ORMのサポートサポート(便利でコードが簡潔)サポートなし
低レベルSQL柔軟に記述可能完全対応(特定DBの機能に最適化)
データベース特有機能限定的(一般的機能が中心)完全対応(特定DBに特化した機能を活用)
移植性高い低い

インストール手順

SQLAlchemyを使うに際しては、SQLAlchemyのインストールの他、接続したいDBごとのライブラリをインストールする必要があります。

SQLAlchemyは、次のpip コマンドでインストールできます。

pip install sqlalchemy

次に、Pythonプログラムの中で下記のインポートを行ってください。

from sqlalchemy import create_engine, Column, Integer, String, Float, func, select
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy.exc import SQLAlchemyError

接続したいDBごとに、ライブラリのインストールとインポートが必要です。下記の一覧を参考にしてください。

DB名称インストール方法import すべき内容
Oraclepip install cx_Oracleimport cx_Oracle
MySQLpip install mysqlclient
又は pip install pymysql
import pymysql; ymysql.install_as_MySQLdb()
PostgreSQLpip install psycopg2
又は pip install psycopg2-binary
import psycopg2
SQL Server、
ODBC
pip install pyodbcimport pyodbc
JDBCpip install JayDeBeApiimport jaydebeapi
SQLite不要import sqlite3

DBへの接続

SQLAlchemyでは、DBごとに用意された接続URLをcreate_engine の引数に指定することで、自動的に該当するドライバを選択してくれます。
また、create_engine()のecho引数をTrueにすることで、SQLAlchemyが内部で生成するSQL文をログ(通常はコンソール)に出力させることが可能です。

from sqlalchemy import create_engine

# SQLiteを使用した例(他のDBでもURLを調整すれば同様に使用可能)
engine = create_engine(r"sqlite:///p:/example.db", echo=True)
Base = declarative_base()
Session = sessionmaker(bind=engine)

URLの記述には、次のルールがあります。

ドライバ名://ユーザー名:パスワード@ホスト名:ポート/DB名

例えば、PostgreSQLに接続したい場合、次のように記述します。

# PostgreSQLへの接続例
user = "hoge"
password = "hoge0123"
host = "hohoho@server.com"
port = "5432"
dbname = "mydb"
driver = "postgresql+psycopg2"

engine = create_engine(f"{driver}://{user}:{password}@{host}:{port}/{dbname}")
Base = declarative_base()
Session = sessionmaker(bind=engine)

下記の内容を参考に、上記 プログラムの driver、port、dbname を、適宜書き換えてご利用ください。

データベースdriverportdbname に記載すべき内容
Oracleoracle+cx_oracle1521サービス名またはSID
MySQLmysql+mysqlconnector
又は mysql+pymysql
3306データベース名
PostgreSQLpostgresql+psycopg25432データベース名
SQL Servermssql+pyodbc1433ODBCデータソース名(DSN)
SQL Servermssql+pymssql1433データベース名
SQLitesqlite-ファイルのパス
JDBCjdbc+jaydebeapi-JDBC接続URL

テーブルの作成

クラスの構造がそのままテーブルになるため、クラスを定義します。この時、Baseを必ず継承してください。

# クラスを定義
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=False)
    age = Column(Integer)
    score = Column(Float)

# テーブル作成
Base.metadata.create_all(engine)

このプログラムでは、 id にプライマリキーを設定し、 autoincrement=True により自動的に連番が振られるように設定しています。

もし、複合キーを指定したい場合、 PrimaryKeyConstraintを使って次のようにプライマリキーを作成します。

class User(Base):
    __tablename__ = "users"

    # カラムを定義
    id = Column(Integer, autoincrement=True)
    name = Column(String, nullable=False)
    age = Column(Integer)
    score = Column(Float)

    # 複合主キーを定義
    __table_args__ = (
        PrimaryKeyConstraint('id', 'name'),  # 複数のカラムを主キーとして指定
    )

インデックスの作成

インデックスはテーブル作成時に指定することも可能です。
但し、複合キーワードによるインデックスは作成できません。

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=False, index=True)  # name列にインデックスを作成
    age = Column(Integer)
    score = Column(Float)

複合インデックスを作成する場合は、Index()を使います。

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=False)
    age = Column(Integer)
    score = Column(Float)

    # 複合インデックスを定義
    __table_args__ = (
        Index('ix_name_age', 'name', 'age'),  # name と age を含む複合インデックス
    )

インデックスは後から自由に追加が可能です。

# インデックスの追加
Index('ix_users_name', User.name).create(bind=engine) 

残念ながら、sqlalchemyはインデックスの削除をサポートしていません
インデックスを削除したい場合は、直接DBに対して後述するexecute() を用いて、SQL(Drop Index文)を実行する必要があります。

データの更新(INSERT、UPDATE、DELETE)

SQLAlchemyは、テーブルをクラスとして、またレコードをオブジェクトとして扱います。このため、追加、更新、削除はSQLを組み立てる必要がなく、Sessionオブジェクトや filter_by()add()delete()などの専用メソッドを使って行うことが可能です。

追加session.add()の引数に、挿入したいオブジェクトを指定
更新filter_by() で絞り込んだオブジェクトの値を直接変更
削除session.delete()の引数に、filter_by()で絞り込んだオブジェクトを指定

SQLAlchemyによるデータ更新では、内部的にデータとクラスのマッピング処理を1件づつ行っているため、SQLのような一括でのUPDATEDELETEができません。複数のデータをまとめて更新する場合でも、for ループを用いて1件づつ処理する必要があります。

また、データの更新が完了したら、 commit()メソッドを呼んで更新内容を確定させます。
rollback() メソッドを呼ぶことで、更新内容を破棄することも可能です。

データの追加(INSERT)

データの追加は、Sessionオブジェクトのadd()メソッドに、挿入したいオブジェクトを指定します。

with Session() as session:
    # データのインサート
    user = User(name="Alice", age=30, score=85.5)
    session.add(user)
    user = User(name="maria", age=28, score=96.7)
    session.add(user)
    session.commit()

データの変更(UPDATE)

データの変更は、対象となるオブジェクトを filter_by()で絞り込んでおき、そのオブジェクトのプロパティを変更するだけです。

with Session() as session:
    # データの変更
    user = session.query(User).filter_by(name="Alice").first()
    if user:
        user.age = 31
        session.commit()

データの削除(DELETE)

データの削除は、Sessionオブジェクトのdelete()を使います。削除対象のオブジェクトをfilter_by()で絞り込み、結果をdelete() の引数に指定します。

with Session() as session:
    # データの削除
    user = session.query(User).filter_by(name="Alice").first()
    if user:
        session.delete(user )
        session.commit()

トランザクション処理

SQLAlchemyでトランザクション処理を行う場合は、一般的なDBと同様にtry ~ except ~ commit()rollback() を組み合わせます。

下記のサンプルでは、データが一意となるような条件を filter_by() に指定し、first() で1件のみ取得しています。
複数のデータに対して処理したい場合は、for ループを使って1件づつ処理してください。

データの更新が完了したら、 commit()メソッドを呼んで更新内容を確定させていますが、エラー発生時はrollback() メソッドが呼ばれるので、それまでの更新内容が破棄されます。

with Session() as session:
    try:
        # インサート
        new_user = User(name="Alice", age=30, score=85.5)
        session.add(new_user)
        new_user = User(name="maria", age=28, score=96.7)
        session.add(new_user)

        # アップデート
        user_to_update = session.query(User).filter_by(name="Alice").first()
        if user_to_update:
            user_to_update.age = 31

        # デリート
        user_to_delete = session.query(User).filter_by(name="Alice").first()
        if user_to_delete:
            session.delete(user_to_delete)

        # すべての変更を一括でコミット
        session.commit()

    except Exception as e:
        session.rollback()  # エラー時にロールバック
        print(f"エラー発生: {e}")

データの検索

Session オブジェクトを使ってデータの検索を行います。データを取得したいカラム名(クラスのプロパティ)と検索条件式を指定し、最後に取得メソッドを読み出します。

session.query(カラム名1,カラム名2,・・・).filter(検索条件1,検索条件2,・・・).取得メソッド

with Session() as session:
    results = session.query(User).filter(User.age > 25).all()
    for user in results:
        print(user.name, user.age, user.score)

カラム名(query)

queryに指定するカラム名は、クラスのプロパティを指定します。必要な数だけカンマ区切りで列挙してください。
クラス名のみを指定すると、そのクラスが持つプロパティが全て抽出されます。

query(カラム名1,カラム名2,・・・)
query(クラス名)

# プロパティを列挙する場合
session.query(user.name, user.age, user.score)

# クラスを指定する場合
session.query(user)

検索条件式(filter)

検索条件式は、クラスのプロパティに対して論理演算子を使って指定します。複数の条件式がある場合は、カンマ区切りで列挙します。

filter(検索条件1,検索条件2,・・・)

カンマ区切りで列挙した場合、全ての条件式は AND条件として動作しますが、and_() や or_() メソッドを使うことで、明示的に指定することも可能です。

# 条件式をカンマ区切りで列挙すると、AND条件として動作
filter(User.age > 25, User.score > 80)

# and_() メソッドにより明示的にAND条件で動作させることが可能
filter(and_(User.age > 25, User.score > 80))

# or_() メソッドにより明示的にOR条件で動作させることが可能
filter(or_(User.age < 18, User.score < 50))

取得メソッド

filter() に続けて取得メソッドを記述します。

取得メソッド動作
all()検索条件に一致した全ての結果を取得する。
first()検索条件に一致した最初の1件目を取得する。
one()結果を1件だけ取得する。2件以上返される場合はエラーとなる。
yield_per(件数)指定した件数だけ取得する。ループで順番に取り出すことが可能。
offset(件数)指定した件数を読み飛ばした後に取得する。
limit(件数)検索条件に一致した結果から指定した件数を取得する。
scalar()値を1つだけ取得する。

取得メソッドを指定しない場合、イテレーターとして動作します。従って、 for ループを用いて1件づつ結果を取り出すことが可能です。

with Session() as session:
    results = session.query(User).filter(User.age > 25)
    for user in results:
        print(user.name, user.age, user.score)

グルーピング集計

グルーピングは、group_by() にグルーピングしたいカラム名(クラスのプロパティ)を列挙し、query() 内に集計関数を指定します。

query(カラム名1,カラム名2,・・・,集計関数1(カラム名3),・・・)

with Session() as session:
    results = session.query(User.age, func.avg(User.score)).group_by(User.age).all()
    for age, avg_score in results:
        print(f"Age: {age}, Average Score: {avg_score}")

集計関数には、次のような種類があります。より詳細な情報は、SQLAlchemy の公式ページ でご確認ください。

スクロールできます
カテゴリ関数説明
集計関数func.sum合計値を計算session.query(func.sum(User.score)).all()
func.min最小値を取得session.query(func.min(User.score)).all()
func.max最大値を取得session.query(func.max(User.score)).all()
func.count件数をカウントsession.query(func.count(User.id)).all()
func.avg平均値を計算session.query(func.avg(User.score)).all()
文字列操作func.lower文字列を小文字に変換session.query(func.lower(User.name)).all()
func.upper文字列を大文字に変換session.query(func.upper(User.name)).all()
func.concat文字列を結合session.query(func.concat(User.first_name, " ", User.last_name)).all()
数学関連func.abs絶対値を計算session.query(func.abs(User.score - 100)).all()
func.round小数点以下を丸めるsession.query(func.round(User.score, 2)).all()
func.pow指数計算を行うsession.query(func.pow(User.score, 2)).all()
日付関連func.now現在日時を取得session.query(func.now()).all()
func.date日付部分を抽出session.query(func.date(User.created_at)).all()
func.year年を抽出session.query(func.year(User.created_at)).all()
func.month月を抽出session.query(func.month(User.created_at)).all()
func.day日を抽出session.query(func.day(User.created_at)).all()

複数テーブル(=複数クラス)の結合

複数クラスを結合する場合、 query()に結合対象のカラム名(クラスのプロパティ)をカンマ区切りで列挙し、filter() に結合条件を記述します。

query(カラム名1,カラム名2 , ・・・).filter(結合条件1,結合条件2,・・・)

# 新しいクラスを定義
class Address(Base):
    __tablename__ = "addresses"
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer)
    city = Column(String)

Base.metadata.create_all(engine)

# Userと Adrees を結合する
with Session() as session:
    # 結合クエリ
    results = session.query(User, Address).filter(User.id == Address.user_id).all()
    for user, address in results:
        print(user.name, address.city)

もう1つの方法として、join() を使う方法もあります。

query(カラム名1,カラム名2,・・・).join(クラス2,結合条件)

results = session.query(User, Address).join(Address, User.id == Address.user_id).all()
for user, address in results:
    print(user.name, address.city)

定義したクラスに外部キーが指定されている場合、SQLAlchemy はキーを推測するため、結合条件の記述を簡略化できます。

    results = session.query(User, Address).join(Address).all()
    for user, address in results:
        print(f"User: {user.name}, City: {address.city}")

並べ替え、重複排除、件数カウントなど

query()に続けて、並べ替えや重複排除、件数カウントなどのメソッドを呼ぶことが可能です。

# 並べ替え
session.query(User).order_by(User.age.desc()).all()

# 重複データの排除
session.query(User.age).distinct().all()

# 件数カウント
session.query(User).filter(User.age > 30).count()

# gropu_by と組み合わせることで、having が利用可能
session.query(User.age, func.count(User.id)).group_by(User.age).having(func.count(User.id) > 1).all()

テーブル一覧の取得

データベース内のテーブル名を全て取得したい場合, inspectget_table_names() を使います。

from sqlalchemy import inspect

tables = inspect(engine).get_table_names() 
print(tables)

結果は、テーブル一覧がリスト形式で返されます。

['addresses', 'users']

指定したテーブルのカラム一覧取得

指定したテーブルのカラム名を全て取得したい場合, inspectget_columns() メソッドの引数に、テーブル名を指定します。

from sqlalchemy import inspect

inspector = inspect(engine)
columns = inspector.get_columns("users")
print(columns)

結果は、カラム名やデータ型、NULL許可、デフォルト値、プライマリキーの状態が辞書で返されます。

{'name': 'id', 'type': INTEGER(), 'nullable': False, 'default': None, 'primary_key': 1}, {'name': 'name', 'type': VARCHAR(), 'nullable': False, 'default': None, 'primary_key': 0}, {'name': 'age', 'type': INTEGER(), 'nullable': True, 'default': None, 'primary_key': 0}, {'name': 'score', 'type': FLOAT(), 'nullable': True, 'default': None, 'primary_key': 0}]

ストアドプロシージャ等の実行

DBに登録されているストアドプロシージャや、SQLiteに用意されているプラグマなど、そのDBに特化した機能を使いたい場合、execute() を用いて直接クエリーやプラグマを実行できます。

with engine.connect() as connection:
    # ストアドプロシージャの呼び出し例(MySQLなどで使用可能)
    result = connection.execute("CALL some_stored_procedure();")
    
    # SQLiteのプラグマ実行例
    connection.execute("PRAGMA foreign_keys=ON;")

まとめ

今回は、 ORMの基本的な使い方(モデルの定義、データの追加・取得) を解説しました。

SQLAlchemyのORMを使えば、データベースをPythonのクラスとして扱い、SQLを書かずに簡単にデータ操作ができます。

次のステップとして、 より高度なクエリの実行方法や、生SQL(executeメソッド)を使った操作 も学ぶと、さらに自由度の高いデータベース操作ができるようになります。

続編として、SQLAlchemyの execute メソッドによる生のSQLの実行 について詳しく解説した記事「【超便利】SQLAlchemy+SQLでデータベース自由自在(応用編)」を用意しましたので、併せてご覧ください。

あわせて読みたい
【超便利】SQLAlchemy+SQLでデータベース自由自在(応用編) 案件に応じて複数のデータベース(DB)を使い分けることは一般的です。通常、DBごとに用意された専用ライブラリを使用しますが、SQLAlchemyを利用することで、DB接続や...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次