【Flask】relationshipの使い方とリレーション構築の基本|Chapter5-6

一つ前のページでは内部結合と外部結合について学習しました。
今回は 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:エラーハンドリングとデバッグ編
Chapter7:アプリ開発編
Flaskを使ってWebアプリケーションを開発する中で、データベースはとても重要な役割を担っています。
そして現実の情報をデータとして扱おうとすると、たとえば「著者と書籍」「ユーザーと投稿」「商品と注文」のように、複数のテーブルを関連付けて使いたい場面が出てきます。
今回の学習テーマは、そんな「テーブル同士をつなげる技術」です。
Flaskでは SQLAlchemy というライブラリを使ってPythonのクラスとテーブルを対応させて使います。
さらに、ForeignKey(外部キー)や relationship() を活用することで、親子関係のような構造を作ることができます。
本記事で学ぶ内容は以下の通りです:
ForeignKeyの意味と使い方relationship()の意味と使い方- 実例:「著者」と「書籍」のリレーションを実装
- Flask開発を Stream Deck でボタン化しよう!
-
Flaskは非常に軽量かつシンプルなフレームワークですが、それゆえに定型作業が多く、開発は単調な作業の連続になりがちです。
それこそがFlaskのメリットであり、習得難易度が低い理由でもありますが、単調な作業は退屈で、ミスも起こりやすいでしょう。
そこで役に立つのが Stream Deck 。
このような定型手順が多い作業を “ボタン化” することで視覚化。
圧倒的に 効率的 かつ ストレスフリー な開発環境が簡単に手に入ります↓↓
あわせて読みたいFlask開発をStreamDeckでボタン化しようあわせて読みたいプログラマー向けStream Deckの選び方|初心者でも失敗しないモデル比較ガイド
ForeignKeyとrelationship()関数で2つのテーブルを繋げよう

ForeignKeyの意味と外部キーの使い方
ForeignKey(外部キー)とは、2つのテーブルのカラムを紐付け、整合性をとるための仕組みです。
たとえば以下の2つのテーブルがあった場合、両方の「著者id」を関連付けることで「この本はこの著者が書いた」という情報を持つ “準備” ができます。
books
authors
| 書籍id | 書籍 | 著者id |
|---|---|---|
| 1 | ノルウェイの森 | 1 |
| 2 | 1Q84 | 1 |
| 3 | 容疑者Xの献身 | 2 |
| 4 | ナミヤ雑貨店の奇蹟 | 2 |
| 著者id | 著者 |
|---|---|
| 1 | 村上春樹 |
| 2 | 東野圭吾 |
この場合、booksテーブルの著者id列に入る数字は、必ずauthorsテーブルの著者id列で事前に定義された数字(今回は1か2のみ)である必要がありますね。
booksテーブルの著者id列を定義する際に、オプションとしてForeignKey(外部キー)を設定することで、その列にその制限を付けることができます。
基本構文は以下の通りです。
# 基本構文
変数 = Column(型, ForeignKey('参照先テーブル.カラム'))
# Columnクラスを使ってテーブルの列を定義。オプションとしてForeignKeyを設定。
# 使用例
変数 = Column(Intege, ForeignKey('authors.id')) # authorsテーブルのidカラムを外部キーとした列を生成このように書くことで、変数が参照先テーブルのカラム(列)を参照する「外部キー」となり、存在する著者IDしか登録できないようになります。
またbooksテーブルの著者idとauthorsテーブルの著者idが1対1で紐づき、2つのテーブル間で整合性が確保されました。
なお、データを定義する側(今回の場合はauthors)が親テーブルであり、外部キーで制限を付ける側(今回の場合はbooks)が子テーブルとなります。
後ほど使用例コードの中でもう少し詳しく解説します。
relationship()関数の仕組みと1対多リレーションの構文
relationship()関数 は、異なるテーブル(=クラス)間の関連性を、Pythonのクラスの中で“属性”として扱えるようにする関数です。
たとえば「本(Book)」と「著者(Author)」の関係を考えると、本からはその著者が、著者からはその人が書いた本一覧が分かるようにしたいですよね。
そのようなときに使うのが relationship()関数 です。
リレーションには「1対1」「1対多」「多対多」などの種類がありますが、ここでは「1人の著者が複数の本を書く」=「1対多」の関係を例にして構文を紹介します。
# 子テーブルのクラス内で使用する構文
親データ = relationship('親クラス名', back_populates='子に対応する属性名')
author = relationship("Author", back_populates="books") # 使用例:Authorクラスのbooks列と紐づける
# 親テーブルのクラス内で使用する構文
子データ = relationship('子クラス名', back_populates='親に対応する属性名')- 子テーブル側では、
relationship()によって「親の情報」を取得できるようにします。 - 親テーブル側では、
relationship()によって「紐づいている子データの一覧」を取得できるようにします。 back_populatesには「相手側のクラスで定義している属性名」を指定し、双方向の関連付けを可能にします。
こちらも次のセクションで、使用例コードの中で詳しく解説します。

実例|1対多リレーションを構築するコード
ForeignKey と relationship() を使って、「著者と書籍」の1対多の関係を表現するプログラムを一緒に見ていきましょう。
コードの上から順に紹介しますので、是非コピーしてVSCodeに貼り付けていきながら読んでください。
必要なモジュールとDBの初期設定
まずはSQLAlchemyを使用する準備部分です。
以下のコード内で分からない部分がある場合は SQLAlchemyの解説記事 へ戻って復習しましょう。
from pathlib import Path # pathlibライブラリのPathクラスをインポート
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
# ====================================
# データベースのパス(保存場所)を設定
# ====================================
base_dir = Path(__file__).parent # このスクリプトファイルがあるフォルダのパスを取得して、変数base_dirに代入
database = base_dir / 'data.sqlite' # base_dir内のdata.sqliteというファイルへのパスを作成し、変数databaseに代入
## データベースエンジンを作成
db_engine = create_engine(f'sqlite:///{database}', echo=True) # create_engine関数を使って、DB接続用のエンジンを作成
Base = declarative_base() # SQLAlchemyに対応した新しい“親クラス”を生成し、変数Baseに代入なお、これらのSQLAlchemy関連のインポートは覚える必要はありません。
Chapter5-7で学習するFlask-SQLAlchemyでまとめてインポートできるようになります。
AuthorとBookのテーブル構造をクラスで定義
ここで2つのテーブル(書籍と著者)をPythonのクラスとして定義します。
書籍テーブル(子テーブル)
# ====================================
# モデル(テーブルを作成するクラス)
# ====================================
## 書籍テーブル
class Book(Base): # Baseクラスを継承したBookクラスの定義
# テーブル名
__tablename__ = 'books'
# Columnクラスを使ってテーブルの列を3つ定義
id = Column(Integer, primary_key=True, autoincrement=True) # 書籍ID(主キー, 連番)
name = Column(String, nullable=False) # 書籍名(入力必須)
author_id = Column(Integer, ForeignKey('authors.id')) # 著者ID(外部キー:authors.idを参照)
# author_id列にはauthorsテーブルのidカラムにある値しか入らない
# このForeignKeyにより、こちらがリレーションの「子」側となる
# リレーション(1対多リレーションの子側)
author = relationship("Author", back_populates="books") # リレーション
# relationship()関数を使って、Authorクラスのbooks属性とauthorを関連付ける
# 表示用関数
def __str__(self):
return f"書籍ID:{self.id}, 書籍名:{self.name}"11行目のColumnオブジェクト生成部に、オプションとしてForeignKey(外部キー)を書いています。
これにより、この列には著者テーブルのid列にある値しか入力できず、2つの表のデータの紐付けが明確になりました。
そしてこの紐付けを利用して、15行目のrelationship関数で親テーブルのbooks属性との関連付けを行っています。
著者テーブル(親テーブル)
## 著者テーブル
class Author(Base): # Baseクラスを継承したAuthorクラスの定義
# テーブル名
__tablename__ = 'authors'
# Columnクラスを使ってテーブルの列を2つ定義
id = Column(Integer, primary_key=True, autoincrement=True) # 著者ID(主キー)
name = Column(String, nullable=False, unique=True) # 著者名(ユニーク・必須)
# リレーション(1対多リレーションの親側)
books = relationship("Book", back_populates="author") # リレーション
# relationship()関数を使って、Bookクラスのauthor属性とbooks属性を関連付ける
# 外部キーが相手側にあるため、こちらがリレーションの「親」側になる
# 表示用関数
def __str__(self):
return f"著者ID:{self.id}, 著者名:{self.name}"子テーブル(Books)から外部キーで紐付けされているので、親テーブル(Author)には外部キーは不要です。
8行目のrelationship関数で子テーブルのauthor属性との関連付けを行っています。
これにより二つのテーブル「著者 ⇄ 書籍」が双方向で繋がりました。
SQLAlchemyでテーブル作成とセッションを定義
次に実際にデータベースにテーブルを作成し、著者と書籍のデータを登録してみます。
# ====================================
# テーブル操作
# ====================================
print('テーブルを削除してから作成')
Base.metadata.drop_all(db_engine) # データベースの削除(初期化)
Base.metadata.create_all(db_engine) # データベースの作成or接続 & テーブルの作成
## セッションの生成
session_maker = sessionmaker(bind=db_engine) # sessionmakerクラスからセッションファクトリを作成し、変数session_makerに代入
session = session_maker() # session_makerをインスタンス生成し、変数sessionに代入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='東野圭吾')
# 著者に書籍を紐づける
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()メソッドを忘れた人は↓↓の記事で復習できます。

最後に、データベースに保存します。
# セッションで「著者」を登録 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() によって実現できる便利な仕組みです。
コードを実行して、結果を確認してみましょう。
まとめ:テーブル間の関係を自在に操れるようになろう
今回の記事では、リレーショナルデータベースの根幹とも言える「テーブル間の関連付け」について学びました。
特に重要だったのは、以下の2つの機能です。
ForeignKey:あるテーブルのカラムが、別のテーブルの主キーを参照するための仕組みrelationship():テーブル間の関係性をPythonコード上で「属性」として表現するための仕組み
FlaskやSQLAlchemyを使ったアプリ開発では、この「リレーションの設計と実装」が欠かせません。
現実世界のデータを正しくモデル化するためには、「どの情報がどの情報に属するのか」「どんな関係があるのか」を明確にし、それをコードに正確に落とし込む必要があります。
ぜひ今回の内容をもとに、自分でも複数のテーブルを持つモデルの設計と実装にチャレンジしてみてください。
このステップを乗り越えることで、アプリケーションの設計力とデータベースの理解が一気に深まります!
- サイト改善アンケート|1分だけ、ご意見をお聞かせください
-
本サイトでは、みなさまの学習をよりサポートできるサービスを目指しております。
そのため、ご利用者のみなさまの「プログラミングを学習する理由」などをアンケート形式でお伺いしています。ご協力いただけますと幸いです。
アンケート
練習問題|relationshipとuselist=Falseで1対1のリレーションを実装しよう

この問題では、「書籍(Book)」と「価格(Price)」の関係を1対1で表現するテーブル構造を作ってみましょう。
通常、リレーショナルデータベースでは「1対多」の関係が多く見られますが、今回はあえて「1冊の書籍に対して1つの価格」という 1対1の関係を構築します。
このようなリレーションを作ることで、ある書籍に対応する価格をシンプルに管理することができ、アプリケーションの設計がより明確になります。
SQLAlchemyでこの関係を表現するには、ForeignKey と relationship() を正しく組み合わせ、uselist=False を設定することが重要です。
この問題の要件
以下の要件に従ってコードを完成させてください。
- 「Price」という名前のテーブルを定義すること
- Priceテーブルには、以下のカラムを定義すること:
- 主キーとなる
id(自動採番される整数) - 書籍の価格を表す
amount(整数で、空欄不可) - 書籍を特定する
book_id(books.idを参照する外部キー、重複不可)
- 主キーとなる
- Priceテーブルにおいて、Bookクラスとリレーションを持つようにすること(
relationship()を使用) - Bookクラスにおいて、Priceとの1対1リレーションを追加すること(
relationship()を使い、uselist=Falseを指定) - 以下の4冊の書籍と対応する価格を定義し、それぞれ1対1で紐づけること:
- ノルウェイの森:1200円
- 1Q84:1800円
- 容疑者Xの献身:850円
- ナミヤ雑貨店の奇蹟:950円
- 書籍と価格の紐づけは
.price =を使って行うこと - 書籍は
session.add_all([author01, author02])によって登録され、同時に価格も保存されるようにすること
ただし、以下のような実行結果となるコードを書くこと。
■:Bookの参照 書籍ID:1, 書籍名:ノルウェイの森 ■:Bookに紐づいたAuthorの参照 著者ID:1, 著者名:村上春樹 ■:Bookに紐づいたPriceの参照 価格ID:1, 金額:1200円
正解コード
例えば、以下のようなプログラムが考えられます。
- 正解コード
-
# ==================================== # 新しいテーブル:価格テーブル(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 も一緒に保存される仕組みになっている






