YASD-TECH
YASD TECH
# Prisma

includeで関連レコードを一緒に取る(selectとの違い・型の導出)

投稿日:2026/7/30

更新日:2026/7/30

ttitleImage

include で関連レコードを一緒に取る(select との違い・型の導出)

Prisma の include は「関連レコードを一緒に取ってくる」指定。ActiveRecord の includes / preload に相当する。
ただの N+1 対策にとどまらず、関連側に where を書けることと戻り値の型が include の内容に応じて変わることが Prisma 独自のポイントになる。

結論

  • 何も書かなければ自分のカラムだけ。リレーションは include で明示しないと付いてこない(型にも存在しない)。
  • include の中に where を書けるので、「関連のうち条件に合うものだけ」を取得時点で絞れる。ActiveRecord の preload にはできない芸当。
  • includeselect は同じ階層に併記できない。include = 全カラム + リレーション、select = 指定したものだけ。
  • 戻り値の型は include の内容から自動で決まる。Prisma.UserGetPayload<{ include: ... }> で引数の型を手書きせずに導出できる。

デフォルトは自分のカラムだけ

ts
const user = await prisma.user.findFirst({ where: { userId } });
// → { userId, familyName, givenName, email, ... }
//   customer も orders も入っていない

user.customer はそもそも型に存在しないので、参照するとコンパイルエラーになる。

ts
const user = await prisma.user.findFirst({
  where: { userId },
  include: { customer: true, orders: true },
});
// → { userId, ..., customer: {...}, orders: [...] }

ActiveRecord との対応

ruby
User.includes(:customer, :orders).find_by(user_id: id)
ts
prisma.user.findFirst({
  where: { userId: id },
  include: { customer: true, orders: true },
});

挙動も近い。Prisma は既定では JOIN せず、別クエリを投げてアプリ側で結合する。ActiveRecord の preload と同じ方式(relationJoins を有効にすると JOIN になる)。

sequenceDiagram
    participant App as アプリ
    participant DB as データベース
    App->>DB: SELECT * FROM users WHERE user_id = ...
    DB-->>App: users 1件
    App->>DB: SELECT * FROM customers WHERE user_id IN (...)
    DB-->>App: customers
    App->>DB: SELECT * FROM orders WHERE user_id IN (...)
    DB-->>App: orders
    Note over App: アプリ側で user.customer / user.orders に組み立て

目的も同じで、N+1 を避けるため。

ts
// include なし → N+1
const users = await prisma.user.findMany();
for (const user of users) {
  // ↓ ユーザーの人数ぶんクエリが飛ぶ
  const customer = await prisma.customer.findFirst({ where: { userId: user.userId } });
}

ネストできる

ts
include: {
  customer: {
    include: {
      customerTags: true,
      favorites: true,
    },
  },
}

UserCustomerCustomerTag / Favorite と3階層たどる。ActiveRecord の includes(customer: [:customer_tags, :favorites]) と同じ。

flowchart LR
    User["User<br/>(ユーザー)"] --> Customer["Customer<br/>(購入者プロフィール)"]
    Customer --> CustomerTag["CustomerTag<br/>(顧客タグ)"]
    Customer --> Favorite["Favorite<br/>(お気に入り)"]
    Favorite --> Product["Product<br/>(商品)"]

where を中に書ける(ActiveRecord に無い)

ここが便利なところで、関連側を絞り込める。

ts
favorites: {
  where: {
    shopId,
    product: { isPublished: true },   // ← さらに関連先の条件
  },
},

「この購入者のお気に入りのうち、自ショップかつ公開中の商品だけ」を取る。

ActiveRecord で同じことをするなら preload ではできず、joins + 条件か、スコープ付きの association が要る。

取得の時点で絞り込めることに意味がある場面がある。
例えば「CSV に無いお気に入り=解除された」と判定する差分同期を書くとき、非公開商品を取得時点で除外しておかないと、非公開商品まで「解除された」と誤判定してしまう。where を後段のフィルタではなくクエリに寄せると、この手の事故が減る。


select との違い

同じ階層ではどちらか一方しか書けない。

意味
include 自分の全カラム + 指定したリレーション
select 指定したものだけ(自分のカラムもリレーションも)
ts
// 必要な列だけ欲しいとき
favorites: {
  where: { shopId },
  select: { restockNotifyEnabled: true, priceDropNotifyEnabled: true },
}

件数を数えたいだけ、フラグを見たいだけ、というときは全カラムは要らないので select を使う。


戻り値の型が変わる

Prisma の特徴。include の内容に応じて型が自動で決まる。

ts
const user = await prisma.user.findFirst({ include: { customer: true } });
user.customer   // OK
user.orders     // コンパイルエラー(include していない)

これを利用すると、関数の引数型を手書きせずに済む。

ts
const activeCustomerInclude = (shopId: string) =>
  ({
    customer: {
      include: {
        customerTags: true,
        favorites: {
          where: { shopId, product: { isPublished: true } },
        },
      },
    },
  }) satisfies Prisma.UserInclude;

type ActiveCustomerSource = Prisma.UserGetPayload<{
  include: ReturnType<typeof activeCustomerInclude>;
}>;

const toActiveCustomer = (source: ActiveCustomerSource) => { /* ... */ };
  • include の定義を変数に切り出し、Prisma.UserGetPayload でその include から型を導出している。
  • include に項目を足せば引数の型も自動で追随する。
  • satisfies Prisma.UserInclude は「include として妥当か」をその場で検算するため。as と違って型を潰さないので、ReturnType で取り出したときに具体的な形が残る(→ Zodとsatisfiesの併用)。
flowchart TD
    Def["activeCustomerInclude<br/>(include の定義)"] -->|satisfies で妥当性を検算| Check["Prisma.UserInclude"]
    Def -->|ReturnType| RT["include の具体的な型"]
    RT -->|UserGetPayload| Payload["ActiveCustomerSource<br/>(戻り値の型)"]
    Payload --> Fn["toActiveCustomer の引数型"]

ハマりどころ

  • includeselect を同じ階層に並べると実行時エラーになる。片方に寄せる。
  • include していないリレーションを参照するとコンパイルエラー。型が守ってくれる代わりに、後から使いたくなったら include 側も直す必要がある。
  • ネストした where はあくまで関連側の絞り込みであって、親の絞り込みではない。「公開中の商品をお気に入りに入れているユーザーだけ」を取りたいなら、親の wheresome を書く必要がある。
ts
// 親を絞りたいときは where 側に some
prisma.user.findMany({
  where: { customer: { favorites: { some: { product: { isPublished: true } } } } },
});
  • 既定は JOIN ではなく別クエリなので、取得件数が多いと IN 句が肥大する。件数が読めないときは take や別クエリへの分割を検討する。

参考

Index

  • include で関連レコードを一緒に取る(select との違い・型の導出)
  • 結論
  • デフォルトは自分のカラムだけ
  • ActiveRecord との対応
  • ネストできる
  • where を中に書ける(ActiveRecord に無い)
  • select との違い
  • 戻り値の型が変わる
  • ハマりどころ
  • 参考