15
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Next.js & Supabase】Supabaseって実際どうなの?

Last updated at Posted at 2021-12-01

はじめに

こんにちは!MYJLab Advent Calendar 2021の2日目を担当するサッチーです!
昨日の@chiba___さんの記事では、かわいいタートルを使って、アドベントカレンダー初日が盛り上がる記事を書いてくれました!⭐
わたしはアドベントカレンダーは3年連続の参加です!たのしんでいくぞ🔥

これは?

最近お友達との開発(あれ、研究...あれ、就活...)で、DBやAPIまわりの技術リサーチをしています。
その際に、最近自分の中で流行っている「GraphQL+Prisma」と、世間的に流行っている?「Supabase」を比較したいと思っていたので、今回はSupabaseを試してみました〜という記事をかきます!
次回の記事でGraphQLとPrismaのことを書く予定です。

Supabaseってなに?

Supabaseは、Firebaseの代替プラットフォームとして、登場しました。
詳しい説明は、こちらの記事で詳しくまとめられています!
今回はDBのみを試しますが、認証やストレージ機能も充実しています。
FirebaseとSupabaseの最もわかりやすい違いは、DBがPostgreSQLである点だと思います!
私はNo SQLが苦手なので、それだけでうれしいです。

やること

事前にProductsテーブルと、Tagsテーブルと、2つのテーブルの中間テーブルであるProductTagsテーブルを用意し、外部キーの設定までしておきます。(次回の記事でも使います)
ひとつの製品が複数のタグを持っていて、ひとつのタグは複数の製品につけられている多対多の状態です。
Supabaseでは、DBの作成〜外部キー設定などの基本的な操作は、GUIで簡単に行なえます。SQL Editorを使えばインデックスをはったり、RLCを設定したりなど、普通のPostgreSQLと同様に扱うことができます。
各テーブルのschemaは、以下のような感じです。

type Product = {
  id: number
  product_name: string
  released_at: string
  image_url: string
  hp_url: string
  average_price: number
  tags: Tag[]
}

type Tag = {
  id: number
  tag_name: string
  products: Product[]
}

type ProductTag = {
  id: number
  product_id: number
  tag_id: number
}

このDBを使って、

  • product一覧の取得
  • tagごとのproduct一覧の取得

を試してみます!
今回私のユースケースでは、データの更新をたまにしかしないので、Next.jsでSSGをする例になっています。
SSR、CSR、SSGなどの説明は本記事では割愛しますが、こちらの記事にとてもわかりやすくまとめられています〜⭐

実装

supabaseの設定

公式ドキュメント通りにセットアップをします。

「NEXT_PUBLIC_SUPABASE_URL」と「NEXT_PUBLIC_SUPABASE_ANON_KEY」を探し出して、.env.localに貼り付けます。
歯車→APIとたどると見つかると思います。
スクリーンショット 2021-11-27 17.53.06.png

.env.local
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
utils/supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

Product一覧

いちばんシンプルなProduct一覧からやってみます。

libs/product.ts
import { supabase } from '../utils/supabaseClient'

export async function getProducts(): Promise<Product[]> {
  const { data, error, status } = await supabase
    .from<Product>('products')
    .select(`
      *,
      tags (
        id,
        tag_name
      )
    `)

  if (error && status !== 406) {
    throw error
  }

  return data;
}

個人的に最高だなと思った点は、

  • selectの書き方がGraphQLライクでレスポンスの形が直感的にわかりやすい
  • 中間テーブル作るだけで、ネストしたデータまで一回のリクエストで取得できる

です!

データを表示する際は以下のように使えると思います。

pages/products/index.tsx
import { FC } from 'react'
import { getProducts } from '../../libs/product'
import ProductList from '../../components/templates/productList'

type Props = {
  products: Product[];
};

const Products: FC<Props> = ({ products }) => {
  return (
    <div>
      <ProductList products={products}/>
    </div>
  )
}

export async function getStaticProps() {
  const products = await getProducts();
  return {
    props: {
      products,
    },
  }
}

export default Products;

tagごとのproduct一覧

「いらすとや」というタグがついている商品のみを表示したい!みたいなケースです。
ルーティングはtag/[tagId]みたいな感じにしようと思います。
Next.jsでは、getStaticPathsというAPIを使うことで、SSGの際にも動的にルーティングを設定することができます。このAPIの中でpathを作るために、tagのID一覧を取得します。

libs/tag.ts
import { supabase } from '../utils/supabaseClient'

export async function getTags(): Promise<Tag[]> {
  const { data, error, status } = await supabase
    .from<Tag>('tags')
    .select(`
      id
    `)
	
  if (error && status !== 406) {
    throw error
  }
  return data;
}

次に、tagのIDを引数にとって、そのtagの名前と、tagがついているproduct(複数)を取得します。

libs/tag.ts
export async function getTags(): Promise<Tag[]> {
  // ・・・省略・・・
}

// ここから追加
export async function getTagProducts(tagId: number): Promise<Tag>{
  const { data, error, status } = await supabase
    .from<Tag>('tags')
    .select(`
      tag_name,
      products (
        *,
        tags (
          id,
          tag_name
        )
      )
    `)
    .eq('id', tagId)
    .single()
	
  if (error && status !== 406) {
    throw error
  }

  return data;
}

eq()でidがtagIdのものだけを取得するようにし、single()で返り値を配列ではなくて一つの要素にするようにしています。

取得したデータの表示

pages/tags/[tagId].tsx
import { FC } from 'react'
import { getTags, getTagProducts } from '../../libs/tag'
import ProductList from '../../components/templates/productList'

type Props = {
  tagName: string,
  products: Product[];
};

const TagProducts: FC<Props> = ({ tagName, products }) => {
  return (
    <div>
      <h1>{tagName}</h1>
      <ProductList products={products}/>
    </div>
  )
}

export const getStaticPaths = async () => {
  const tags = await getTags();
  const paths = tags.map((tag:Tag) => `/tags/${tag.id}`);
  return { paths, fallback: false };
};

export const getStaticProps = async (context) => {
  const tagId = context.params.tagId;
  const tagProducts = await getTagProducts(tagId);
  return {
    props: {
      tagName: tagProducts.tag_name,
      products: tagProducts.products,
    },
  };
};

export default TagProducts;

まとめ

  • 導入が簡単
  • PostgreSQL
  • GraphQLっぽく、返してほしいカラムを指定できる
  • TypeScriptと相性いい

など、趣味の開発では十分な機能が備わっているように感じました!
これから積極的に使っていきたいです。
気になる点や修正点などあればお申し付けください!フロントエンドまだまだ初心者なのでお手柔らかに🙏
最後までお読みいただきありがとうございました!

参考

15
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?