前端與 SQL 全面梳理

會程式設計的銀豬發表於2017-06-12

本篇將介紹前端本地儲存裡的Web SQL和IndexedDB,通過一個案例介紹SQL的一些概念。

1. 地圖報表的案例

現在要做一個地圖報表,如下圖所示:

將所有的訂單資料做一個圖表展示,左邊的地圖展示每個city的成單情況,右邊的圖形,展示最近7天的成單情況。由於後端的資料需要前端做一些解析,如向谷歌請求每個city的經緯度,所以後端給前端原始的訂單資料,前端進行格式化和歸類展示。另外把原始資料直接放前端,前端處理起來可以比較靈活,想怎麼展示就怎麼展示,不用每次展示方式變的時候都需要找後端新加介面。

但是資料放在前端管理,相應地就會引入一個問題——如何高效地儲存和使用這些資料。最起碼處理起來不要讓頁面卡了。

2. cookie和localStorage

cookie的資料量比較小,瀏覽器限制最大隻能為4k,而localStorage和sessionStorage適合於小資料量的儲存,firefox和Chrome限制最大儲存為5Mb,如下火狐的config:

localStorage是存放在一個本地檔案裡面,在筆者的Mac上是放在:

/Users/yincheng/Library/Application Support/Google/Chrome/Default/Local Storage/ http_www.test.com.localstorage

文字編輯器開啟這個二進位制檔案,可以看到本地儲存的內容:

可以參照控制檯的輸出:

如果一個網站要用掉5Mb硬碟空間,那麼開啟過一百個網頁就得花500Mb的空間,所以本地儲存localStorage的空間限制得比較小。

另外,可以看到localStorage是以字串的方式儲存的,存之前要先JSON.stringify變成字串,取的時候需要用JSON.parse恢復成相應的格式。localStorage適合於比較簡單的資料存放和管理。

3. 管理複雜資料

後端給我這樣的JSON資料:

[

{“orderId”:100314,”userId”:379558604617762,”city”:”ca”,”state”:”ca”,”zipcode”:”91000″,”address”:”11″,”price”:2698.00,”createTime”:1477651308000},

{“orderId”:100821,”userId”:514694887070560,”city”:”San Francisco”,”state”:”CA”,”zipcode”:”94103″,”address”:”251 Rhode Island St #105″,”price”:2182.00,”createTime”:1481104358000}

]

我用這些資料去請求它們的經緯度。

這些資料的量比較大,有成百上千甚至幾萬條資料,資料需要複雜的查詢,需要支援:

  1. 訂單按日期分類和排序
  2. 訂單按照city分類

如果自己管理JSON資料就會比較麻煩,所以這裡嘗試使用Web SQL來管理這些資料。

4. Web SQL

(1)什麼是SQL

SQL作用在關係型資料庫上面,什麼是關係型資料庫?關係型資料庫是由一張張的二維表組成的,如下圖所示:

那什麼是SQL呢?SQL是一種操作關係型DB的語言,支援建立表,插入表,修改和刪除等等,還提供非常強大的查詢功能。

常見的關係型資料庫廠商有MySQL、SQLite、SQL Server、Oracle,由於MySQL是免費的,所以企業一般用MySQL的居多。

Web SQL是前端的資料庫,它也是本地儲存的一種,使用SQLite實現,SQLite是一種輕量級資料庫,它佔的空間小,支援建立表,插入、修改、刪除表格資料,但是不支援修改表結構,如刪掉一縱列,修改表頭欄位名等。但是可以把整張表刪了。同一個域可以建立多個DB,每個DB有若干張表,如下圖示意:

(2)建立一個DB

如下程式碼所示:

使用openDatabase,傳4個引數,指定資料庫大小,如果指定太大,瀏覽器會提示使用者是否允許使用這麼多空間,如Safari的提示:

如果不允許,瀏覽器將會拋異常:

QuotaExceededError (DOM Exception 22): The quota has been exceeded.

這樣就建立了一個資料庫叫order_test,返回了一個db物件,使用這個db物件建立一張表

(3)建立表

如下程式碼所示:

db.transaction(function(tx){
    tx.executeSql(
"create table if not exists order_data(order_id primary key, format_city, lat, lng, price, create_time)", [], null,  
function(tx, err){
        throw(`execute sql failed: ${err.code} ${err.message}`);
    });
});

傳一個回撥給db.transaction,它會傳一個SQLTransaction的例項,它表示一個事務,然後調executeSql函式,傳四個引數,第一個引數為要執行的SQL語句,第二個引數為選項,第三個為成功回撥函式,第四個為失敗回撥函式,這裡我們拋一個異常,列印失敗的描述。我們執行的SQL語句為:

create table if not exists order_data(order_id primary key, format_city, lat, lng, price, create_time)

意思是建立一張order_data表,它的欄位有6個,第一個order_id為主鍵,主鍵用來標誌這一列,並且不允許有重複的值。

現在往這張表插入資料。

(4)插入資料

準備好原始資料和對資料做一些處理,如下所示:

var order = { 
    orderId: 100314, format_city: "New York, NY, USA", 
    lat: 40.7127837, lng: -74.0059413,
    price: 150, createTime: 1473884040000
};
//把時間戳轉成年月日2017-06-08型別的
var date = dataProcess.getDateStr(order.createTime);

然後執行插入:

tx.executeSql(`
insert into order_data 
values(${order.orderId}, '${order.format_city}', 
          ${order.lat}, ${order.lng}, ${order.price}, '${date}')`);

就可以在瀏覽器控制檯看到剛剛建立的資料庫、表,如下圖所示:

如果把剛剛的那條資料再插入一遍會怎麼樣呢?如重新整理一下頁面,它又重新執行。

(5)主鍵唯一約束

插入一個重複主鍵,這裡為id,executeSql的失敗函式將會執行,如下所示:

所以一般id是自動生成的,mysql可以指定某個整數字段為auto_increment,而web sql對整數字段不指定也是auto_increment,需要在建立的時候指定當前欄位為integer,如下語句:

create table student(id integer primary key auto_increment, age, score);

作用是建立一張student表,它的id是自動自增的,執行insert插入時會自動生成一個id:

insert into student(grade, score) values(5, 88);

這樣插入幾次,得到如下表:

可以看到id由1開始自動增長。經常利用這種自增功能生成使用者的id、訂單的id等等。

上面指定了id為整型,就不能插入一個字串的資料,否則會報錯。而如果沒指定,可以插入數字也可以插入字串,當然同一欄位最好型別要一致。如mysql、SQL Server等資料庫都是強型別的。

這裡有一個細節需要注意,後端的mysql的id一般採用64位的長整型,這個數最大值為一個19位數:

9223372036854775807

而JS的最大整數為一個16位數,大於這個數的值將會是不可靠的,如下圖所示:

因此如果發生這種情況的話,需要讓後端把ID當作字串的方式傳給你。這個我在《為什麼0.1 + 0.2不等於0.3?》這篇文章裡面做過討論。

(6)全部的資料

把所有的資料都插入之後,得到如下表:

然後我們開始做查詢。

(7)Select查詢

a)查出每個城市的單數和,按日期升序。便於地圖按city展示,可以執行以下SQL:

     select format_city as city, count(order_id) as ‘count’, sum(price) as amount from order_data group by format_city order by date

結果如下圖所示:

b)然後再查一下最近7天每一天的單數,用於右邊柱狀圖的展示,執行以下SQL:

select date, count(order_id) as ‘count’, sum(price) as amount from order_data group by date order by date desc limit 0, 7

得到:

c)查詢某個orderId是否存在,因為資料需要動態更新,例如每兩個小時更新一次,如果有新資料需要去查詢格式化的地址以及經緯度。而每次請求都是拉取全部資料,因此需要找出哪些是新資料。可以執行:

select order_id from order_data where order_id = ${order.orderId}

IndexedDB是本地儲存的第三種方式,它是非關係型資料庫。它的建立資料庫、建表、插入資料等操作如下程式碼如下,這裡不進行拆分講解,具體API細節讀者可查MDN等相關文件。

//建立和開啟一個資料庫
var request = window.indexedDB.open("orders", 7);
var db = null;
request.onsuccess = function(event){
    db = event.target.result;
    //如果order_data表已經存在,則直接插入資料
    if(db.objectStoreNames.contains("order_data")){
        var orderStore = db.transaction("order_data", "readwrite").objectStore("order_data");
        //insertOrders(orderStore);
    }

};

request.onupgradeneeded = function(event){
    db = event.target.result;
    //如果order_data表不存在則建立,並插入資料
    if(!db.objectStoreNames.contains("order_data")){
        var orderStore = db.createObjectStore("order_data", {keyPath: "orderId"});
        insertOrders(orderStore);
    }
};

function insertOrders(orderStore){
    var orders = orderData.data;
    for(var i = 0; i < orders.length; i++){
        orderStore.add(orders[i]);          //add是一個非同步的操作,返回一個IDBRequest,有onsucess
    }
}

執行完之後就有了一張order_data的表,如下所示:

現在要查詢某個orderId的資料,可執行以下程式碼:

function query(orderId){
    db.transaction("order_data", "readonly") //IDBTransaction
      .objectStore("order_data")             //IDBObjectStore
      .get(orderId)                          //IDBRequest
      .onsuccess = function(event){
          var order = event.target.result;
          console.log(order)
    };
}

結果如下圖所示:

怎麼查詢value欄位裡面的資料呢?如要查詢state為CA的訂單,那麼給state這個欄位新增一個索引就可以查詢 了,如下所示:

這裡就可以知道,為什麼要叫IndexedDB或者索引資料庫了,因為它主要是通過建立索引進行查詢的。

上面只返回了一個結果,但是一般需要獲取全部的結果,就得使用遊標cursor,如下程式碼所示:

列印結果如下:

IndexedDB還支援插入json格式不一樣的資料,如下程式碼:

var specilaData = {
    orderId: 'hello, world',
    text: "goodbye, world"
};

var orderStore = db.transaction("order_data", "readwrite").objectStore("order_data");
orderStore.add(specilaData).onsuccess = function(event){
    orderStore.get('hello, world').onsuccess = function(event){
        console.log(event.target.result);
    };
};

結果如下圖所示:

(2)非關係型資料庫的橫向擴充套件

上面說關係型資料庫不利於橫向擴充套件,而在一般的非關係型資料庫裡面,每個資料儲存的型別都可以不一樣,即每個key對應的value的json欄位格式可以不一致,所以不存在新增欄位的問題,而相同型別的欄位可以建立索引,提高查詢效率。

NoSQL做不了複雜查詢,如上面的案例要按照日期/city歸類的話,需要自己開啟一個遊標迴圈做處理。所以我選擇用Web SQL主要是這個原因。

(3)相容性

WebSQL相容性如下caniuse所示:

主要是IE和火狐不支援,而IndexedDB的相容性會好很多:

8. 資料庫與Promise

資料庫的查詢,新增等都是非同步操作,有時候你可能需要先發個請求獲取資料,然後插入資料,重複N次之後,再查詢資料。例如我需要先一條條地向谷歌伺服器解析地址,再插入資料庫,然後再做查詢。在查詢資料之前需要保證資料已經都全部寫到資料庫裡面了,可以用Promise解決,在保證效率的同時達到目的。如下程式碼所示:

9. SQL隱碼攻擊

談SQL一般會離不開SQL隱碼攻擊的話題,什麼是SQL隱碼攻擊呢?

假設有個表單,支援使用者查詢自己在某個地方的訂單,如下圖所示:

所寫的SQL語句是這樣的:

     select * from order_data where user_id = 514694887070560 and state = ‘${userData.state}’

userId根據使用者的登陸資訊可以知道,而state則使用使用者傳來的資料,那麼就變成了一道填(song)空(ming)題,如下圖所示:

正常的查詢如下圖所示:

現在進行指令碼注入,如我要查一下所有使用者的訂單情況,如下所示:

     select * from order_data where user_id = 514694887070560 and state = ‘CA’ union select * from order_data where ”=’‘;

加粗的字就是我在空格里面填入的東西,它就會拼成一句合法的SQL語句——查詢order_data表的所有資料,結果如下:

由於資料庫是放在遠端伺服器,我怎麼知道你這張表叫做order_data呢?這就需要猜,根據一般的命名習慣,如果order_data不對,那麼對方服務將會返回出錯,那就再換一個,如order/orders等,不斷地猜,一般可以在較少次數內猜中。

我還猜測有張使用者表,存放著使用者的密碼,要查一下某個人的密碼,執行以下SQL語句:

select * from order_data where user_id = 514694887070560 and state = ‘CA’ union select order_id, order_data.user_id, price, address, user.password as city, zipcode, state, format_city, date, lat, lng from order_data join user on user.user_id = order_data.user_id and ”=”;

結果如下:

第二個city就是那個使用者的密碼,如果資料庫是明文儲存密碼,那就更便利了。

還可以再做一些增刪改的操作,這個就比查詢其它使用者資訊更危險了。那怎麼防止SQL隱碼攻擊呢?

如果欄位型別是數字,則沒有注入的風險,而如果欄位是字串則存在。需要把字串裡面的引號進行轉義把它變成查詢的內容,在引號裡面是使用連在一起的兩個引號表示一個引號。

更常見的是底層框架先把sql語句編譯好,傳進來的字串只能做為內容查詢,這種通常是最安全的,就是有時候不太靈活,特別是查詢條件比較多樣時,如果一個條件就寫一句sql還是挺煩的,並且條件還可以組合。

10. 分散式資料庫

如果網站日訪問量太大,一個資料庫服務很可能會扛不住,需要搞幾臺相同的資料庫伺服器分擔壓力,但是要保證這幾個資料庫資料一致性。這個有很多解決方案,最簡單的如mysql的replication:

假設線上有3個資料庫,使用者的一個操作寫到了其中的一個資料庫裡面,這個庫就叫主庫master,其它兩個庫叫從庫slave,主庫會把新資料遠端複製到另外兩個從庫。

11. 資料庫備份

談到資料庫離不開另外一個話題——備份,備份很重要,假設你的網站某一天被攻擊了,一夜之間幾十萬個使用者的資料沒了,要是找不回來,或者寫了十年的部落格全沒了,就真的得一夜白頭了。例如筆者會不對期地對自己的部落格網站做備份:

用wordpress和db的備份檔案,可以在一個小時之內從0恢復整個部落格網站。

備份mysql資料庫可以執行mysqldump的命令,以root使用者的身份:

mysqldump order > order.bak.mysql –u root –p

就可以把order這個資料庫備份起來,恢復的時候只需執行:

mysql -u root -p < order.bak.mysql

就可以把order這個資料庫導進來。

綜合以上,本文談到了本地儲存的三種方式:

  1. localStorage/sessionStorage
  2. Web SQL
  3. IndexedDB

並比較了它們的特點。還談了下DB結合Promise做一些操作和SQL隱碼攻擊等。

最主要是分析了關係型資料庫和非關係型資料庫的特點,關係型資料庫是一名老將,而非關係型隨著大資料的產生應運而生,但它又不侷限於在大資料上使用。html5也增加了這兩種型別的資料庫,為做Web Application做好準備。雖然Web SQL很早前被deprecated,但是隻要你不用支援IE和Firefox還是可以用的,它的好處是查詢比較方便,而IndexedDB儲存比較靈活,查詢不方便。說不定在不久的將來會有一種全新的web關係型資料庫出現。現在很多網站都使用IndexedDB儲存它們的資料。

所以可以兩者嘗試學習和使用一下,一方面為做那種資料驅動型別的網頁提供便利,另一方面可以對資料庫的概念有所瞭解,知道後端是如何建表如何查詢資料返回給你的。

相關文章