Python | DB・SQL:relationship

Python
スポンサーリンク

relationship って何?まずはイメージから

SQLAlchemy の relationship は、
「テーブル同士のつながり(リレーション)を、Python のオブジェクト同士のつながりとして表現するための仕組み」です。

SQL の世界では、
users テーブルと posts テーブルを外部キー(user_id)で結びつけて、JOIN して使います。
ORM の世界では、
User クラスと Post クラスを relationship で結びつけて、user.postspost.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)
);
SQL

posts.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_iduser.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()
Python

p.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 がいる」
authoruser など単数形の属性名にする

この「名前の付け方」が、コードを読んだときの分かりやすさに直結します。

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)
);
SQL

ORM モデルでは、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()
Python

u.groups.append(g1) と書くと、
裏では user_group テーブルに (user_id, group_id) の行が追加されます。

多対多は少し難しめですが、
「中間テーブルを secondary で指定する」
「両側に relationship を書く」
というパターンを覚えておけば、あとからじっくり慣れていけます。


まとめ(relationship は「JOIN をオブジェクトの世界に持ち込む道具」)

SQLAlchemy の relationship を初心者目線でまとめると、こうなります。

SQL の世界で外部キーと JOIN で表現していた「テーブル同士の関係」を、Python のオブジェクト同士の関係として扱えるようにするのが relationship
1 対多では、「多」の側に ForeignKey を置き、両方のクラスに relationship を書いて、user.postspost.author のように自然な形でアクセスできる。
back_populates で「この関係はあっち側のこの属性とペア」と教えることで、双方向の同期(author を変えたら posts 側にも反映、など)が効く。
多対多では、中間テーブルを secondary で指定し、user.groupsgroup.users のように扱えるようにする。

JOIN を毎回手で書く代わりに、
relationship で「関係」をモデルに埋め込んでおくことで、
アプリ側のコードは「オブジェクト同士のつながり」を意識するだけで済むようになります。

タイトルとURLをコピーしました