進度條

【實體課程】WordPress 跨界網站接案講座暨工作坊 | 開源研究室 x 進度條線上課程

開源研究室 x 進度條線上課程,台北場盲鳥票開賣中!! 至 11/08 (日) 23:50 為止喔!購票與詳情請看「KKTIX 購票頁面」或「進度條文章

SQL 語法好難背不起來?關聯式資料庫苦無對策?或許你該試試 ORM 函式庫!

你寫程式不用框架嗎?建議可以先用框架完成功能再回去練習原理細節喔!調整學習順序可以進展更順利。

作者: 進度條編輯群 更新日期:

對於關聯式資料庫苦手的你來說,是不是有更好的方式來處理資料庫之間與物件的對應問題呢?如果你心有戚戚焉,或許你該使用看看 ORM 函式庫?

 

 

在後端的世界裡,雖然 NoSQL 形式的資料庫有一定的擁護者與確實適合的情框架構,但是就目前而言,「關聯式資料庫」幾乎還是所有網站的基石。所以對於一個後端的開發者來說,完全不了解 SQL 語法基本上就是職涯唯一死刑一般的問題。說是這麼說,其實現在的後端開發者很多情況都不真的需要對 SQL 語法有透徹的了解也可以開始開發,而且開發效率可能還更好。這是因為大多數的後端框架現在都已經包含了所謂的 ORM 類型的函式庫來幫助大家開發。


 

 



 

 

ORM 全名為物件關聯對映(Object Relational Mapping,簡稱ORM),簡單來說是一種程式設計技術與概念。

 

在我們解釋這項技術之前,請大家先想一下,純使用 SQL 所撈取的資料對程式設計上面有什麼問題呢?


一般來說,無論你用哪種後端架構,如果要撈出所有的 Post 文章。那你可能會這樣做。  

 

 sql.exec(" select * from posts;")  // sql.exec 是虛擬碼 (或稱偽代碼 / pseudocode 可參考 WIKI)

 

因為 SQL 本質上就是資料塊,所以結果會得到一個 Array,裡面會包含所有的文章。資料會是 Array 裡面包著數個array。例如像下面這樣的內容。

 

[
  ["文章1",  文章1的內容", false], 
  ["文章2",  文章2的內容", false], 
.....如果有更多的話
]

其column 依序是 "title" 標題 , "content" 內容, "memberOnly" 會員專屬


 

如果對直接語法執行議題有興趣的話可以參考影片

利用 PHP 讀取 MySQL 資料庫並顯示出資料( mysqli )
 



這樣的內容使用上其實也不是真的那麼難用,一樣可以跑迴圈產生UI的列表。不過有時候你可以能會想要做一些程式判斷,比方說有一些文章他可能是會員才可以看的。

 

那你可能會寫出這樣的程式:
 

for(let post in posts){
   let title = post[0]
   let content = post[1]
   let memberOnly = post[2]
   if (memberOnly) {
      會員獨享...
   } else {
      禁止畫面...
   }
}


當然,你也可能不宣告 title, content, memberOnly, 直接使用 post[0] 之類的寫法,但是基本上寫多了會很難維護。


想避開 0, 1, 2 等 index 魔術數字的話的話,可以使用不要使用 select * 而改成寫出每個column,並且預設一個 Key-value object 類似下面結構:

 

let columns = {
   title: 0,
   content: 1,
   memberOnly: 2
}

這樣 index 可以改成 columns["title"] 來表示


如果程式語言包含 enum 則會更方便一些。


還有一種方式是從資料庫要 table 的 columns 

sql.exec (" select * from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='tableName' ")


這樣會給你該 table 所有的 columns 名稱,一樣是包成一個 array,再自己去對應 index。
 

 

 


 

不過這樣的寫法其實不算很好,像這樣簡單的範例都看得出來每行宣告都佔了一些空間,再加上 if-else 判斷式,複雜一點的話這個 for 迴圈會變成非常大。你可能會想說 JavaScript 有語法糖,可以節省掉一些空間和提高可讀性,但是因為這邊不是專指 NodeJS 的情況,所以我就用每種語言大概都看得懂的寫法。


像這樣子的 let title = post[0] 某種程度上就是一種 mapping,一般像這樣子的內容如果沒辦法避免的話,通常我們會想把它藏起來,這樣至少商業邏輯的程式看起來就會比較乾淨。所以大概會變成這樣:


 


class Post {

  constructor(rawData) {
     this.columns = {
         title: 0,
         content: 1,
        memberOnly: 2
    }

     this.memberOnly = rawData[ columns["memberOnly"] ]
     .... 其他變數
  }
   .... 簡化版,畢竟其他物件導向語言有封裝,不用像JavaScript 那麼麻煩
}


for(let rawData in rawDataAry){
   let post = new Post(rawData)

   if ( post.memberOnly ) {
      會員獨享...
   } else {
      禁止畫面...
   }
}



像這樣的寫法其實很常見,也可能是沒在使用 ORM 函式庫的話第一件要做的事。不過如果你有使用 ORM 函式庫的話,通常第一步就是靠 cmd 幫你建立好這樣的 class。


以 Laravel 來說,你只要確定資料庫有對應的table,剩下的就是執行

 

$ php artisan make:model Post



這樣即可,連寫 $this->title = rawData[0]; 這樣的語法都不用。因為 ORM 會從資料庫抓他有哪些 column,或是在 Migration 的時候先建立一個靜態的schema 內容列表,只要是比較動態的程式語言,其實property 都是可以動態產生的,ORM 會自動幫你完成。寫得好的 ORM 系統幾乎可以幫你把所有常見的 SQL 語法利用應用程式語法組合出來。

 

所以上面範例完整來寫的話可能會是這樣:
 

沒使用 ORM (用 js 語法示範)

1. rawDataAry =  sql.exec(" select * from posts;") 
2. class Post {
   ....
}
3. for(let rawData in rawDataAry){
   let post = new Post(rawData)

   if ( post.memberOnly ) {
      會員獨享...
   } else {
      禁止畫面...
   }
}

----------

有使用 ORM (用 PHP 語法示範,很誠實的告訴大家這是商業考量,畢竟我們有 Laravel 課程 )

1. $posts = Post::all();  // 幫你自動組出 select * from posts 並且執行。

2. foreach($posts as $post){
    if ( $post->memberOnly ) {
      會員獨享...
   } else {
      禁止畫面...
   }
}
 

 

 

或許你可能會想說好像沒差太多,但是那是因為第一個版本有自製 Model 層。
 

 

如果都是直接用 key-value object 硬做的話可能就沒那麼好看與好維護了。所以在這層意義上,用 ORM 系統就有好處,至少每個開發者在專案內都被強迫要建立 Model 層的 Class。專案內部的一致性絕對是好維護的重要依據之一。

 

(即使是 前端 或 APP 接收 API,其實我們的課程也都會教大家建立一個簡單的 Model 層做容器,而非直接 kay-value 取值硬寫,有興趣可以參加我們的 React 或 iOS 課程暸解細節。)

 



除了上面這層因素外, ORM 系統他在語法組成上通常比新手要來得有經驗的多(都是高手加上長時間修正)。新手寫 SQL 很容易碰到的問題就是 SQL injection。這個問題主要是因為 SQL 語法本身就是個字串。所以如果開發者使用字串組成的方式且沒有驗證參數,那就有可能被別人塞有問題的內容,最後組出有問題的字串導致資安問題。像這樣的問題都已經發生已久,也有固定的方式來解決。如果你不使用 ORM 的話你就要自己留意,但是如果你使用有一直在維護的 ORM 函式庫的話,他們多半會有一定程式的防護。尤其是對團隊新手有所幫助,至少比較不容易搞砸。


 

常見有問題會被 injection 的 SQL 語法

$column1 = $_POST["column1"];

sql->exec(" insert into table_name (column1) values ($column1) ");


SQL injection 是一個大議題, 所以這邊不多說,不過最簡單的方式就是先檢查傳進來的值的內容,不要預設相信任何傳入的值他是合法的。
 

 

 

 


另外還有一個更顯而易見的原因就是類似物件導向方式的使用。
 

SQL join table 在網站撈取資料時幾乎是無法避免的方式。但是一般來說,Join table 所撈取到的資料為類似 Flatten 過的資料,也就是說你幾乎不會拿到像 JSON Object 這樣巢狀的資料,而是表單型的資料。

 

 

這樣的資料其實也不是真的有問題,只是人的直覺上很難去區分說每個 column 是從哪一個欄位取得的。比方說如果你拿到的是像下面這樣的內容:

 

id name price color size brand
1 上衣 100 藍色 L 夜市牌
2 褲子 2000 黑色 M 某名牌

 

 

實際上正規化後,通常會是至少包含以下幾個 table 表


product

color

size

brand

 

對於熟悉 SQL的人當然沒問題,但是對於從應用面或是物件導向來思考的話,其實 JSON 形式的想法會比較直覺。像是下面這種表示法:

 

[
  {
    id: 1,
    name: "上衣",
    price: 100,
    color : { name: "藍色" },
    size : { name: L },
    brand : { name: "夜市牌"},
  },
  {
    id: 2,
    name: 褲子",
    price: 2000,
    color : { name: "黑色" },
    size : { name: M },
    brand : { name: "某名牌"},
  },
]

 


所以取值的時候,就會是比較像這樣

 

product.name // 上衣
product.color.name // 藍色


而不是原本的

 

product["name"] // 上衣
product["color"]  // 藍色


*以上是為了比較乾淨的表示,但實際上可能為
product [ columns["name"] ] 或是 product[0] 這種型式
 

 


如果對 product.color.name 這樣的方式不滿意,可以在 Model Color 的底下補上 color_name() 或是 read only 的 property color_name。

這樣你就可以寫成

 

product.color_name 


看起來物件導向很多,進階的使用上也可以把繼承、封裝等概念引進 Model 的 class。這些都不是單個 SQL 指令可以輕鬆達成的。

 


此外,很多時候其實並不需要一次 join 到位,越複雜的 SQL 指令大多耗時越久,如果因此鎖住部分資料表而導致後面的指令也跟者排隊,整體效能會變差。對於新手來說使用 ORM 也可以直接避免掉這個問題,大部分的情況都會是比較簡單的 SQL 撈取資料再用應用層去組合,而非讓資料庫去組合。
(不過很多情況 ORM 效能低落也是這原因造成的,所以還是要看實際狀況下去調整語法。)
 

 

 

當然很多高手會想說他們自己寫的 class 也可以達成這些目的,根本不需要用厚重的 ORM 函式庫。但這邊就比較講到維護方面的概念,一般來說這些自訂的函式庫比較輕量,有可能讓應用程式整體效能比較好。但是一般積年累月補強過後的函式庫,其實很多後來加上去的都是安全機制,雖然必要性要各別函式庫判斷,但是對於新手而言護具全穿上總比裸奔來得好。其次就是文檔的完整性與真實世界測試過的次數也有差異。一般自訂的函式庫多半不會像 open source 熱門的函式庫那麼多人使用,所以比較難各種情況都驗證過。新人不熟悉 API 的使用情境也沒文檔說明,出問題的機會相對很大。

 

 

很多時候沒必要為了很刁專的效能需求而去整個重建。還不如從既有架構上再去做細微調整,各大 ORM 函式庫也都是有留後路給需要進階使用的開發者直接執行 SQL 的管道。有需要再這樣做即可。
 


 

不過最後還是提醒一下大家,各大 ORM 函式庫都有作者自己的偏好,有一些作者的想法不見得符合你的思維,反而讓自己在寫程式的時候異常挫折。例如完全學術導向或是效率至上的 ORM 函式庫可能會禁止你做一些 "反正規化" 的動作,這點好不好就見仁見智了。強迫自己學習與使用學術上正確的寫法,或是雖然有基本規範但還是可以讓你一定程度上的自由奔放的 ORM, 哪種適合你就還是要使用過才知道。
 

 


最後,如果你喜歡我們的文章,別忘了到我們的FB粉絲團按讚喔!!

Small logo

進度條編輯群

進度條編輯團隊