relationship って何?まずはイメージから
SQLAlchemy の relationship は、
「テーブル同士のつながり(リレーション)を、Python のオブジェクト同士のつながりとして表現するための仕組み」です。
SQL の世界では、
users テーブルと posts テーブルを外部キー(user_id)で結びつけて、JOIN して使います。
ORM の世界では、
User クラスと Post クラスを relationship で結びつけて、user.posts や post.user のようにアクセスします。
つまり、relationship は、
テーブル間の関係(1 対多、多対 1、多対多)を
「オブジェクト同士の関係」として扱うための橋
だと思ってください。
まずは「1 対多」の基本例から押さえる
users と posts のテーブルをイメージする
よくある例として、「ユーザーとブログ記事」を考えます。
ユーザーは複数の記事を書ける(1 ユーザー : 多数の記事)
記事は 1 人のユーザーに属する(多対 1)
SQL のテーブルで書くと、だいたいこうです。
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users(id)
);
SQLposts.user_id が「この投稿はどのユーザーのものか」を表す外部キーです。
JOIN するときは users.id = posts.user_id で結びつけます。
同じ構造を ORM モデルで書く
これを SQLAlchemy の ORM モデルで書くと、こうなります。
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
posts = relationship("Post", back_populates="author")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String, nullable=False)
body = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", back_populates="posts")
Pythonここで重要なのは 2 つです。
User 側の posts = relationship("Post", back_populates="author")
Post 側の author = relationship("User", back_populates="posts")
これで、
User インスタンスからは user.posts でその人の投稿一覧にアクセスできる
Post インスタンスからは post.author でその投稿のユーザーにアクセスできる
という「オブジェクト同士のつながり」が生まれます。
relationship が実際にどう動くかを体感する
データを作ってみる
先ほどのモデルを使って、実際にデータを入れてみます。
engine = create_engine("sqlite:///example.db", echo=True)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
user = User(name="Taro")
post1 = Post(title="Hello", body="First post", author=user)
post2 = Post(title="Second", body="Another post", author=user)
session.add(user)
session.add_all([post1, post2])
session.commit()
session.close()
Pythonここで注目してほしいのは、Post を作るときに author=user と書いているところです。
author=user と書く
→ Post.author に User オブジェクトをセットする
→ 裏で post.user_id に user.id が入る(外部キーが設定される)
つまり、relationship を通して「オブジェクトをつなぐ」と、
SQL 的には「外部キーが正しく設定される」ことになります。
つながりをたどってデータを読む
今度は、保存したデータを読み出してみます。
session = SessionLocal()
u = session.query(User).filter_by(name="Taro").first()
print("User:", u.name)
for p in u.posts:
print("Post:", p.title, "-", p.body)
session.close()
Pythonここで u.posts が効いているのは、User クラスに書いた posts = relationship("Post", ...) のおかげです。
裏では JOIN を含むクエリが発行されますが、コード上は「オブジェクトの属性」として扱えます。
逆方向も同じです。
session = SessionLocal()
p = session.query(Post).filter_by(title="Hello").first()
print("Post:", p.title)
print("Author:", p.author.name)
session.close()
Pythonp.author が効いているのは、Post クラスに書いた author = relationship("User", ...) のおかげです。
ここでのポイントは、
JOIN を自分で書かなくても、relationship を定義しておけば、
「オブジェクト同士のつながり」として自然に扱える
ということです。
relationship の「向き」と「名前」をしっかり理解する
1 対多のとき、どっち側に何を書くか
1 対多の関係では、
「多」の側に外部キー(ForeignKey)を置きます。
今回の例だと、
User(1)
Post(多)
なので、Post に user_id = Column(Integer, ForeignKey("users.id")) を書きました。
そして relationship は、両方のクラスに書きます。
User 側:posts = relationship("Post", back_populates="author")
Post 側:author = relationship("User", back_populates="posts")
ここでの考え方はこうです。
User から見たら、「自分に紐づく Post がたくさんある」
→ posts という複数形の属性名にする
Post から見たら、「自分には 1 人の User がいる」
→ author や user など単数形の属性名にする
この「名前の付け方」が、コードを読んだときの分かりやすさに直結します。
back_populates の役割
back_populates は、「この関係はあっち側のこの属性とペアですよ」と教えるためのものです。
User 側:posts = relationship("Post", back_populates="author")
Post 側:author = relationship("User", back_populates="posts")
これで、SQLAlchemy は「User.posts と Post.author は互いに対応している」と理解します。
その結果、
post.author = user と書くと、自動的に user.posts にもその post が含まれるようになるuser.posts.append(post) とすると、自動的に post.author もその user になる
という「双方向の同期」が効くようになります。
relationship を使うときにハマりやすいポイント
ForeignKey を忘れると動かない
relationship だけ書いて、ForeignKey を書き忘れると、
「見た目はそれっぽいけど、実際には外部キーが張られていない」状態になります。
必ず、「多」の側のカラムに ForeignKey を書きます。
user_id = Column(Integer, ForeignKey("users.id"))
Pythonこの "users.id" は、「参照先テーブル名.カラム名」です。
ここを間違えると、テーブル作成時にエラーになったり、意図しない構造になったりします。
relationship の属性名は「意味が伝わる名前」にする
例えば、User と Post の関係で、
User 側に children
Post 側に parent
のような名前を付けることも技術的にはできますが、
コードを読む人には意味が伝わりにくくなります。
User 側:posts
Post 側:author または user
のように、「何と何の関係なのか」が一目で分かる名前にするのが大事です。
多対多の relationship をざっくりイメージしておく
例:ユーザーとグループ(多対多)
多対多の関係も、relationship で表現できます。
例えば、「ユーザーは複数のグループに所属できる」「グループには複数のユーザーが所属できる」という関係です。
SQL 的には、中間テーブルを作ります。
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE groups (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE user_group (
user_id INTEGER NOT NULL REFERENCES users(id),
group_id INTEGER NOT NULL REFERENCES groups(id),
PRIMARY KEY (user_id, group_id)
);
SQLORM モデルでは、association_table を使います。
from sqlalchemy import Table, Column, Integer, String, ForeignKey
association_table = Table(
"user_group",
Base.metadata,
Column("user_id", ForeignKey("users.id"), primary_key=True),
Column("group_id", ForeignKey("groups.id"), primary_key=True),
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
groups = relationship("Group", secondary=association_table, back_populates="users")
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
users = relationship("User", secondary=association_table, back_populates="groups")
Pythonここで新しく出てきたのが secondary です。
これは「多対多をつなぐ中間テーブル」を指定する引数です。
使うときは、こうなります。
session = SessionLocal()
u = User(name="Taro")
g1 = Group(name="Admin")
g2 = Group(name="Member")
u.groups.append(g1)
u.groups.append(g2)
session.add(u)
session.commit()
session.close()
Pythonu.groups.append(g1) と書くと、
裏では user_group テーブルに (user_id, group_id) の行が追加されます。
多対多は少し難しめですが、
「中間テーブルを secondary で指定する」
「両側に relationship を書く」
というパターンを覚えておけば、あとからじっくり慣れていけます。
まとめ(relationship は「JOIN をオブジェクトの世界に持ち込む道具」)
SQLAlchemy の relationship を初心者目線でまとめると、こうなります。
SQL の世界で外部キーと JOIN で表現していた「テーブル同士の関係」を、Python のオブジェクト同士の関係として扱えるようにするのが relationship。
1 対多では、「多」の側に ForeignKey を置き、両方のクラスに relationship を書いて、user.posts や post.author のように自然な形でアクセスできる。back_populates で「この関係はあっち側のこの属性とペア」と教えることで、双方向の同期(author を変えたら posts 側にも反映、など)が効く。
多対多では、中間テーブルを secondary で指定し、user.groups や group.users のように扱えるようにする。
JOIN を毎回手で書く代わりに、relationship で「関係」をモデルに埋め込んでおくことで、
アプリ側のコードは「オブジェクト同士のつながり」を意識するだけで済むようになります。
