iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 14
0
Modern Web

向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List系列 第 14

[DAY 14] 復刻 Rails - 實作 ORM 初體驗

昨天我們了解到 Migration 做的事情,也透過 Migration 建立了第一個 Table,還處理了 Model 和 Table 單數複數的轉換問題,今天將會開始來實作 ORM 的新增資料部分

用 SQL 語法來新增資料

還記得我們在之前的 file_model 也做過類似的事情,但不同以往的是,這次我們真的要用 SQL 語法來新增一筆資料,而不是用新增檔案的方式,在開始講解前,先直接貼上今天的程式碼吧

# mavericks/lib/mavericks/sqlite_model.rb

# .
# .
# (略)
module Mavericks
  module Model
    class SQLite

      def initialize(data = nil)
        @hash = data
      end

      def self.to_sql(val)
        case val
        when Numeric
          val.to_s
        when String
          "'#{val}'"
        else
          raise "Can't support #{val.class} to SQL!"
        end
      end

      def self.create(values)
        values.delete :id
        keys = schema.keys - ['id']
        vals = keys.map do |key|
          values[key.to_sym] ? to_sql(values[key.to_sym]) : "null"
        end

        DB.execute <<-SQL
          INSERT INTO #{table} (#{keys.join ","})
          VALUES (#{vals.join ","});
        SQL

        data = Hash[keys.zip values.values]
        sql = "SELECT last_insert_rowid();"
        data["id"] = DB.execute(sql)[0][0]
        self.new data
      end

      def self.count
        DB.execute(<<-SQL)[0][0]
          SELECT COUNT(*) FROM #{table}
        SQL
      end
      
      # .
      # .
      # (略)
    end
  end
end

其實程式碼並不難,做得事情也很單純,就讓我來一步一步講解吧

ORM 的精髓

前面曾經說過,我們會以 類別名稱 對應到 表格名稱,那 物件 自然就對應到表格的 每一筆資料,利用這樣的設計,讓我們可以做到 物件關聯對映,也就是所謂的 ORM,所以這裡我們可以用 initialize 來替每一個物件做初始化,當作新增資料時的接口,我們簡單的將資料以 Hash 的方式存在 instance variable(實體變數)裡面,方便讓我們對資料進行操作

def initialize(data = nil)
  @hash = data
end

接著我們建立了一個 class methodto_sql,用來處理寫入資料庫所需要的語法轉換,例如在 SQL語法 裡面,我們會把字串用單引號包起來

# 一般的 SQL 語法大概長這樣,會發現字串都會用 '' 來包住
INSERT INTO Tasks (C_Id, title, content)
VALUES (3, '鐵人30', '每天發一篇文章');

所以我們必須比照當初 Schema 的欄位類型,來轉換相對應的語法

def self.to_sql(val)
  case val
  when Numeric
    val.to_s
  when String
    "'#{val}'"
  else
    raise "Can't support #{val.class} to SQL!"
  end
end

關於 create method

還記得 Rails 怎麼新增一筆資料嗎?

Task.create("title": "鐵人30", "content": "每天一篇文章")

我們將會傳一個 Hash 來新增資料 ,處理方式很簡單,就是把 key 和 value 做分離,這邊有個地方要注意的是,處理的過程中,要把 ID 拿掉,因為在 INSERT 的過程中, SQLite 會自動生成一個 ID,處理的程式碼如下

values.delete :id
keys = schema.keys - ['id']
vals = keys.map do |key|
  values[key.to_sym] ? to_sql(values[key.to_sym]) : "null"
end

會看到我們取得昨天做得 schema 並且把 ID 去掉,接著利用 map 來處理每個欄位資料

# vals 會長這樣
# vals = ["'鐵人30'", "'每天一篇文章'"]

接著利用 SQL 語法的INSERT INTO

INSERT INTO table_name (column1, column2, column3, ...)
VALUES (value1, value2, value3, ...);

加上之前處理好的 key 和 value,用 heredoc 串起 SQL 語法並且執行,這時候才是真正將資料存在資料庫裡面

DB.execute <<-SQL
  INSERT INTO #{table} (#{keys.join ","})
  VALUES (#{vals.join ","});
SQL

最後完成 create 以後,這邊我們用 zip,來將 key 和 value 做 merge,然後重新 new 一個物件,記得要把 SQL 產生的 ID 值加回去,因為每新增一筆資料資料庫會自動生成 ID,靠著內建 SQL 的語法 last_insert_rowid 我們會知道剛剛新增的 ID 是多少,並且加回去新 new 出來的物件,這樣之後對這個物件進行操作時,不管是刪除還是修改,才知道要依據那個 ID 的資料做操作

data = Hash[keys.zip values.values]
sql = "SELECT last_insert_rowid();"
data["id"] = DB.execute(sql)[0][0]
self.new data

另外為了讓我們方便知道資料有沒有成功新增,就一起把 count 的方法也加上去,這裡利用 SQL 語法的 COUNT 來得知現在總共有多少筆資料,因為 DB.execute 最後會回傳像 [[5]] 這樣的結構,所以我們要用 [0][0] 來取得 5

def self.count
  DB.execute(<<-SQL)[0][0]
    SELECT COUNT(*) FROM #{table}
  SQL
end

這樣實作部分的程式碼都解說完了,讓我們回到 just_do 的 sqlite_test.rb 來執行看看結果吧

require 'sqlite3'
require 'mavericks/sqlite_model'

class Task < Mavericks::Model::SQLite

end

Task.create('title': '鐵人30', 'content': '每天發一篇')
puts Task.count

試試看每執行一次 sqlite_test.rb,印出來的數字應該都會 +1,如果有順利 +1 代表資料數量有增加,也代表我們成功新增資料了

$ bundle exec ruby sqlite_test.rb
# 1

看起來現在我們有初步的 ORM 功能,也把 CRUDC 給完成了,明天會繼續將其他功能陸續補上,但如果對於其他部分已經知道怎麼做的人,也可以先做,到時候再看我們是不是做得一樣XD,一起堅持下去吧!


上一篇
[DAY 13] 復刻 Rails - 進入 ORM 前,先了解 Migration
下一篇
[DAY 15] 復刻 Rails - 更多的 ORM 實作
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言