Flask入門|内部結合と外部結合を理解しよう【チャプター5-05】

ながみえ

一つ前のページではSQLAlchemyの基本について学習しました。

今回は 内部結合と外部結合 について見ていきましょう。

Chapter1:Flask入門編
Chapter2:Jinja2入門編
Chapter3:フィルター編
Chapter4:フォーム編
Chapter5:データベース編

 ・Chapter5-1:データベースとは何か
 ・Chapter5-2:データベースを作ろう
 ・Chapter5-3:データベースを操作しよう
 ・Chapter5-4:SQLAlchemyを使おう
 ・Chapter5-5:内部結合と外部結合を理解しよう ◁今回はここ
 ・Chapter5-6:relationshipを理解しよう
 ・Chapter5-7:Flask-SQLAlchemyを使おう
 ・Chapter5-8:Flask-Migrateを使おう
Chapter6:Flaskの便利機能編
Chapter7:アプリ開発編

データベースでは、複数のテーブルに分割して保存された情報を必要に応じて結びつける必要があります。

このとき使用するのが「結合(JOIN)」という操作です。現実のアプリ開発でも非常によく使われます。

今回は、PythonのORMライブラリ「SQLAlchemy」を使って、次の2つの結合方法を学びます。

  • 内部結合とは何か?
  • 外部結合とは何か?
  • join()メソッドの意味と使い方
  • outerjoin()メソッドの意味と使い方
  • 複数テーブルを結合したORMクエリの書き方

本記事は 有料記事(100円)ですが、現在は期間限定で無料公開中です。

<<前のページ

Flaskの記事一覧

次のページ>>

結合処理に必要なSQLAlchemyの基本コード

以下のコードはここまでの記事で解説した「SQLAlchemyを使ったテーブル作成・データ登録の基本形」です。

今回はこのデータ構造を前提として、結合処理の使い方に焦点を当てて解説を進めていきます。

以下の3つのテーブルが登場します:

  • books(書籍情報)
  • libraries(図書館情報)
  • stocks(図書館ごとの蔵書冊数)

以下のコードの中に分からない部分がある場合は、過去の記事に戻って復習しましょう。

import os
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# ====================================
# データベースのパス(保存場所)を設定
# ====================================
base_dir = os.path.dirname(__file__) # 現在のファイルがあるディレクトリのパスを取得して、変数base_dirに代入
database = 'sqlite:///' + os.path.join(base_dir, 'data.sqlite')
    # base_dir内のdata.sqliteというファイルへのパスを作成し、
    # SQLite用の接続文字列(URL形式)を追加して変数databaseに代入
## データベースエンジンを作成
db_engine = create_engine(database, echo=True) # create_engine関数を使って、DB接続用のエンジンを作成
Base = declarative_base() # SQLAlchemyに対応した新しい“親クラス”を生成し、変数Baseに代入
    # declarative_baseは「クラスを返す」という特殊な関数

# ====================================
# モデル(テーブルを作成するクラス)
# ====================================
## 商品書籍テーブル
class Book(Base): # Baseクラスを継承したBookクラスの定義
    # テーブル名
    __tablename__ = 'books'
    # 書籍ID
    book_id = Column(Integer, primary_key=True)
    # 書籍名
    book_name = Column(String(255), nullable=False, unique=True)
    # 著者名
    author = Column(String(255), nullable=False)

## 図書館テーブル
class Library(Base): # Baseクラスを継承したLibraryクラスの定義
    # テーブル名
    __tablename__ = 'libraries'
    # 図書館ID
    library_id = Column(Integer, nullable=False, primary_key=True)
    # 図書館名
    library_name = Column(String(255), nullable=False, unique=True)

## 蔵書冊数テーブル
class Stock(Base): # Baseクラスを継承したStockクラスの定義
    # テーブル名
    __tablename__ = 'stocks'
    # 図書館id
    library_id = Column(Integer, primary_key=True)
    # 書籍ID
    book_id = Column(Integer, primary_key=True)
    # 蔵書冊数
    stock = Column(Integer)

# ====================================
# テーブル操作
# ====================================
print('テーブルを削除してから作成')
Base.metadata.drop_all(db_engine)   # データベースの削除(初期化)
Base.metadata.create_all(db_engine) # データベースの作成 & テーブルの作成

## セッションの生成
session_maker = sessionmaker(bind=db_engine) # sessionmakerクラスからセッションファクトリを作成し、変数session_makerに代入
session = session_maker() # session_makerをインスタンス生成し、変数sessionに代入

## データ作成
print('データ登録:実行')
# 書籍データ
book01 = Book(book_id=1, book_name='吾輩は猫である', author='夏目漱石') # Bookクラス(モデル)をインスタンス生成し、book01に代入
book02 = Book(book_id=2, book_name='走れメロス', author='太宰治')
book03 = Book(book_id=3, book_name='銀河鉄道の夜', author='宮沢賢治')
book04 = Book(book_id=4, book_name='人間失格', author='太宰治')
session.add_all([book01, book02, book03, book04]) # .add_allメソッドにて、リストをテーブルに一括登録
session.commit() # 上の操作をDBに書き込み・保存
# 図書館データ
library01 = Library(library_id=1, library_name='第1図書館')
library02 = Library(library_id=2, library_name='第2図書館')
session.add_all([library01, library02])
session.commit()
# 蔵書冊数データ
stock01 = Stock(library_id=1, book_id=1, stock=1)
stock02 = Stock(library_id=1, book_id=2, stock=2)
stock03 = Stock(library_id=1, book_id=3, stock=0)
stock04 = Stock(library_id=2, book_id=1, stock=0)
stock05 = Stock(library_id=2, book_id=2, stock=1)
stock06 = Stock(library_id=2, book_id=3, stock=1)
session.add_all([stock01, stock02, stock03, stock04, stock05, stock06])
session.commit()

## 内部結合
# ここに内部結合のコード例を追記

## 外部結合
# ここに外部結合のコード例を追記

## 練習問題
# ここに練習問題の解答を追記

内部結合(INNER JOIN)をSQLAlchemyで実装する方法

【Python】勉強猫がノートパソコンを前にして学習を始める様子。記事内の学習スタート用イラスト

内部結合とは|共通データだけを抽出するJOINの基本

内部結合(INNER JOIN)は、両方のテーブルに共通するデータだけを取り出す結合方法です。

結合条件に一致するレコード(テーブルの行)が存在しない場合は、その行は結果に含まれません。

内部結合の基本構文|SQLAlchemyでの書き方

session.query(テーブルA, テーブルB.カラム名).join(テーブルB, 結合条件).all()
  • query():取得したいテーブルやカラムを指定
  • join():内部結合するテーブルと条件を指定
  • .all():結果をすべて取得
session.query()メソッドとは

FlaskとSQLAlchemyでデータベースを操作する際、ほぼ必ず登場するのが session.query() という記述です。

このメソッドは、「どのテーブルから、どのカラムを取得するのか」をSQL文のように指定するためのものです。

SQLAlchemyでの内部結合の具体例と実行結果

以下のコードを、このページの冒頭で紹介した「chapter5-5.py」の末尾に追記して下さい。

print('データ参照:実行')
print('■:内部結合')
join_3tables_all = session.query( # ORMによるクエリの構築 ⇒ 変数join_3tables_allに代入
    Library,        # 図書館テーブルの全カラム(カラム=列)を取得
    Book.book_name, # 書籍テーブルのbook_nameカラムを取得
    Stock.stock     # 蔵書冊数テーブルのstockカラムを取得
).join(Stock, Library.library_id == Stock.library_id # Library と Stock を library_id で内部結合
).join(Book, Book.book_id == Stock.book_id           # Stock と Book を book_id で内部結合
).all() # クエリを実行し、すべての結果をリストとして取得

for row in join_3tables_all:
    print(f"図書館:{row.Library.library_name} -> 書籍名:{row.book_name} -> 蔵書冊数:{row.stock}")

このクエリでは、次の3テーブルを内部結合しています:

  • Library:図書館の基本情報
  • Stock:蔵書情報(図書館IDと書籍IDを持つ)
  • Book:書籍情報

つまり「どの図書館に、どの本が何冊あるか」を一覧で表示するものです。

join()はテーブルを一つずつ段階的に結合していくことができます。この例では2回のjoin()を使って、3つのテーブルを結びつけています。

VSCodeのSQLite Viewerを使用して、出来上がった表を視覚的に確認してみましょう。

【Python】勉強猫がノートパソコンを見ながら考え込む様子。記事内の休憩用イラスト

外部結合(OUTER JOIN)の仕組みとSQLAlchemyでの使い方

外部結合とは|NULLも取得できる柔軟なJOIN

外部結合(OUTER JOIN)は、一方のテーブルにデータが存在しない場合でも、もう一方のテーブルのデータを取得する結合方法です。

主に以下の2種類がありますが、ここでは「左外部結合(LEFT OUTER JOIN)」を扱います。

外部結合の基本構文|outerjoin()の使い方

session.query(テーブルA, テーブルB.カラム名).outerjoin(テーブルB, 結合条件).all()
  • query():取得したいテーブルやカラムを指定
  • outerjoin():指定したテーブルと外部結合を行う
  • .all():結果をすべて取得

SQLAlchemyで外部結合を実行する方法とサンプルコード

以下のコードを、このページの冒頭で紹介した「chapter5-5.py」の末尾、先ほどの内部結合のコードの下に追記して下さい。

## リストの追加
print('■' * 20)
print('■:外部結合')
outerjoin_2tables_all = session.query( # ORMによるクエリの構築 ⇒ 変数outerjoin_2tables_allに代入
    Book,        # 書籍テーブルの全カラムを取得
    Stock.stock  # 蔵書冊数テーブルのstockカラムを取得
).outerjoin(Stock, Book.book_id == Stock.book_id # Bookを基点にStockを外部結合
).all() # クエリを実行し、すべての結果をリストとして取得

for row in outerjoin_2tables_all:
    print(f"書籍名:{row.Book.book_name} -> 在庫数:{row.stock}")

このコードでは、「書籍」を基点として外部結合を行っています。

つまり、すべての書籍を取得し、該当する在庫情報がある場合は在庫数を表示、なければ「None」と表示されます。

たとえば、データベースに存在するすべての書籍を一覧表示したいとき、特定の図書館にその書籍が未登録でも表示したい場合に使います。

VSCodeのSQLite Viewerを使用して、出来上がった表を視覚的に確認してみましょう。

まとめ|JOINを理解してSQLAlchemyを使いこなそう

今回は、SQLAlchemyを使って内部結合と外部結合を実行する方法を学びました。

  • 内部結合では、両方のテーブルに共通するデータだけを抽出
  • 外部結合では、結合元のテーブルのデータをすべて取得できる
  • join()outerjoin()の違いと使い分け方
  • 複数のテーブルを連結して、複雑なデータ構造を扱う方法

SQLAlchemyの結合は初めは複雑に感じるかもしれませんが、一度理解できると非常に強力なツールになります。

データを自在に操るための第一歩として、ぜひコードを自分でも書いて試してみてください。

次は、より高度な結合方法である relationshipの活用方法 を学びます!

練習問題|session.query()の基本を使って各テーブルを出力しよう

【Python】勉強猫がノートパソコンに向かい、練習問題に挑戦する様子。記事内の休憩用イラスト

本問題では、あえて「結合」を使わずに、書籍テーブル・図書館テーブル・蔵書冊数テーブルをそれぞれ単独で表示するコードを書いてみましょう。

各テーブルの内容をそのまま出力する session.query() の使い方を復習しつつ、解説記事中のコードと見比べて結合の効果を実感しましょう。

この問題の要件

以下の要件に従ってコードを完成させてください。

  • session.query()を使って、3つのテーブルそれぞれから全件を取得すること
  • 書籍テーブル(Book)からは「書籍ID・書籍名・著者名」を日本語で出力すること
  • 図書館テーブル(Library)からは「図書館ID・図書館名」を日本語で出力すること
  • 蔵書冊数テーブル(Stock)からは「図書館ID・書籍ID・在庫数」を日本語で出力すること
  • 各出力処理にはループ(for文)を用いて全データを表示すること
  • 出力結果は日本語で見やすく表示すること(例:図書館ID:1|書籍ID:1|在庫数:1

ただし、以下のような実行結果となるコードを書くこと。

▼ 書籍テーブルの全データ
ID:1|書籍名:吾輩は猫である|著者:夏目漱石
ID:2|書籍名:走れメロス|著者:太宰治
ID:3|書籍名:銀河鉄道の夜|著者:宮沢賢治
ID:4|書籍名:人間失格|著者:太宰治

▼ 図書館テーブルの全データ
ID:1|図書館名:第1図書館
ID:2|図書館名:第2図書館

▼ 蔵書冊数テーブルの全データ
図書館ID:1|書籍ID:1|在庫数:1
図書館ID:1|書籍ID:2|在庫数:2
図書館ID:1|書籍ID:3|在庫数:0
図書館ID:2|書籍ID:1|在庫数:0
図書館ID:2|書籍ID:2|在庫数:1
図書館ID:2|書籍ID:3|在庫数:1

この問題を解くヒント

1からコードを組み立てることが難しい場合は、以下のヒントを開いて参考にしましょう。

Q
ヒント:コードの構成を見る

正解のコードは上から順に以下のような構成となっています。

1:書籍テーブルの全データを取得して表示する処理
 → session.query()を使ってBookテーブルの全レコードを取り出し、変数に代入
 → for文で1行ずつ取り出し、書籍ID・書籍名・著者名を日本語で表示
 → 表示の前に「何のデータを出力しているのか」がわかるようタイトル文を print() で出力

2:次に、図書館テーブルの全データを取得して表示する処理
 → session.query()を使ってLibraryテーブルの全レコードを取り出し、変数に代入
 → for文で1行ずつ取り出し、図書館IDと図書館名を日本語で表示
 → 書籍と同じようにタイトル文も付ける

3:最後に、蔵書冊数テーブルの全データを取得して表示する処理
 → session.query()を使ってStockテーブルの全レコードを取り出し、変数に代入
 → for文で1行ずつ取り出し、図書館ID・書籍ID・在庫数を日本語で表示
 → 書籍と同じようにタイトル文も付ける

このヒントを見てもまだ回答を導き出すのが難しいと感じる場合は、先に正解のコードと解説を見て内容を理解するようにしましょう。

正解コード

例えば、以下のようなプログラムが考えられます。

Q
正解コード
# 書籍テーブルをそのまま表示
print('▼ 書籍テーブルの全データ')
books = session.query(Book).all()
for book in books:
    print(f"ID:{book.book_id}|書籍名:{book.book_name}|著者:{book.author}")

# 図書館テーブルをそのまま表示
print('▼ 図書館テーブルの全データ')
libraries = session.query(Library).all()
for lib in libraries:
    print(f"ID:{lib.library_id}|図書館名:{lib.library_name}")

# 蔵書冊数テーブルをそのまま表示
print('▼ 蔵書冊数テーブルの全データ')
stocks = session.query(Stock).all()
for stock in stocks:
    print(f"図書館ID:{stock.library_id}|書籍ID:{stock.book_id}|在庫数:{stock.stock}")

正解コードの詳細解説

正解コードをブロックごとに分割して解説します。

Q
正解コードの詳細解説

書籍テーブルの全データを表示

print('▼ 書籍テーブルの全データ')
books = session.query(Book).all()
for book in books:
    print(f"ID:{book.book_id}|書籍名:{book.book_name}|著者:{book.author}")
  • book.book_id, book.book_name, book.author
     → それぞれ「書籍ID」「書籍名」「著者名」のカラムにアクセスしています。
  • print('▼ 書籍テーブルの全データ')
     → タイトルとして何のデータを出力するのかを画面に表示しています。日本語で書くことで分かりやすくなっています。
  • session.query(Book).all()
     → SQLAlchemyのsession.query()メソッドを使って、Bookテーブルのすべてのレコードを取得しています。
     → .all()をつけることで「全件取得」になります。
  • for book in books:
     → 取得したレコード(リスト)を1件ずつ取り出してループ処理しています。

図書館テーブルの全データを表示

print('▼ 図書館テーブルの全データ')
libraries = session.query(Library).all()
for lib in libraries:
    print(f"ID:{lib.library_id}|図書館名:{lib.library_name}")
  • lib.library_id, lib.library_name
     → 図書館のIDと名前を出力しています。libは図書館(library)の略です。
  • session.query(Library).all()
     → Libraryテーブルから全レコードを取得しています。
  • for lib in libraries:
     → それぞれの図書館情報を1件ずつ表示するループ処理です。

蔵書冊数テーブルの全データを表示

print('▼ 蔵書冊数テーブルの全データ')
stocks = session.query(Stock).all()
for stock in stocks:
    print(f"図書館ID:{stock.library_id}|書籍ID:{stock.book_id}|在庫数:{stock.stock}")
  • stock.library_id, stock.book_id, stock.stock
     → 「どの図書館に」「どの書籍が」「何冊あるか」の情報を日本語で出力しています。
  • session.query(Stock).all()
     → Stockテーブル(図書館と書籍の在庫情報)をすべて取得しています。
  • for stock in stocks:
     → 各在庫情報を1件ずつ表示しています。
もっと分かりやすい学習サイトにするために

この記事を読んで「ここが分かりにくかった」「ここが難しかった」等の意見を募集しています。

世界一わかりやすいFlask学習サイトにするため、ぜひ 問い合わせフォーム からご意見下さい。

<<前のページ

Flaskの記事一覧

次のページ>>

FAQ|内部結合と外部結合のよくある疑問

Q
Q1. SQLAlchemyの内部結合と外部結合は何が違いますか?

内部結合は、両方のテーブルに共通するデータだけを取得します。一方、外部結合(特に左外部結合)は、片方のテーブルにしかないデータも含めて取得できます。用途に応じて使い分けます。

Q
Q2. outerjoin()を使った結果にNoneが表示されるのはなぜですか?

外部結合では、結合先のテーブルに該当データが存在しない場合、対応するカラムがNoneになります。これは正常な動作であり、「データが存在しないこと」を示しています。

Q
Q3. 複数のテーブルを結合するときにjoinを何回も使って良いのですか?

はい、複数テーブルを結合する場合はjoin()やouterjoin()を連続で使うことができます。ただし、結合条件を正しく指定しないと意図しない結果になるので注意が必要です。

記事URLをコピーしました