Notionあれやこれや

情報一元化ツールNotion遊泳のTips

NotionのCSV取込みでリレーション張れないからGASで一括設定した

Notionってデータベースを直感的に作成できて、それが簡単に様々なレイアウト表示できるという利点が魅力ですよね。異なるテーブルを、多対多のリレーションできるのも素敵じゃないですか。

で、CSVで複数データを一括登録するとき、リレーションもできるでしょ!という頭で取り込んだら、張れていないわけですね。ショック🤯

仕組みを理解すれば、まあ難しいよねというのは理解できるんですが、手作業で全部設定するのはしんどいよ・・と思って、GAS(Google Apps Script)で一括リレーション設定にトライしてみた話です。

茶番含めて時系列で書いていきますので、GASのコードだけ見たい!という方は、目次の「コード.gsのサンプルコード」からどうぞ。

やりたかった完成図

「食材一覧」と「悩み別レシピ」という2つのテーブルがあります。この二つを関連付けしました。

具体的な使用例

🍅 食材一覧

スーパーにて「アボカドが安い!何に効くんだっけ。いいレシピあれば追加食材も一緒に買っておきたいな」

😷 悩み別レシピ

「風邪引きそう!家にある食材で薬膳できないかなあ」


こんなかんじで、目次と索引みたいな感じのテーブルを目指しました。

実際に作ってみよう

手元にいろいろ散らかったレシピ集があるので、CSVに整理して一括登録することにしました。

1. 2つのテーブルを作成

「食材一覧」と「悩み別レシピ」のふたつ。とりあえず最低限の項目を用意します。

🍅 食材一覧

  • 食材名|タイトル
  • 分類|セレクト
  • 😷 悩み別レシピ|リレーション|対象:😷 悩み別レシピ
    • ※プロパティ設定で「悩み別レシピに表示」をオンにすると、「悩み別レシピ」データベースにもリレーション項目が自動追加される
  • 効能|ロールアップ|リレーション:😷 悩み別レシピ|プロパティ:悩み|計算:オリジナルを表示する

😷 悩み別レシピ

  • レシピ名|タイトル
  • 悩み|セレクト
  • 🍅 食材一覧|リレーション|対象:🍅 食材一覧

2. 取り込むCSVを用意

それぞれ、項目の見出しは忘れずに

3. まずは食材一覧をCSV取り込み

早速メニューから「CSV取り込み」で先ほどのCSVを選択してインポート!

「分類」(種類:セレクト)の項目が空欄です。なぜ?

オプション一覧に新規登録されないんですね~
ならば・・

一度、種類を「テキスト」に変えると表示されました。
そこから・・

再び、種類を「セレクト」に変更。
オプション化されて一安心。

4. 悩み別レシピをCSV取り込み

新しい「食材一覧」の列が出来たぞ・・?

「食材一覧」項目も、あとから種類変更でリレーションにすれば出来るか・・・?

上手くいかないぞ、なんてこった🤢

🙃インポートでリレーションは張れない

■ Notion よくあるご質問(FAQ)
❓ リレーションをエクスポートやインポートできますか?
リレーションを含むデータベースをCSVファイルとしてエクスポートすると、リレーションプロパティは、プレーンテキストでURLとしてエクスポートされます。現時点では、そのCSVをNotionに再インポートすることで他のデータベースとのリレーションを再構築することはできません。

リレーションとロールアップ – Notion (ノーション)ヘルプセンター

以下リンクも参考までに貼り付け。Notion APIでリレーション設定はできるらしいけど、あくまでデータベースのプロパティに、紐づけたい対象データベースを設定するだけっぽい。私がやりたいのは、プロパティ(項目)ではなく行の一括登録なの・・

developers.notion.com

5. GASで一括設定してやろうじゃないか

いったん、手順4で行った2つ目のデータベース(悩み別レシピ)のCSV取込み直後の状態に戻します。

一旦もとに戻しておく

GASでの処理で各食材名を取得しやすいように、種類はマルチセレクトへ変更しておく
あと、リレーションする項目と名前が被らないよう「食材一覧」→「食材」へ変更しておく

6. 必要な設定・キー等を取得しておく

  1. Notionインテグレーションを作成・シークレットキーをコピーしておく
  2. 各データベースに、上記1のインテグレーションをコネクト追加する
  3. 各データベースのIDをコピーしておく

上記の具体的な方法は、以下の記事をご参照ください。
目次の手順①と手順②が参考になるかなと思います😀

aki-toto.hateblo.jp

7. Google Apps Script(GAS)にコード貼り付け

コード内の上三行は、自身で取得したキーやIDに貼り替えてください。

コード.gsのサンプルコード

const NOTION_API_KEY = 'インテグレーションのシークレットを貼り付けてください'; // 【インテグレーションシークレット】
const FOODS_DATABASE_ID = '1つ目のデータベースIDを貼り付けてください'; // 【食材一覧DB】
const WORRIES_DATABASE_ID = '2つ目のデータベースIDを貼り付けてください'; // 【悩み別レシピDB】

/**
 * メイン処理
 */
function setRelation() {
  // 全食材取得
  const foods = getFoods();
  // 全悩み取得
  const worries = getWorries();

  // 各悩み別レシピごとに食材をリレーション
  worries.forEach(worry => {
    const foodPageIds = [];
    worry.foodItems.forEach(itemName => {
      // リレーション先となる食材ページIDを取得
      let foodPageId = getFoodPageId(foods, itemName);
      if (foodPageId) {
        foodPageIds.push({ id: foodPageId });
      }
    });

    // 更新
    if (foodPageIds.length !== 0) {
      updateRelation(worry.pageId, foodPageIds);
    }
  });
}

/**
 * 「食材一覧」DBの食材取得
 */
function getFoods() {
  const url = `https://api.notion.com/v1/databases/${FOODS_DATABASE_ID}/query`;
  const options = {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${NOTION_API_KEY}`,
      'Content-Type': 'application/json',
      "Notion-Version": "2022-06-28",
    }
  };
  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());
  const foodDatas = data.results.map(food => {
    return {
      pageId: food.id,
      name: food.properties['食材名'].title[0].text.content,
    };
  });

  return foodDatas;
}

/**
 * 「悩み別レシピ」DBの食材取得
 */
function getWorries() {
  const url = `https://api.notion.com/v1/databases/${WORRIES_DATABASE_ID}/query`;
  const options = {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${NOTION_API_KEY}`,
      'Content-Type': 'application/json',
      "Notion-Version": "2022-06-28",
    }
  };
  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());
  const worriesData = data.results.map(worry => {
    const foodItems = worry.properties['食材'].multi_select.map(item => item.name);
    return {
      pageId: worry.id,
      foodItems: foodItems
    };
  });

  return worriesData;
}

/**
 * 悩み別レシピDBの項目「食材」から
 * 食材一覧DBの食材ページIDを取得
 */
function getFoodPageId(foods, itemName) {
  const matchingFood = foods.find(food => food.name === itemName);
  if (matchingFood) {
    return matchingFood.pageId;
  }
}

/**
 * 更新
 */
function updateRelation(worryPageId, foodPageIds) {
  const url = `https://api.notion.com/v1/pages/${worryPageId}`;
  const options = {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${NOTION_API_KEY}`,
      'Content-Type': 'application/json',
      "Notion-Version": "2022-06-28",
    },
    payload: JSON.stringify({
      properties: {
        "🍅 食材一覧": { // アイコンがあるならアイコンやスペースも忘れずに
          relation: foodPageIds
        },
      },
    }),
  };

  UrlFetchApp.fetch(url, options);
}

コード.gsに貼り付けたら、ツールバーの実行ボタンを押す。

8. 完成!

双方ともにリレーション設定完了!
「食材一覧」データベースに設定したロールアップ(「効能」項目)も連動反映されました◎

おわりに

一度GASでこういう処理をつくる作業をやってみると、自分であれこれ試行錯誤できて、本体機能で実現できないことも実現できちゃうのが面白いところです🥰

ちなみにNotion APIを使うにあたり、APIプラットフォームの『Postman』がテスト用に便利です。データ構造も確認できるし、Notion APIコレクション(データ取得・更新・追加などの設定がテンプレ化されたもの)も用意されています。
どこかのタイミングで紹介出来たらな~と思いつつ、以下の記事も丁寧に説明されているので、まずは参考先として貼らせていただきます🙇‍♀️

dev.classmethod.jp