ShopifyでPrisma Postgresを使用する方法
はじめに
Shopifyは、eコマースストア構築のための人気プラットフォームです。このガイドでは、ShopifyアプリをPrisma Postgresデータベースに接続し、商品の内部メモを作成する方法を説明します。
前提条件
1. プロジェクトをセットアップする
Shopify CLIがインストールされていない場合は、npm install -g @shopify/cli
でインストールできます。
まず、Shopify CLIを使用して新しいShopifyアプリを初期化します。
shopify app init
セットアップ中に、アプリのカスタマイズを求めるプロンプトが表示されます。心配いりません。これらの推奨オプションに従うだけで、すぐに始められ、アプリが成功するようにセットアップされます。
- アプリの構築を開始する:
Build a Remix app (recommended)
- Remixテンプレートの言語は何にしますか:
JavaScript
- アプリ名:
prisma-store
(名前にshopify
は含められません)
prisma-store
ディレクトリに移動します。
cd prisma-store
2. Prismaをセットアップする
Prismaはプロジェクトにプリインストールされていますが、最新バージョンに更新しておきましょう。これにより、最新の機能、改善点、そしてアプリを構築する上での最高の体験にアクセスできます。
Prisma Postgresデータベースに切り替えるため、prisma
ディレクトリ内のmigrations
フォルダーとdev.sqlite
ファイルを削除してください。
RemixとPrisma Postgresで動作させるには、schema.prisma
ファイルのいくつか更新する必要があります。
- 新しい
prisma-client
ジェネレーターに切り替えます。 - プロバイダーを
postgresql
に更新します。 - URLを新しいデータベースURLに更新します。
generator client {
provider = "prisma-client-js"
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "sqlite"
provider = "postgresql"
url = "file:../dev.db"
url = env("DATABASE_URL")
}
model Session {
// ... existing model
}
アプリが各製品のメモを保存できるように、新しいProductNote
モデルをPrismaスキーマに追加しましょう。
このモデルを使用すると、productGid
フィールドを介して、個々の製品にリンクされたメモをデータベースに保存・整理できます。
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Session {
// ... existing model
}
model ProductNote {
id String @id @default(uuid())
productGid String
body String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
次に、Prismaを最新バージョンに更新する必要があります。以下を実行してください。
npm install prisma --save-dev && npm install @prisma/client
Prisma Postgresでは、その場で新しいデータベースを作成できます。プロジェクトを初期化する際に--db
フラグを追加することで、同時に新しいデータベースを作成できます。
npx prisma init --db
プロンプトが完了したら、新しいデータベースにアクセスします。
- を開きます。:
- ログインして、新しく作成したデータベースプロジェクトを見つけます。
- データベースの認証情報を設定する
- サイドバーで、Databaseをクリックし、Setupを選択します。
- Existing projectを選択し、Generate database credentialsを押します。
- 環境を設定する
- プロジェクトのルートに新しい
.env
ファイルを作成します。 - 生成したばかりの
DATABASE_URL
をこのファイルにコピー&ペーストします。以下のような形式になります。
DATABASE_URL="prisma+postgres://accelerate.prisma-data.net/?api_key=..."
- プロジェクトのルートに新しい
- データベーススキーマを適用する
- 以下のコマンドを実行してテーブルを作成し、データベースの準備をします。
npx prisma migrate dev --name init
次に進む前に、db.server.ts
ファイルを更新して、新しく生成されたPrismaクライアントを使用するようにしましょう。
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from "./generated/prisma/client.js";
if (process.env.NODE_ENV !== "production") {
if (!global.prismaGlobal) {
global.prismaGlobal = new PrismaClient();
}
}
const prisma = global.prismaGlobal ?? new PrismaClient();
export default prisma;
app/generated/prisma
を.gitignore
ファイルに追加することを推奨します。
3. Remixモデルを作成する
プロジェクトを整理するために、新しいmodels/
フォルダーを作成しましょう。このフォルダーの中に、notes.server.js
というファイルを追加します。これはすべてのメモ関連ロジックの拠点となり、アプリの成長に伴ってコードベースを管理しやすくなります。
notes.server.js
ファイルには2つの関数が含まれます。
getNotes
- 特定の製品のすべてのメモを取得します。createNote
- 特定の製品の新しいメモを作成します。
まず、db.server.ts
からPrismaクライアントをインポートし、getNotes
関数を作成します。
import prisma from "../db.server";
export const getNotes = async (productGid) => {
const notes = await prisma.productNote.findMany({
where: { productGid: productGid.toString() },
orderBy: { createdAt: "desc" },
});
return notes;
};
ユーザーがデータベースに新しいメモを追加できるように、notes.server.js
にprisma.productNote.create
を使用する関数を作成しましょう。
import prisma from "../db.server";
export const getNotes = async (productGid) => {
const notes = await prisma.productNote.findMany({
where: { productGid: productGid.toString() },
orderBy: { createdAt: "desc" },
});
return notes;
};
export const createNote = async (note) => {
const newNote = await prisma.productNote.create({
data: {
body: note.body,
productGid: note.productGid,
},
});
return newNote;
};
4. レイアウトルートを作成する
これらの関数を呼び出す前に、ルートが配置されるレイアウトが必要です。このレイアウトルートには製品選択ボタンがあり、ProductNotes
ルートの親として機能し、アプリを整理してユーザーフレンドリーに保ちます。
4.1. ProductNotesLayoutコンポーネントを作成する
まず、routes/app.product-notes.jsx
フォルダーを作成し、その中にProductNotesLayout
コンポーネントを追加します。
import { Page, Layout } from "@shopify/polaris";
export default function ProductNotesLayout() {
return (
<Page title="Product Notes">
<Layout>
<Layout.Section></Layout.Section>
</Layout>
</Page>
);
}
次に、selectProduct
関数と、ユーザーが製品を選択できるようにするButton
を作成します。
import { useNavigate } from "@remix-run/react";
import { Page, Layout } from "@shopify/polaris";
import { Button, Page, Layout } from "@shopify/polaris";
export default function ProductNotesLayout() {
const navigate = useNavigate();
async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select",
});
const selectedGid = products[0].id;
navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`);
}
return (
<Page title="Product Notes">
<Layout>
<Layout.Section>
<Button onClick={selectProduct} fullWidth size="large">
Select Product
</Button>
</Layout.Section>
</Layout>
</Page>
);
}
Remixはネストされたルートをレンダリングする機能を提供します。routes/app.product-notes.jsx
ファイルに<Outlet />
を追加し、そこにProductNotes
ルートがレンダリングされるようにします。
import { useNavigate } from "@remix-run/react";
import { Outlet, useNavigate } from "@remix-run/react";
import { Page, Button, Layout } from "@shopify/polaris";
export default function ProductNotesLayout() {
const navigate = useNavigate();
async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select",
});
const selectedGid = products[0].id;
navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`);
}
return (
<Page title="Product Notes">
<Layout>
<Layout.Section>
<Button onClick={selectProduct} fullWidth size="large">
Select Product
</Button>
</Layout.Section>
<Outlet />
</Layout>
</Page>
);
}
4.2. ProductNotesLayoutをサイドバーに追加する
npm run dev
を実行しても、Product Notes
ルートは表示されません。これを修正するには、ProductNotesLayout
をapp.jsx
ファイルに追加して、サイドバーに表示されるようにする必要があります。
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
import { boundary } from "@shopify/shopify-app-remix/server";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import { NavMenu } from "@shopify/app-bridge-react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
import { authenticate } from "../shopify.server";
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
export const loader = async ({ request }) => {
await authenticate.admin(request);
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};
export default function App() {
const { apiKey } = useLoaderData();
return (
<AppProvider isEmbeddedApp apiKey={apiKey}>
<NavMenu>
<Link to="/app" rel="home">
Home
</Link>
<Link to="/app/product-notes">Product Notes</Link>
</NavMenu>
<Outlet />
</AppProvider>
);
}
// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
return boundary.error(useRouteError());
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};
5. 商品メモのルートを作成する
現在、npm run dev
を実行してProduct Notes
ルートに移動し、製品を選択しても何も表示されません。
以下の手順に従って、商品メモのルートを作成します。
新しいroutes/app/app.notes.$productGid.jsx
ファイルを作成します。このファイルは、productGidをパラメーターとして受け取り、製品に関連する製品メモと、新しいメモを作成するためのフォームを返します。
export default function ProductNotes() {
return (
<></>
);
}
5.1. メモをレンダリングする
ロード時に、ルートは製品のメモを取得して表示する必要があります。
ルートにloader
関数を追加します。
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
return (
<></>
);
}
Polarisコンポーネントを使用して、ProductNotes
コンポーネントでメモをマップします。
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
return (
<>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}
「まだメモがありません。」と表示されるはずです。もしそうなら、正しい方向に進んでいます。
5.2. フォームを追加する
新しいメモを作成するために、ルートにいくつかの項目を追加する必要があります。
- ルートに
action
関数を追加します。 - メモが作成されたときに
Toast
通知を表示します。 createNote
関数をmodels/note.server.js
からインポートします。useActionData
とuseAppBridge
をインポートします。
import { json, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { useLoaderData, useActionData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
import { getNotes, createNote } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
import { useAppBridge } from "@shopify/app-bridge-react";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export const action = async ({ request, params }) => {
const formData = await request.formData();
const body = formData.get("body")?.toString() || null;
const { productGid } = params;
await createNote({ productGid, body });
return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`);
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
const actionData = useActionData();
const app = useAppBridge();
useEffect(() => {
if (actionData?.ok) {
app.toast.show("Note saved", { duration: 3000 });
setBody("");
}
}, [actionData, app]);
return (
<>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}
これで、action
関数を呼び出すフォームを構築できます。
import { json, redirect } from "@remix-run/node";
import { useLoaderData, useActionData } from "@remix-run/react";
import { getNotes, createNote } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
import { Card, Layout, Text, BlockStack, Form, FormLayout, TextField, Button } from "@shopify/polaris";
import { useAppBridge } from "@shopify/app-bridge-react";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export const action = async ({ request, params }) => {
const formData = await request.formData();
const body = formData.get("body")?.toString() || null;
const { productGid } = params;
await createNote({ productGid, body });
return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`);
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
const actionData = useActionData();
const app = useAppBridge();
useEffect(() => {
if (actionData?.ok) {
app.toast.show("Note saved", { duration: 3000 });
setBody("");
}
}, [actionData, app]);
return (
<>
<Layout.Section>
<Card sectioned>
<Form method="post">
<FormLayout>
<BlockStack gap="200">
<input type="hidden" name="productGid" value={productGid} />
<TextField
label="Note"
value={body}
onChange={setBody}
name="body"
autoComplete="off"
multiline={4}
/>
<Button submit primary>
Add Note
</Button>
</BlockStack>
</FormLayout>
</Form>
</Card>
</Layout.Section>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}
これで、商品にメモを追加し、それが表示されるのを確認できるようになります。
6. ルートをテストする
npm run dev
を実行し、Product Notes
ルートに移動します。
- サイドバーのProduct Notesに移動します。
- 製品を選択します。
- メモを追加します。
- メモが正しく表示され、保存されることを確認します。
次のステップ
ShopifyアプリをPrisma Postgresデータベースに接続して動作するようになったので、以下を行うことができます。
- Prismaスキーマをさらに多くのモデルとリレーションで拡張する
- 作成/更新/削除ルートとフォームを追加する
- パフォーマンス向上のためにPrisma Postgresでクエリキャッシュを有効にする
詳細情報
Prismaとつながる
Prismaの旅を続けるために、以下とつながりましょう。 私たちの活発なコミュニティ。情報収集、参加、他の開発者との協力ができます。
- Xでフォローする アナウンス、ライブイベント、役立つヒントをお届けします。
- Discordに参加する 質問したり、コミュニティと話したり、会話を通して活発なサポートを受けたりできます。
- YouTubeを購読する チュートリアル、デモ、ストリームを視聴できます。
- GitHubで関わる リポジトリにスターを付けたり、問題を報告したり、問題に貢献したりできます。