2019年1月29日 星期二

MongoDB document schema 設計思維入門

本文摘要自Thinking in Documents: Part 2.

此文章將介紹MongoDB的Document schema設計思維,包含如何用embedding及referencing來整理相關資料,也會介紹一點索引及MongoDB的transaction model。

定義你自己的Document Schema

首先,最重要的是你的app查詢資料的模式,也別忘記善用document model的彈性,意即其embedding和豐富的BSON based資料結構支援。

app資料存取的模式首要在以下幾點:

1. 資料庫的讀寫比(R/W ratio)
2. 資料庫所做的查詢指令及資料更新的類型(type of queries and updates)
3. 資料的生命週期以及document的成長速度

若你有關聯式資料庫的背景,可以先想像以下兩點:

1. 你會如何使用關聯式DB來實作這些操作。
2. 在MongoDB要怎麼實作。

另外也可以透過RDBMS的日誌來分析最常使用的查詢指令或是最常被共同查詢的資料,作為是否合併在單一document下的考量。

使用Embedding或Referencing來建立關聯

直接embed或是對其他collection的document做reference的時機並無絕對,但是還是有一些準則可供參考。

Embedding

對於一對一(one-to-one)或是一對多(one-to-many)的對應關係,很適合使用embedding模式,因為這些資料很可能本身就是依存在parent document之下的一種特性或是資訊。此時parent document就像是data owner或是container一樣。可以想像當parent document消滅,若該document將會失去意義的話,就是屬於這種類型。

若是需要被一起更動(update atomically)的資料,也應該使用此模式,詳細可參照下面關於Transaction model的說明。

然而,並不是所有的一對一或是一對多對應關係都適合embedding。舉例來說,以下的時機適合使用referencing:

1. 當一個document很頻繁被讀取,但是其內的embedded document卻幾乎不會被存取到。舉例來說,一個顧客紀錄document之中的年度總報告。由於年度總報告並不會被經常使用,嵌入它只會增加該collection消耗的記憶體。
2. 某一document其中一部分很頻繁被更新且不斷成長,但是剩下的部份卻相對沒有變動。
3. document size超過MongoDB目前的 16 MB限制。

Referencing

Referencing讓資料正規化(data normalization)變得可能,也比embedding有彈性。但是MongoDB伺服器卻需要追加查詢來解析關聯,因此會需要多次讀取動作,消耗較多時間。

實作上通常是在一個document中儲存另一個document的_id field當作參照,接著再由app執行查詢來取得參照的資料。

Referencing應該被用在:

1. 當使用embedding會造成資料冗餘(data duplication),無法期待效能向上時。
2. 物件從各種不同來源被關聯時。
3. 多對多(many-to-many)關係時。
4. 大型、階層式的資料集時。

不同的設計目標

從以上觀察我們可以發現RDBMS和document model的根本上差異:

1. RDBMS對資料的處理是以儲存空間效率來考量(因為早期儲存空間是系統中成本最高的元件)。
2. MongoDB的document model則是以app存取資料的效率為考量(因為開發者的時間和上線速度現在比儲存空間還要受重視)。

MongoDB Transaction Model

MongoDB提供了如關聯式DB的document-level ACID compliance,包含可以atomically update embedded arrays以及sub-document,且無須付出如RDBMS的耗時ACID operation,以及在不同table之間維護關聯完整性(referential integrity)。

Document-level ACID compliance保證在document改動時的隔離性(isolation),任何錯誤都會讓資料還原到操作前的樣貌,且用戶端也會收到原本的document view。

NOTE:在 MongoDB 4.0之後,加入了 multi-document ACID transactions來確保replica sets之間的資料是一致的,並確保任何執行都是「有或全無(all-or-nothing)」來維護資料完整性。在4.0版本之前,您還是可以透過findandmodify或是two-phase commit來達到一樣的目的。

如何找出我的資料?

MongoDB使用B-tree indexes,並原生支援secondary indexes。因此,SQL背景的人很快就能上手。不過索引也還是一樣會在寫入及資源使用上增加負擔,如同所有的資料庫。預設MongoDB會在document的 _id欄位建立索引,其他user-defined indexes都是secondary indexes。
任何欄位都能當作secondary index,包含在arrays中的欄位。在MongoDB中的索引有:

1. Compound Indexes
2. Geospatial Indexes
3. Text Search Indexes
4. Unique Indexes
5. Array Indexes
6. TTL Indexes
7. Sparse Indexes
8. Hash Indexes

另外,MongoDB也支援index intersection以使用多個索引做查詢。

2019年1月17日 星期四

How to clone an object in Javascript with Object.defineProperties?

TL;DR

Sometimes we want to clone an object in JS, we will use something like:


const obj = {foo:'bar'};
const newobj = {};
for (const key in obj){
    newobj[key] = obj[key];
}

This will work fine under most circumstances, since you might never adjust the property flags of the object. Also, when we create an object property, theare default to true, so everything is "default normal." And you might even not know there are such flags. Property flags, in brief, are metadata for the property in the object. They define the configuration of the property such as if the property is writable/enumerable/configurable.

Here are some simple explanation about the flags:

Writable: can reassign value or not.
Enumerable: can enumerate it with for...in loop or not.
Configurable: can change the flags shown above and this flag or not.

Okay, the problem with the above cloning code is that when someone changes the flags to non-default values, and you unconsciously clone the object without knowing those property flags. In a nutshell, if you want to clone an object along with the flags, please do as following:


let newobj = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

Here we use Object.defineProperties(object, descriptors) and Object.getOwnPropertyDescriptors(object) to accomplish this. The former will define properties for the objects in the first argument ( here is an empty object literal. ) with the property descriptors in the second argument ( here is the latter function getOwnPropertyDescriptors. ). However, although the code is cleaner, this method should be employed only when you really need to clone the property descriptors since it is slower and not straight-forward enough.

For more on Property flags and descriptors: