0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js + Express】Google スプレッドシートを無理やりデータベース化した話

Last updated at Posted at 2025-02-01

はしがき

この前遊びでちょっとしたワークフローサービスみたいなのを作ったときの話。
MySQL等の本物DBは扱いが面倒くさいのと、デプロイしても無料でデータベースを使いたいという理由からGoogle スプレッドシートを無理やりDB運用しました。
Google スプレッドシートだとデータベースへのアクセス・確認と直接のデータ操作が容易でUIもきれいなので非エンジニアでも使えるDBとして個人的には結構有用かと思います。
色々な記事を参考にしながら作りましたが、バージョンの違いや認証方法の違いなどで苦労しつつ、サービスアカウントを使った、一番簡単な方法で実装できたかと思います。

環境

フレームワーク: Node.js
API: Google APIs

やり方

Google Cloudでの作業

Google Cloudにアクセスしてコンソールを開き、新規プロジェクトを作成してください。
ナビゲーションメニューからAPIとサービスに進んで、Google Sheets APIを有効にしてください。
CleanShot 2025-01-31 at 23.18.23.png

CleanShot 2025-01-31 at 23.19.10.png

続きまして、APIとサービス>認証情報に進んでください。
認証情報を作成からサービスアカウントを選んでください。
CleanShot 2025-01-31 at 23.09.28.png

適当なIDを付けたあと、このサービスアカウントにプロジェクトへのアクセスを許可するで編集者ロールを付けてあげてから作成してください。
CleanShot 2025-01-31 at 23.22.49.png

作成後、作ったサービスアカウントを選択してからキーを追加>新しい鍵を作成に進んでください。
形式はそのままJSONにしてください。作成すると自動的にファイルがダウンロードされます。これは後で使うものです。

このJSONファイルはその名の通りです。絶対に外部へ流出しないようにしてください。

CleanShot 2025-01-31 at 23.15.32.png

Google Cloudでの作業は以上ですが、最後にサービスアカウントのIDをコピーしてください。
CleanShot 2025-01-31 at 23.37.28.png

Google スプレッドシートでの作業

DB用のスプレッドシートを作ります。
適当に命名した後、共有から先程コピーしたサービスアカウントのIDで共有してください。
CleanShot 2025-01-31 at 23.36.37.png

Google スプレッドシートでの作業はこれだけです。

APIのインストール&コーディング

プロジェクトディレクトリでターミナルからGoogle APIsをインストールします。

ターミナル
npm install googleapis

先ほどダウンロードされたJSONファイルを任意のフォルダ(publicとか)に投げます。
導入したいjsファイルで以下のコードを挿入します。

// モジュール関係/定数定義
const KEYFILE_PATH = "[先ほどのJSONファイルのパス]";
const { google } = require("googleapis");
// SPREADSHEET_IDはスプレッドシートのURLの~/spreadsheets/d/[ココ]/edit~の[ココ]の部分です。
const SPREADSHEET_ID = "[スプレッドシートのID]"

//認証関係(Google スプレッドシート用です)
function googleAuthenticate(){
    console.log("||Start Authentication||")
    try {
        // ここで認証。
        const auth = new google.auth.GoogleAuth({
            keyFile: KEYFILE_PATH,
            scopes: ['https://www.googleapis.com/auth/spreadsheets'],
        });
        console.log("||Authentication successful||");
        // ここでGoogle Sheets APIを返しています。なので関数名とはちょっと矛盾しています(笑)。
        return google.sheets({version: "v4", auth});
    } catch(error) {
        console.log(error);
    }
}

以上がGoogle スプレッドシートを操作するために必要な認証関係です。

サンプルコード

以下で操作のサンプルコードを紹介します。

関数以外のコードはExpressのgetやpostの中(?)に書かないとエラーを吐きます。
requireを使うのがだめなようなのですが、今回はExpress環境で動くので良いことにさせてください。

↓今回のサンプルデータベース
CleanShot 2025-02-01 at 00.43.37.png

Google SheetsをDB化するにあたって取得したデータからはレコードの行番号を取得できないのでF列に=ROW()を入れることで行番号を無理やり取得できるようにしています。工夫ポイントです(^^)

データ取得(Select)

データ取得
async function getData(sheet){
    // Google Sheets APIのインスタンス化
    const sheets = googleAuthenticate();
    try {
        // データ取得
        const result = await sheets.spreadsheets.values.get({
            spreadsheetId: SPREADSHEET_ID, // スプレッドシートのID
            range: `${sheet}!A2:F` // 取得する範囲指定
        });
        return result.data;
    } catch (error) {
        console.log(error);
    }
}

const data = await getData("シート1");
console.log(data);

実行結果

ターミナル
||Start Authentication||
||Authentication successful||
{
  range: "'シート1'!A2:F1000",
  majorDimension: 'ROWS',
  values: [
    [ '1', '渋沢 栄一', '91', 'Male', '実業家', '2' ],
    [ '2', '津田 梅子', '64', 'Female', '女子教育家', '3' ]
  ]
}

Google Sheets APIの戻り値はオブジェクトです。
データはvaluesに2次元配列でレコードごとに格納されています。
なのでデータだけを取り出したければconsole.log(data.values);になります。
また、要素はすべて文字列になります。数値などは適宜キャストする必要があります。

データ書き込み(Insert)

データ書き込み
// この関数は最終行のレコードを取得するための関数です。
const getLastData = async (sheet) => {
    const data = await getData(sheet);
    if(data.values === undefined){
        return null;
    }
    return data.values[data.values.length - 1];
}


async function addData(sheet, name, age, sex, job){
    // Google Sheets APIのインスタンス化
    const sheets = googleAuthenticate();
    try {
        // IDインクリメントのために最終行のレコードを取得
        const lastData = await getLastData(sheet);
        // インクリメント処理
        let incrementId;
        if(lastData === null){
            incrementId = 1; // データがまだなければIDは1
        }else{
            incrementId = parseInt(lastData[0]) + 1; //最終レコードのID + 1
        }
        
        // データ追加(append)
        const result = await sheets.spreadsheets.values.append({
            spreadsheetId : SPREADSHEET_ID,
            range: `${sheet}!A1`, // テーブルの左上端を指定。appendだと最後に挿入してくれます。
            valueInputOption: "USER_ENTERED", // 入力データの解釈方法
            insertDataOption: "INSERT_ROWS", // 入力データの挿入方法 
            // 追加するデータ
            resource:{
                values: [[incrementId, name, age, sex, job, "=ROW()"]]
            }
        })
        console.log("||New record saved||");
        console.log(result.data);
    } catch (error) {
        console.log(error);
    }
}

            // シート名, 名前, 年齢, 性別, 職業
await addData("シート1", "野口 英世", 51, "Male", "細菌学者");
const data = await getData("シート1");
console.log(data);

実行結果

ターミナル
||Start Authentication||
||Authentication successful||
||Start Authentication||
||Authentication successful||
||New record saved||
{
  spreadsheetId: '1npUO3tdGDw73HwUGBt93OffZQv9H_lLI2TmGtzZ9aiY',
  tableRange: "'シート1'!A1:F3",
  updates: {
    spreadsheetId: '1npUO3tdGDw73HwUGBt93OffZQv9H_lLI2TmGtzZ9aiY',
    updatedRange: "'シート1'!A4:F4",
    updatedRows: 1,
    updatedColumns: 6,
    updatedCells: 6
  }
}
||Start Authentication||
||Authentication successful||
{
  range: "'シート1'!A2:F1001",
  majorDimension: 'ROWS',
  values: [
    [ '1', '渋沢 栄一', '91', 'Male', '実業家', '2' ],
    [ '2', '津田 梅子', '64', 'Female', '女子教育家', '3' ],
    [ '3', '野口 英世', '51', 'Male', '細菌学者', '4' ]
  ]
}

CleanShot 2025-02-01 at 17.09.41.png

valueInputOption: というのはデータを入力した際に数式(=SUM()=ROW())があった際にどう処理するかというものです。"USER_ENTERED"の場合、そのまま数式として処理されます。"RAW"とすると文字列として処理されます。
今回"USER_ENTERED""=ROW()"をインデックス5番に指定しているので、5列目(F列)に動的な行番号が記録されます。

データ書き換え(Update)

新紙幣のメンツのなかに旧紙幣の野口 英世さんが混ざってしまいました。
ということで野口 英世さんから北里 柴三郎さんに書き換えましょう。

データ書き換え
// この関数はIDから特定のデータを取得するための関数です。
const getTargetedIdData = async (sheet, id) => {
    try {
        const data = await getData(sheet);
        let target = [];
        data.values.forEach((item) => {
            if(parseInt(item[0]) === parseInt(id)) {
                console.log(item);
                console.log(item[0]);
                target.push(item);
            }
        })
        return target;
    } catch(error) {
        console.log(error);
    }
}



async function updateData(sheet, id, name, age, sex, job){
    // Google Sheets APIのインスタンス化
    const sheets = googleAuthenticate();
    try {

        const targetData = await getTargetedIdData(sheet, id);
        
        // データ書き換え
        const result = await sheets.spreadsheets.values.update({
            spreadsheetId : SPREADSHEET_ID,
            range: `${sheet}!A${targetData[0][5]}`, // 対象レコードのF列に格納した行番号を指定
            valueInputOption: "USER_ENTERED", // 入力データの解釈方法
            // 書き換えるデータ。書き換えなくて良いところはnullを指定します。
            resource:{
                values: [[null, name, age, sex, job, null]]
            }
        })
        console.log("||Record updated||");
        console.log(result.data);
    } catch (error) {
        console.log(error);
    }
}

                // シート名, ID, 名前, 年齢, 性別, 職業
await updateData("シート1", 3, "北里 柴三郎", 78, "Male", "微生物学者, 教育者");
const data = await getData("シート1");
console.log(data);

この書き換え処理をするときに行番号が必要になってきます。
range: `${sheet}!A${targetData[0][5]}`
targetDataには書き換え対象のレコードが2次元配列で格納されており、そのレコードのF列(インデックス番号でいう5番)に格納されている行番号を取得してます。
この場合だとrange: `${sheet}!A${targetData[0][5]}`の変数代入後はrange: `シート1!A4`となります。

実行結果

ターミナル
||Start Authentication||
||Authentication successful||
||Start Authentication||
||Authentication successful||
[ '3', '野口 英世', '51', 'Male', '細菌学者', '4' ]
3
||Record updated||
{
  spreadsheetId: '1npUO3tdGDw73HwUGBt93OffZQv9H_lLI2TmGtzZ9aiY',
  updatedRange: "'シート1'!A4:E4",
  updatedRows: 1,
  updatedColumns: 4,
  updatedCells: 4
}
||Start Authentication||
||Authentication successful||
{
  range: "'シート1'!A2:F1001",
  majorDimension: 'ROWS',
  values: [
    [ '1', '渋沢 栄一', '91', 'Male', '実業家', '2' ],
    [ '2', '津田 梅子', '64', 'Female', '女子教育家', '3' ],
    [ '3', '北里 柴三郎', '78', 'Male', '微生物学者, 教育者', '4' ]
  ]
}

CleanShot 2025-02-01 at 17.00.24@2x.png

新紙幣メンツが揃いましたね。

以上がサンプルコードです。
Deleteの方法は公式ドキュメントなどを参考にしながら作ってみてください(作れって言われたら作ります)。

ちなみに、このサンプルコードは見れば分かる通りGoogle Auth認証を処理ごとに何度も繰り返すようになっています。
無駄な認証リクエストをなくしたい場合は、ご自身でのコードの修正をお願いします(_ _)。

まとめ

最後の最後に適当なところが残っていますが、だいたいこんな感じです。v4でサービスアカウントを使って実装する方法がなかなかインターネットを調べても見つからず苦労しました。本当にお遊び程度ならデータの管理も楽ちんで、お金もかからないので、もしよかったら使ってみてください。
他のデータの取得方法などについては公式ドキュメントをご参照ください。

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?