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

一つ前のページでは内部結合と外部結合について学習しました。
今回は 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()
の意味と使い方- 実例:「著者」と「書籍」のリレーションを実装
本記事は 有料記事(100円)ですが、現在は期間限定で無料公開中です。
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 も一緒に保存される仕組みになっている
FAQ|
- Q1.
-
あ
- Q2.
- Q3.