# Twiker (twitter_clone) 專案實作學習筆記
## 關於
### 簡介
本專案的主要技術組合包括以 [Node.js](https://nodejs.org/en) + [Express.js](https://expressjs.com/) 作為後端伺服器,採用 [PostgreSQL](https://www.postgresql.org/) 為資料庫管理系統,並以 [node-postgres](https://node-postgres.com/) 與 [Sequelize](https://sequelize.org/) 連接 Node.js 與 PostgreSQL。
後端資料快取機制則利用 [Redis](https://redis.io/),以 [node-redis](https://github.com/redis/node-redis) 銜接 Node.js 與 Redis。
身份驗證機制採用 [JSON Web Tokens](https://jwt.io/)、 Node.js [bcrypt](https://github.com/kelektiv/node.bcrypt.js#readme) 套件對密碼進行雜湊、 [dotenv](https://github.com/motdotla/dotenv#readme) 用以作全域參數調整與隱藏敏感資訊。
### [Github 連結](https://github.com/Libright1558/twitter_clone/tree/master)
## 介紹
### 1. Database Schema (PostgreSQL)

user_table:記載使用者資訊。
* userId:用戶識別號碼。
* firstname 與 lastname:使用者姓名。
* username:使用者帳號名稱。
* email:使用者電子信箱。
* password:使用者密碼。
* profilepic:使用者大頭貼照片位置。
* createdAt:使用者的註冊日期。
* updatedAt:使用者的帳號更新日期。
</br>
post_table:使用者的發文紀錄。
* postId:文章識別號碼。
* postby:該文章作者。
* content:文章內容。
* createdAt:發文時間。
</br>
like_table:文章的按讚者。
* postId:文章識別號碼。
* username:按讚的使用者。
* createdAt:按讚時間點。
</br>
retweet_table:文章的推文者。
* postId:文章識別號碼。
* username:推文的使用者。
* createdAt:推文時間點。
</br>
pinned_table:使用者釘選的文章。
* postId:文章識別號碼。
</br>
### 2. Redis key-value pair

Hash (user info):以 Hash Table 暫存使用者資訊。以下鍵值皆以 userId 為 Field。
* firstname 與 lastname:使用者姓名。
* username:使用者帳號名。
* email:使用電子信箱資訊。
* profilepic:使用者大頭貼照片位置。
</br>
Hash (post info):以 Hash Table 暫存使用者文章。以下鍵值皆以 userId 為 Field。
* postOwner:發文作者。
* content:文章內容。
* createdAt:發文時間。
* likeNums:按讚數。
* retweetNums:推文數。
* \{ userId \}_selfLike:作者是否給自己的文章按讚。(以 "0" 與 "1" 的形式儲存,前者為否,後者為是。userId 為用戶識別號碼)
* \{ userId \}_selfRetweet:作者是否給自己的文章推文。(以 "0" 與 "1" 的形式儲存,前者為否,後者為是。userId 為用戶識別號碼)
* firstname 與 lastname:發文者姓名。
* profilepic:發文者大頭貼照片位置。
</br>
List (post info):暫存依發文時間排序後的使用者文章識別號碼,以 Redis list 方式暫存。發文時間較晚者位於 list 左方。
</br>
Set (Locks):以 set 方式暫存,暫存文章按讚時間與推文時間更新的時間點,likeNums 或 retweetNums 更新時,\{ postId \}_postLikeNums 或 \{ postId \}_postRetweetNums 位於同一個 transaction 內一起執行,並依照 timestamp 來判斷是否更新 likeNums 或 retweetNums,以此確保 likeNums 或 retweetNums 更新時,不會把時間點上較舊的資料覆寫上去。
</br>
### 3. Database 存取過程
#### [postInfo (function)](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/controller/databaseController/get.js#L40C1-L114C2)
該函式用以取得使用者的發文資訊。首先會建立 [temporary tables](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/database/queryString/queryPost.js#L1C1-L62C18) 用以暫存文章的資訊,之後再將這些 temporary tables 用 `JOIN` 進行合併,再一併將資料傳到 application server 進行處理。
以下為 temporary tables 範例:
```
const likeNum =
`CREATE TEMP TABLE IF NOT EXISTS "lN" (
"likeNum" INT,
"postId" UUID
) ON COMMIT DROP`
```
暫存文章資訊到 temporary tables:
```
const appendLikeNum =
`INSERT INTO "lN" ("postId", "likeNum")
SELECT post_table."postId", COUNT(like_table.username)
FROM post_table
LEFT JOIN like_table ON post_table."postId" = like_table."postId"
WHERE postby = $1
GROUP BY post_table."postId"`
```
將 temporary tables 用 `JOIN` 進行合併:
```
const fetchPost =
`SELECT "pOwn".postby, "pCon".content, "pT"."createdAt", post_table."postId", "likeNum", "retweetNum", "selfRetweet", "selfLike",
firstname, lastname, profilepic
FROM post_table
LEFT JOIN "lN" ON post_table."postId" = "lN"."postId"
LEFT JOIN "rN" ON post_table."postId" = "rN"."postId"
LEFT JOIN "sL" ON post_table."postId" = "sL"."postId"
LEFT JOIN "sR" ON post_table."postId" = "sR"."postId"
LEFT JOIN "pOwn" ON post_table."postId" = "pOwn"."postId"
LEFT JOIN "pCon" ON post_table."postId" = "pCon"."postId"
LEFT JOIN "pT" ON post_table."postId" = "pT"."postId"
LEFT JOIN "pFn" ON post_table."postId" = "pFn"."postId"
LEFT JOIN "pLn" ON post_table."postId" = "pLn"."postId"
LEFT JOIN "postPropic" ON post_table."postId" = "postPropic"."postId"
WHERE post_table.postby = $1`
```
該函式中,傳入的 fetchList 參數為陣列,內含值為 0 或 1,代表以下資料是否存在於 Redis 快取當中:
\[content, createdAt, likeNums, retweetNums, selfLike, selfRetweet, postOwner, firstname, lastname, profilepic\]
舉例,如果 Redis 快取當中有缺漏 likeNums 這一項資料,該陣列對應到的 likeNums 位置,其值為 0,反之為 1。 application server 只會向資料庫拿取在 Redis 快取中有缺漏的資料,以減少資料傳輸量,降低傳輸延遲。
### 4. Redis 作為快取機制
#### 為何使用 Redis ?
減少資料存取的反應時間:因為 Redis 將資料暫存在記憶體,相較於從硬碟中存取資料,能有效降低反應時間,不需每次在存取資料時都透過 database,因此可減輕 database 的負荷。
#### 存取使用者發文資訊
在 `/redis/get.js` 當中, [`getPosts`](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/redis/get.js#L13C1-L20C2) 函式會呼叫 `/redis/transaction.js` 中的 [`getPostInfo`](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/redis/transaction.js#L22C1-L54C1) 函式,將存取不同鍵值的執行用 transaction 包裹,一次性完成所有操作,並且透過 `HMGET` 一次性存取資料,避免重複請求,產生不必要的 round-trip。
[`fetchPostIdArray`](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/redis/get.js#L22C1-L29C2) 函式用途為取得已經依照發文時間排序好的 PostId 陣列,之後將該陣列作為 `getPosts` 的參數之一,所取得的發文資訊都會是排序好的。
#### 寫入使用者發文資訊
在 `/redis/write.js` 當中,[`postWriteBack`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/write.js#L12C1-L18C2) 函式會呼叫 `/redis/transaction.js` 中的 [`writePostInfo`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/transaction.js#L104C1-L144C2) 函式。listArray 參數為陣列,用以表示對應的發文資訊是否存在於 Redis 中,如無則寫入。
#### 按讚或推文
在 `/redis/update.js` 中, [`updateLikeNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/update.js#L13C1-L19C2) 與 [`updateRetweetNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/update.js#L30C1-L36C2) 分別根據使用者按讚或推文,對 redis 中的按讚數或推文數進行更新。上述兩個函式分別會呼叫 `/redis/transaction.js` 中的 [`renewLikeNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/transaction.js#L160C1-L180C2) 與 [`renewRetweetNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/transaction.js#L191C1-L211C2)函式。
renewLikeNums 與 renewRetweetNums 函式中,分別會建立 postId + 'postLikeNums' 與 postId + 'postRetweetNums' ,屬於 redis SET 底下的 key-value pair,value 儲存 LikeNums 與 RetweetNums 更新的時間點 (即更新並寫入 database 的時間點)。
更新 LikeNums 、 RetweetNums 時,會先檢查是否已經有值存在,如果存在,則會先檢查 postId + 'postLikeNums' 、 postId + 'postRetweetNums' 這兩個對應的值(時間戳記),如果已經存在值的時間戳記小於更新時的值所帶的時間戳記,將舊值以新值進行覆寫,如否則不更新,避免舊值將新值覆蓋。
使用 WATCH 觀察對應的值,client 在 transaction 內將值進行變更時,該值是否被另一個 client 變更,如已被另一個 client 變更,則 abort 整個 transaction,此即 optimistic lock,假設在 transaction 間衝突較小,因此等到 commit 之際再檢查是否有衝突發生。