我把資料庫放到 Google Sheets 上了

大家好,我是卡米哥。今天要教大家怎麼用 Google Sheets 存放資料並且在 rails 當中使用。

在本次的教學中,我們主要的需求是想要直接以 Google Sheets 作為後台,來取代 rails 原本的資料庫以及需要自己寫的管理介面。

使用 Google Sheets 作為後台,比起自己做的管理介面,有許多好處,像是可以省下製作後台的時間,編輯速度快,支援共同編輯,支援版本管理等。

注意:在本次的教學中我們不保證資料的安全性。

建立 Google Sheet 並且發布成 CSV 格式

首先先新增一個 Google Sheet,我這邊做好了一個示範:

https://docs.google.com/spreadsheets/d/1kDEbaStqDG-g6HTTwbUx_MOWTtmHtISZzMtcinAKQqI/edit?usp=sharing

然後將這個 sheet 做發布:

點選 File

file dropdown menu

點選 Publish to the web...

Publish to the web...

點選 Entire Document 改選為 Sheet1

Sheet menu

點選 Web page 改選為 Comma-separated values (.csv)

Format menu

都選好的時候看起來像這樣:

Ready to publish

點選 Publish 並且按下確定後:

after publish

把中間的網址複製起來,我的 csv 做好之後的連結如下:

https://docs.google.com/spreadsheets/d/e/2PACX-1vROPREvtOJD2nRGeRjXAzFgG2Qx4Zdx0DM1bLl7VsPtI8ftp7juEW7g0IPhGsXI5g-fNWdfl1erwV7M/pub?gid=0&single=true&output=csv

使用瀏覽器開啟這個網址時,會下載一個 csv 檔。若使用文字編輯器開啟這個檔案,會看到以下內容:

keyword,message
喔~,氣氣氣氣氣

請注意,任何人只要擁有這個網址,就能夠讀取到完整的資料內容。

在 Rails 讀取 CSV 並且解析為 Model

首先,先建立一個新的專案:

rails new google_sheets_demo

./app 資料夾下建立 sheets 資料夾,在 sheets 資料夾中新增一個 keyword.rb 檔案,並且輸入以下內容:

require 'net/http'
require 'csv'

class Keyword
  SHEETS_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vROPREvtOJD2nRGeRjXAzFgG2Qx4Zdx0DM1bLl7VsPtI8ftp7juEW7g0IPhGsXI5g-fNWdfl1erwV7M/pub?gid=0&single=true&output=csv'

  attr_accessor :keyword
  attr_accessor :message

  def initialize(keyword: nil, message: nil)
    self.keyword = keyword
    self.message = message
  end

  def self.all
    body = Net::HTTP.get(URI(SHEETS_URL)).force_encoding('UTF-8')
    sheet = CSV.parse(body)
    title = sheet.shift.map(&:to_sym)
    sheet.map do |row|
      Keyword.new(**title.zip(row).to_h)
    end
  end
end

以上的程式碼就是在做一個類似 Model 的 all 方法,用來取得所有的資料,我大概解釋一下原理:

  1. 使用 Net::HTTP.get 取得 csv 資料
  2. 使用 CSV 解析 csv 資料
  3. 去除標題列
  4. 將資料轉換為物件模型

好的,那麼馬上來試驗一下,先進入 rails console 後輸入以下程式碼:

Keyword.all

其結果為:

 => [#<Keyword:0x00007faf28560e58 @keyword="喔~", @message="氣氣氣氣氣">]

取得了一個陣列,雖然沒辦法使用 Active Record 提供的 where 功能,但 Array 以及 Enumerable 還是有類似的內建方法可以用。

雖然取得了資料,但每次都會去抓取是不好的,畢竟後台的資料不會一直更新,而且 google sheets 的抓取時間其實蠻久的,大約要2~3秒左右。

實作簡易的快取機制

我們可以增加一個快取機制,讓他每分鐘重抓一次就好,我們對 keyword.rb 進行一些調整,以下是調整後的程式碼:

require 'net/http'
require 'csv'

class Keyword
  SHEETS_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vROPREvtOJD2nRGeRjXAzFgG2Qx4Zdx0DM1bLl7VsPtI8ftp7juEW7g0IPhGsXI5g-fNWdfl1erwV7M/pub?gid=0&single=true&output=csv'
  $cached_keywords = nil
  $cached_keywords_expired_at = nil

  attr_accessor :keyword
  attr_accessor :message

  def initialize(keyword: nil, message: nil)
    self.keyword = keyword
    self.message = message
  end

  def self.all
    if $cached_keywords_expired_at.nil? || $cached_keywords_expired_at < Time.current
      body = Net::HTTP.get(URI(SHEETS_URL)).force_encoding('UTF-8')
      sheet = CSV.parse(body)
      title = sheet.shift.map(&:to_sym)
      $cached_keywords = sheet.map do |row|
        Keyword.new(**title.zip(row).to_h)
      end

      $cached_keywords_expired_at = 1.minutes.since
    end

    $cached_keywords
  end
end

我們將快取儲存在整個網站的全域變數上,也就是錢字號 $ 開頭的變數上,如此一來就能夠加快查詢速度。

在每次取得 Keyword.all 的時候先檢查是否存在快取,如果不存在的話,才去抓取 CSV 以及解析成 Model 物件。

實際應用的話,我會建議將快取功能以及 Sheets 功能拆解成獨立的檔案,最理想的情況則是做成 gem,在這裡為求簡單理解,就不將問題複雜化了。

試用做好的 Model

那麼我們做個簡單的網頁介面來試用看看這個 Model。

先修改 config/routes.rb 建立一個 routes 規則指向 chatbot#talk

Rails.application.routes.draw do
  get 'talk/:say', to: 'chatbot#talk'
end

然後在 app/controllers 資料夾下建立一個 chatbot_controller.rb,內容如下:

class ChatbotController < ApplicationController

  def talk
    reply_message = Keyword.all.find{ |keyword| keyword.keyword == params[:say] }&.message
    render plain: reply_message || "蛤?"
  end
end

接著開啟 rails server 做測試,在瀏覽器輸入網址 http://localhost:3000/talk/喔~ 後,就會看到:

氣氣氣氣氣

那麼今天的教學就到這邊,我是卡米哥,謝謝收看。