Flask入門|relationshipの使い方とリレーション構築の基本【チャプター5-06】

ながみえ

一つ前のページでは内部結合と外部結合について学習しました。

今回は relationship について見ていきましょう。

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:アプリ開発編

Flaskを使ってWebアプリケーションを開発する中で、データベースはとても重要な役割を担っています。

そして現実の情報をデータとして扱おうとすると、たとえば「著者と書籍」「ユーザーと投稿」「商品と注文」のように、複数のテーブルを関連付けて使いたい場面が出てきます。

今回の学習テーマは、そんな「テーブル同士をつなげる技術です。

Flaskでは SQLAlchemy というライブラリを使ってPythonのクラスとテーブルを対応させて使います。

さらに、ForeignKey(外部キー)relationship() を活用することで、親子関係のような構造を作ることができます

本記事で学ぶ内容は以下の通りです:

  • ForeignKey の意味と使い方
  • relationship() の意味と使い方
  • 実例:「著者」と「書籍」のリレーションを実装

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

<<前のページ

Flaskの記事一覧

次のページ>>

ForeignKeyの意味と外部キーの使い方を初心者向けに解説

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

ForeignKey(外部キー)とは、あるテーブルのカラムが、別のテーブルの主キーを参照するための仕組みです。

この仕組みを使うことで、「どの本がどの著者に属しているか」「どの注文がどの商品に対応しているか」といった、テーブル間の関係性を明確に表現できます。

たとえば、ある「books」というテーブルに「著者ID(author_id)」というカラムがあった場合、それは「authors」テーブルの「id」カラムを参照することで、「この本はこの著者が書いた」という情報を持つことができます。

基本構文は以下の通りです。

変数 = Column(型, ForeignKey('参照先テーブル.カラム'))

このように書くことで、変数が参照先テーブルのカラム(列)を参照する「外部キー」となり、存在する著者IDしか登録できないようになります。

これによりデータの整合性が保たれるだけでなく、関連するデータを簡単に取得できるようにもなります。

後ほど使用例コードの中で詳しく解説します。

relationship()の仕組みと1対多リレーションの構文を理解しよう

relationship()関数 はSQLAlchemyが提供する非常に便利な機能で、異なるテーブル(=クラス)間の関連性を、Pythonのクラスの中で“属性”として扱えるようにする関数です。

たとえば「本(Book)」と「著者(Author)」の関係を考えると、本からはその著者が、著者からはその人が書いた本一覧が分かるようにしたいですよね。

そのようなときに使うのが relationship() です。

リレーションには「1対1」「1対多」「多対多」などの種類がありますが、ここでは「1人の著者が複数の本を書く」=「1対多」の関係を例にして構文を紹介します。

# 子テーブルのクラス内で使用する構文
親データ = relationship('親クラス名', back_populates='子に対応する属性名')

# 親テーブルのクラス内で使用する構文
子データの一覧 = relationship('子クラス名', back_populates='親に対応する属性名')
  • 子テーブル側では、relationship() によって「親の情報」を取得できるようにします。
  • 親テーブル側では、relationship() によって「紐づいている子データの一覧」を取得できるようにします。
  • back_populates には「相手側のクラスで定義している属性名」を指定し、双方向の関連付けを可能にします。
  • relationship() はデータベース上のテーブル構造を変更するものではなく、Pythonコード内での扱いを便利にするためのものです。
  • 実際にデータを登録・取得する際に大きな力を発揮します。

こちらも次のセクションで、使用例コードの中で詳しく解説します。

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

実例|著者と書籍で1対多リレーションを構築する手順

ここでは前章で学んだ ForeignKeyrelationship() を使って、「著者と書籍」の1対多の関係を表現するプログラムを一緒に見ていきましょう。

コードの上から順に紹介しますので、是非コピーしてVSCodeに貼り付けていきながら読んでください。

必要なモジュールとDBの初期設定

import os
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
  • create_engine はデータベースと接続するための関数です。
  • Column, Integer, String, ForeignKey はテーブル定義に必要な部品です。
  • relationship を使うことでリレーションを扱えます。

なお、SQLAlchemy関連のインポートは覚える必要はありません。
Chapter5-7で学習するFlask-SQLAlchemyでまとめてインポートできるようになります。

次に、SQLite用のデータベースファイルを作成します。

# ====================================
# データベースのパス(保存場所)を設定
# ====================================
base_dir = os.path.dirname(__file__)
database = 'sqlite:///' + os.path.join(base_dir, 'data.sqlite')
## データベースエンジンを作成
db_engine = create_engine(database, echo=True)
Base = declarative_base()
  • データベースファイルを作成する場所を設定し、SQLAlchemy用のエンジンを作ります。
  • Base = declarative_base() により、全てのテーブルクラスの土台を作成します。

AuthorとBookのテーブル構造をクラスで定義

ここで2つのテーブル(書籍と著者)をPythonのクラスとして定義します。

書籍テーブル(子テーブル)

# ====================================
# モデル(テーブルを作成するクラス)
# ====================================
## 書籍テーブル
class Book(Base):  # Baseクラスを継承したBookクラスの定義
    # テーブル名
    __tablename__ = 'books'
    # 書籍ID(主キー)
    id = Column(Integer, primary_key=True, autoincrement=True)
    # 書籍名(必須)
    name = Column(String, nullable=False)
    # 著者ID(外部キー:authors.idを参照)
    author_id = Column(Integer, ForeignKey('authors.id'))
        # booksテーブルにauthor_idという列ができ、ここにはauthorsテーブルのidフィールドにある値しか入らない
        # このForeignKeyにより、こちらがリレーションの「子」側となる
    # 著者(1対多リレーションの子側)
    author = relationship("Author", back_populates="books")
        # relationship()関数を使って、Authorクラスのbooks属性と関連付ける
    # 表示用関数
    def __str__(self):
        return f"書籍ID:{self.id}, 書籍名:{self.name}"
  • author_id で、著者テーブルの id を参照するようにしています(外部キー)。
  • relationship("Author", back_populates="books") により、著者(親)へアクセスできるようになります。

著者テーブル(親テーブル)

## 著者テーブル
class Author(Base):  # Baseクラスを継承したAuthorクラスの定義
    # テーブル名
    __tablename__ = 'authors'
    # 著者ID(主キー)
    id = Column(Integer, primary_key=True, autoincrement=True)
    # 著者名(ユニーク・必須)
    name = Column(String, nullable=False, unique=True)
    # 書籍一覧(1対多リレーションの親側)
    books = relationship("Book", back_populates="author")
        # relationship()関数を使って、Bookクラスのauthor属性と関連付ける
        # 外部キーが相手側にあるため、こちらがリレーションの「親」側になる
    # 表示用関数
    def __str__(self):
        return f"著者ID:{self.id}, 著者名:{self.name}"
  • books = relationship(...) の部分で、著者が持つ書籍の一覧を取得できるようにしています。
  • これにより、「著者 ⇄ 書籍」が双方向でつながります。

SQLAlchemyでテーブル作成とセッションを定義

次に実際にデータベースにテーブルを作成し、著者と書籍のデータを登録してみます。

# ====================================
# テーブル操作
# ====================================
print('テーブルを削除してから作成')
Base.metadata.drop_all(db_engine)
Base.metadata.create_all(db_engine)

## セッションの生成
session_maker = sessionmaker(bind=db_engine)
session = session_maker()
  • drop_all()create_all() で、テーブルの初期化と作成を行います。
  • session はデータベースへの操作を行うためのインターフェースです。

インスタンス生成

# 書籍のインスタンス作成
book01 = Book(name='ノルウェイの森') # Bookクラス(モデル)をインスタンス生成し、book01に代入
book02 = Book(name='1Q84')
book03 = Book(name='容疑者Xの献身')
book04 = Book(name='ナミヤ雑貨店の奇蹟')

# 著者のインスタンス作成
author01 = Author(name='村上春樹') # Authorクラス(モデル)をインスタンス生成し、author01に代入
author02 = Author(name='東野圭吾')

このように、まずはデータのインスタンス(オブジェクト)を作成します。

続いて、著者に対して書籍を紐づけるために、以下のようにします。

# 著者に書籍を紐づける
# 村上春樹:ノルウェイの森、1Q84
# 東野圭吾:容疑者Xの献身、ナミヤ雑貨店の奇蹟
author01.books.append(book01)  # author01のbooksリストにappendメソッドを使用
    # これによりauthor01のbooksリストにbook01を追加、両者のリレーションが構築される
author01.books.append(book02)  # 同上
author02.books.append(book03)  # 同上
author02.books.append(book04)  # 同上
  • これが 1対多リレーションの構築方法です。
  • 親(著者)の books 属性に子(書籍)を append() するだけで、自動的に author_id がセットされます。

リストのappend()メソッドを忘れた人は↓↓の記事で復習できます。

あわせて読みたい
Pythonデータ構造|リストの定義と要素の追加をステップごとに解説【レッスン4-1】
Pythonデータ構造|リストの定義と要素の追加をステップごとに解説【レッスン4-1】

最後に、データベースに保存します。

# セッションで「著者」を登録
session.add_all([author01, author02])  # 著者ごとセッションに追加(書籍も自動的に追加される)
session.commit()

登録したデータの確認

print('データの参照:実行')
print('■:Bookの参照')
target_book = session.query(Book).filter_by(id=1).first()
    # Bookテーブルの、id=1のレコードの、最初の1件を取得
print(target_book)
print('■:Bookに紐づいたAuthorの参照')
print(target_book.author)

print('■' * 20)

print('■:Authorの参照')
target_author = session.query(Author).filter_by(id=1).first()
    # Authorテーブルの、id=1のレコードの、最初の1件を取得
print('■:Authorに紐づいたBookの参照')
for book in target_author.books:
    print(book)
  • 書籍から著者へ
  • 著者から書籍一覧へ

このように双方向のアクセスが可能になります。
これが relationship() によって実現できる便利な仕組みです。

コードを実行して、結果を確認してみましょう。

【Python】勉強猫がコーヒーを片手にリラックスしている様子。記事内の休憩用イラスト

コラム:uselist=False による1対1のリレーション

今回紹介した「著者と書籍」のような関係は、1人の著者が複数の書籍を持つという 1対多 の関係でした。

一方で、世の中には「1対1」の関係もたくさん存在します。

たとえば、

  • 「ユーザー」と「プロフィール」
  • 「社員」と「社員証」
  • 「顧客」と「会員情報」

のように、片方のデータに対してもう片方のデータがただ1つだけ対応するケースです。

SQLAlchemyでは、通常 relationship() を使うと「リスト形式(多対1、多対多)」で子データが取得されるようになっています。

1対1の関係を構築したい場合は、relationship() の中に次のようなオプションを指定します:

1対1のための構文

# 親テーブル側
子データ = relationship('子クラス名', back_populates='親に対応する属性名', uselist=False)
  • uselist=False を付けることで、「リスト」ではなく「1つのオブジェクト」として扱われるようになります。
  • これにより子データ で取得されるのはリストではなく、1つのオブジェクトになります。

uselist=Falseの使用例

たとえば「ユーザー(User)」と「プロフィール(Profile)」が1対1で紐づく場合:

# 親テーブル側
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    profile = relationship('Profile', back_populates='user', uselist=False)

# 子テーブル側
class Profile(Base):
    __tablename__ = 'profiles'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    user = relationship('User', back_populates='profile')

このように設定すると、ユーザーからプロフィールは .profile で直接アクセスでき、リスト操作は不要です。

uselist=Falseの注意点

  • テーブル設計上も「必ず1対1の関係になる」よう制約を整えておくと、より安全です(例:unique=True を外部キー側に付けるなど)。
  • uselist=False を忘れると、1対1のつもりが1対多として動作してしまうため注意しましょう。

このように uselist=False を活用すれば、関係性が明確な1対1データ構造もPythonコード上で直感的に扱えるようになります。

まとめ:テーブル間の関係を自在に操れるようになろう

今回の記事では、リレーショナルデータベースの根幹とも言える「テーブル間の関連付け」について学びました。

特に重要だったのは、以下の2つの機能です。

  • ForeignKey:あるテーブルのカラムが、別のテーブルの主キーを参照するための仕組み
  • relationship():テーブル間の関係性をPythonコード上で「属性」として表現するための仕組み

FlaskやSQLAlchemyを使ったアプリ開発では、この「リレーションの設計と実装」が欠かせません。

現実世界のデータを正しくモデル化するためには、「どの情報がどの情報に属するのか」「どんな関係があるのか」を明確にし、それをコードに正確に落とし込む必要があります。

ぜひ今回の内容をもとに、自分でも複数のテーブルを持つモデルの設計と実装にチャレンジしてみてください。

このステップを乗り越えることで、アプリケーションの設計力とデータベースの理解が一気に深まります!

練習問題|relationshipとuselist=Falseで1対1のリレーションを実装しよう

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

この問題では、「書籍(Book)」と「価格(Price)」の関係を1対1で表現するテーブル構造を作ってみましょう。

通常、リレーショナルデータベースでは「1対多」の関係が多く見られますが、今回はあえて「1冊の書籍に対して1つの価格」という 1対1の関係を構築します。

このようなリレーションを作ることで、ある書籍に対応する価格をシンプルに管理することができ、アプリケーションの設計がより明確になります。

SQLAlchemyでこの関係を表現するには、ForeignKeyrelationship() を正しく組み合わせ、uselist=False を設定することが重要です。

この問題の要件

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

  1. 「Price」という名前のテーブルを定義すること
  2. Priceテーブルには、以下のカラムを定義すること:
    • 主キーとなる id(自動採番される整数)
    • 書籍の価格を表す amount(整数で、空欄不可)
    • 書籍を特定する book_idbooks.idを参照する外部キー、重複不可)
  3. Priceテーブルにおいて、Bookクラスとリレーションを持つようにすること(relationship()を使用)
  4. Bookクラスにおいて、Priceとの1対1リレーションを追加すること(relationship() を使い、uselist=False を指定)
  5. 以下の4冊の書籍と対応する価格を定義し、それぞれ1対1で紐づけること:
    • ノルウェイの森:1200円
    • 1Q84:1800円
    • 容疑者Xの献身:850円
    • ナミヤ雑貨店の奇蹟:950円
  6. 書籍と価格の紐づけは .price = を使って行うこと
  7. 書籍は session.add_all([author01, author02]) によって登録され、同時に価格も保存されるようにすること

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

■:Bookの参照
書籍ID:1, 書籍名:ノルウェイの森
■:Bookに紐づいたAuthorの参照
著者ID:1, 著者名:村上春樹
■:Bookに紐づいたPriceの参照
価格ID:1, 金額:1200円

正解コード

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

Q
正解コード
# ====================================
# 新しいテーブル:価格テーブル(Price)
# 書籍1冊に価格1つを紐づける「1対1の関係」
# ====================================

class Price(Base):  # Priceテーブルの定義(Baseクラスを継承)
    __tablename__ = 'prices'  # テーブル名

    id = Column(Integer, primary_key=True, autoincrement=True)  # 主キー
    amount = Column(Integer, nullable=False)  # 価格(必須)

    book_id = Column(Integer, ForeignKey('books.id'), unique=True)  
    # 外部キーとして書籍IDを参照(1対1のため unique=True を指定)

    book = relationship("Book", back_populates="price")  
    # Bookクラスと双方向にリレーション(親への参照)

    def __str__(self):
        return f"価格ID:{self.id}, 金額:{self.amount}円"
# BookテーブルにPriceとの1対1リレーションを追加(uselist=Falseがポイント)
Book.price = relationship("Price", back_populates="book", uselist=False)
# 書籍1冊につき価格1つ。リストではなく単一のオブジェクトとして扱う
# ====================================
# 価格のインスタンスを作成(それぞれの書籍に対応)
# ====================================

price01 = Price(amount=1200)  # ノルウェイの森の価格
price02 = Price(amount=1800)  # 1Q84の価格
price03 = Price(amount=850)   # 容疑者Xの献身の価格
price04 = Price(amount=950)   # ナミヤ雑貨店の奇蹟の価格

# ====================================
# 書籍と価格を1対1で紐づける(relationship による関連付け)
# ====================================

book01.price = price01  # 書籍と価格を対応させる
book02.price = price02
book03.price = price03
book04.price = price04

# すでに前の処理で書籍は著者に紐づけられているので、
# ここでは書籍と価格のリレーションだけを追加すればOK
# 書籍(book01〜book04)を session に追加済みであれば、価格も一緒に登録される

# すでに以下のように登録していれば:
# session.add_all([author01, author02])
# session.commit()
# その中で book01〜book04 が著者経由で登録されると同時に、
# 紐づいた price01〜price04 も一緒に保存される仕組みになっている
もっと分かりやすい学習サイトにするために

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

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

<<前のページ

Flaskの記事一覧

次のページ>>

FAQ|

Q
Q1.

Q
Q2.
Q
Q3.
記事URLをコピーしました