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円)ですが、現在は期間限定で無料公開中です。
ForeignKeyの意味と外部キーの使い方を初心者向けに解説
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コード内での扱いを便利にするためのものです。- 実際にデータを登録・取得する際に大きな力を発揮します。
こちらも次のセクションで、使用例コードの中で詳しく解説します。
実例|著者と書籍で1対多リレーションを構築する手順
ここでは前章で学んだ ForeignKey
と relationship()
を使って、「著者と書籍」の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()メソッドを忘れた人は↓↓の記事で復習できます。
最後に、データベースに保存します。
# セッションで「著者」を登録 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()
によって実現できる便利な仕組みです。
コードを実行して、結果を確認してみましょう。
コラム: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のリレーションを実装しよう
この問題では、「書籍(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.