今日代碼,可以 checkout ff5e9bb
在這次重寫博客時,設計數據庫部分將考慮反範式設計,最關鍵一點就是捨棄所有外鍵,通過代碼邏輯來保證表之間的聯繫。雖然我的博客可能沒有多大訪問量,但這個設計早已被驗證。
關於 MySQL 表結構的設計和使用上的一些建議可參見書籍《高性能 MySQL(High Performance MySQL)》以及文檔 啓發式規則建議-By Xiaomi,最基本的一點就是,設計列名之前,先搜下名稱是否在 MySQL 保留關鍵字裏(不使用文檔中標有 R 的詞,慎重使用其他關鍵詞)。
表設計構思
通用表設置
這裏設置兩個主要參數,一個是默認爲 innoDB 引擎,另一個是設置字符集爲utf8mb4
,避免一些編碼問題。
這兩個設置放置在一個字典中,作爲模型的表參數(__table_args__
)
# Detail MySQL on SQLAlchemy, ref: https://docs.sqlalchemy.org/en/13/dialects/mysql.html
COMMON_TABLE_SETTINGS = {
'mysql_charset': 'utf8mb4',
'mysql_engine': 'InnoDB'
}
通用字段和抽象類
共有 5 張表,每張表都有 自增 id (unsigned bigint)、創建時間 (create_time)、更新時間 (update_time) 三個字段。其中更新時間將會自動隨着記錄更新而更新。
雖然 SQLAlchemy 的模型類可以對列設置 onupdate=func.now()
,但根據其官方文檔以及 db.create_all()
生成的表結構來看,這只是通過客戶端生成的 SQL 來保證,表結構上的 update_time
並沒有 ON UPDATE CURRENT_TIMESTAMP
,這即是文檔上所說的 Client-Invoked SQL Expressions,只是 DML,不會生成 DDL。 server_default
nullable
這些參數就會生成 DDL。
既然是通用字段,自然而然地想到用一個基類來定義這些字段,其他模型繼承即可。SQLAlchemy 設置抽象模型只需要簡單地加上 __abstract__
,在博客中用的是最簡單的抽象方式,如果你想了解更高級的用法,可參見 Mixin and Custom Base Classes
三個通用字段的 DDL 表示:
-- 三個通用字段
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
對應的類設計:
class BaseModel(db.Model):
__abstract__ = True
__tablename__ = ''
# Configuration Document: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/table_config.html
__table_args__ = ''
id = db.Column(BIGINT(unsigned=True), primary_key=True, autoincrement=True)
create_time = db.Column(db.DateTime, server_default=func.now())
# This is a client-side update trigger.
# Will not be reflect on db.
update_time = db.Column(db.DateTime, server_default=func.now(), onupdate=func.now())
def __repr__(self):
pass
變更爲默認自動更新的語句如下, 單獨放在 setup.py
中與創建表一起執行
ALTER TABLE table_name CHANGE `update_time` `update_time` datetime
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
儘量選擇無符號整型——包括 IP 地址
博客訪問中暫時沒有想到與計算相關且有意義的負值,因此整型都設置爲無符號,擴大數值範圍(指正數),而 IP 地址實際上可以通過移位操作來進行轉化,MySQL、Python等也有內置的函數或庫來進行 IP 地址和整數的轉化。因此,在設計時,也把原先的 VARCHAR 類型改爲 unsigned int,節省存儲空間。
爲所有字段設置默認值
除了文章表 body 字段爲 TEXT 不能設置默認值外,其他表字段均設置默認值。需要注意的是,TEXT 不能設置默認值,因此也不設置 NOT NULL 限制。
邏輯上定長,物理上變長的密碼字段
密碼字段爲了安全性考慮,存儲的是哈希值,用了 VARCHAR(128),通過類的設置密碼方法使保存的密碼都是定長的哈希值。
表設計落地
用戶認證表——用於登錄
CREATE TABLE `tbl_user_auth` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`password_hash` varchar(128) NOT NULL DEFAULT '',
`email` varchar(128) NOT NULL DEFAULT '',
`username` varchar(64) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
KEY `ix_user_auth_email` (`email`),
KEY `ix_user_auth_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
用戶信息表——公開展示的信息
不使用外鍵,通過代碼邏輯來實現關聯,下同。
CREATE TABLE `tbl_user_info` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`username` varchar(64) NOT NULL DEFAULT '',
`country` varchar(8) DEFAULT '',
`city` varchar(32) DEFAULT '',
`avatar` varchar(128) DEFAULT '',
`intro` varchar(128) DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
KEY `ix_user_info_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
文章表
由於 column
是 MySQL 的關鍵字,這裏改用 section
,由於專欄與文章是一對一,因此暫不對這個字段做冗餘,只進行索引。
CREATE TABLE `tbl_article` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`username` varchar(64) NOT NULL DEFAULT '',
`title` varchar(128) NOT NULL DEFAULT '',
`body` text,
`slug` varchar(256) NOT NULL DEFAULT '',
`section` varchar(64) DEFAULT '',
PRIMARY KEY (`id`),
KEY `ix_article_title` (`title`),
KEY `ix_article_username` (`username`),
KEY `ix_article_section` (`section`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
評論表
暫時保留評論體作爲索引
CREATE TABLE `tbl_comment` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`username` varchar(64) NOT NULL DEFAULT '',
`body` varchar(256) NOT NULL DEFAULT '',
`ip` int(10) unsigned DEFAULT '0',
`country_code` varchar(8) DEFAULT '',
PRIMARY KEY (`id`),
KEY `ix_comment` (`body`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
標籤表
與文章是多對多的關係,單獨設置一張表來保存
CREATE TABLE `tbl_tag` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`tag` varchar(32) NOT NULL DEFAULT '',
`username` varchar(64) DEFAULT '',
`title` varchar(128) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `ix_tag_tag` (`tag`),
KEY `ix_tag_title` (`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
公衆號:程序員的碎碎念