據(jù)到連接真實(shí)數(shù)據(jù)庫:SQLite到MySQL的入門實(shí)操指南)
1. 為什么想聊這個(gè)話題一千個(gè)“寫死數(shù)據(jù)”的項(xiàng)目1.1 我見到太多“寫死數(shù)據(jù)”的現(xiàn)場(chǎng)真的替他們著急先說說我最近的一次經(jīng)歷有個(gè)做運(yùn)營(yíng)平臺(tái)的朋友拿項(xiàng)目給我看說他們小團(tuán)隊(duì)費(fèi)了好大勁做了一個(gè)“客戶管理頁面”前端界面、登錄判斷、角色區(qū)分全都有了結(jié)果一點(diǎn)開用戶列表——三個(gè)測(cè)試賬號(hào)、兩行假記錄所有數(shù)據(jù)都直接寫死在代碼里。改一個(gè)手機(jī)號(hào)得先找到那段數(shù)組改完還要重新打包發(fā)布。我當(dāng)時(shí)就問他你沒有接數(shù)據(jù)庫嗎他愣了一下說“數(shù)據(jù)庫不是得學(xué)很多東西嗎我們幾個(gè)都是文科生轉(zhuǎn)過來的能把這個(gè)頁面跑起來已經(jīng)不容易了?!边@句話我太熟悉了。這些年我看過太多類似的項(xiàng)目寫死用戶、寫死訂單、寫死商品列表甚至有的連“今日新聞”都是早上手動(dòng)替換的。不能說他們不認(rèn)真只能說大家都把“連數(shù)據(jù)庫”這件事想得太重總覺得那是資深后端工程師才能碰的領(lǐng)域。但事實(shí)完全不是這樣。今天這個(gè)環(huán)境普通開發(fā)者、非科班轉(zhuǎn)行的人要邁過“從寫死數(shù)據(jù)到連接真實(shí)數(shù)據(jù)庫”這一步門檻已經(jīng)低到了非常友好的程度。甚至可以說一旦你掌握了這個(gè)切換你手里的項(xiàng)目就從“演示品”變成“真正能用起來的東西”。這不只是技術(shù)升級(jí)是整個(gè)項(xiàng)目性質(zhì)的改變。1.2 “寫死數(shù)據(jù)”到底死在哪我不反對(duì)在最早的原型階段用寫死數(shù)據(jù)。當(dāng)年我自己做Demo的時(shí)候也這么干畢竟最快。但你要搞清楚它的問題是什么改動(dòng)一次數(shù)據(jù)就要改代碼改完還要重新構(gòu)建、重新部署。你早上改了個(gè)價(jià)格客戶下午看到的還是舊版你說這難受不難受。多個(gè)人一起用的時(shí)候各看各的版本。每個(gè)人都有一份自己的“假數(shù)據(jù)”誰都不知道哪個(gè)是準(zhǔn)的。沒有歷史記錄。數(shù)據(jù)被覆蓋就被覆蓋了回頭找都沒地方找。數(shù)據(jù)量一大前端頁面直接卡死。你試試把一萬條訂單寫死在JavaScript里瀏覽器直接吃內(nèi)存。寫死數(shù)據(jù)的本質(zhì)是什么是把“數(shù)據(jù)”和“程序”捆死在了一起。真正靠譜的架構(gòu)數(shù)據(jù)應(yīng)該獨(dú)立于程序存活程序停了數(shù)據(jù)還在版本升級(jí)了數(shù)據(jù)還能遷移換一個(gè)終端數(shù)據(jù)依然能查。這才叫“數(shù)據(jù)資產(chǎn)”寫死在代碼里的那些只能叫“臨時(shí)變量”。2. 數(shù)據(jù)庫到底解決了什么給數(shù)據(jù)一個(gè)“正規(guī)編制”2.1 千萬別把數(shù)據(jù)庫想成高深的技術(shù)它就是個(gè)“公共儲(chǔ)物柜”很多人一聽“數(shù)據(jù)庫”三個(gè)字就自動(dòng)聯(lián)想到高薪DBA、復(fù)雜SQL調(diào)優(yōu)、分布式架構(gòu)其實(shí)對(duì)絕大部分項(xiàng)目來說數(shù)據(jù)庫干的事情非常簡(jiǎn)單就是提供一個(gè)統(tǒng)一的地方讓你把數(shù)據(jù)放進(jìn)去、拿出來、改掉、刪掉并且讓別人也能跟你同步操作。我經(jīng)常用一個(gè)比喻寫死數(shù)據(jù)就像你自己兜里揣著所有物品別人要用你得掏出來遞給他數(shù)據(jù)庫就像一個(gè)小區(qū)里的公共儲(chǔ)物柜人人都有鑰匙你放進(jìn)去別人按編號(hào)取誰也不會(huì)拿錯(cuò)里面還有登記冊(cè)記錄誰動(dòng)過。這個(gè)“儲(chǔ)物柜”解決了一個(gè)很關(guān)鍵的問題程序可以有多個(gè)實(shí)例服務(wù)可以重啟但是數(shù)據(jù)是持久的、共享的、一致的。而這恰恰是企業(yè)級(jí)應(yīng)用和“作業(yè)級(jí)Demo”最本質(zhì)的區(qū)別。2.2 關(guān)系數(shù)據(jù)庫最核心的三個(gè)概念表、行、列以及它們的主鍵連接真實(shí)數(shù)據(jù)庫之前花十分鐘搞懂三個(gè)概念就夠了。表Table就是一類東西的集合比如用戶表、訂單表。行Row表里面的一條具體記錄比如某個(gè)具體用戶張三。列Column一條記錄的一個(gè)屬性比如用戶名、手機(jī)號(hào)、注冊(cè)時(shí)間。主鍵每行數(shù)據(jù)唯一的身份標(biāo)識(shí)。就好比身份證號(hào)一張表里不能有兩行完全相同的主鍵。搞懂這些之后再看SQL語句就一點(diǎn)都不嚇人了。查詢數(shù)據(jù)的語句核心就一句SELECT * FROM users WHERE name 張三;意思就是從“用戶表”里把“姓名”這一列等于“張三”的所有行都拿出來。所有復(fù)雜的查詢本質(zhì)上都是在“哪張表”“取哪些列”“什么條件”這三個(gè)維度上做組合。2.3 什么時(shí)候該下定決心從寫死切到真庫我總結(jié)了一個(gè)“觸發(fā)信號(hào)”滿足任何一條你就別猶豫了數(shù)據(jù)要由多個(gè)用戶、多個(gè)入口維護(hù)和查看數(shù)據(jù)量會(huì)持續(xù)增長(zhǎng)比如記錄每天都會(huì)新增同一個(gè)數(shù)據(jù)需要被多個(gè)功能模塊復(fù)用上線之后需要有人在不碰代碼的情況下修改內(nèi)容。只要你的項(xiàng)目碰了上面任意一條“寫死數(shù)據(jù)”就撐不住了。反過來講如果你只是做一個(gè)單機(jī)的小玩具自己演示完就丟那繼續(xù)寫死也沒問題。工具不丟人選錯(cuò)工具才麻煩。3. 動(dòng)手實(shí)操把寫死的數(shù)據(jù)搬到真實(shí)數(shù)據(jù)庫3.1 第一步先選一個(gè)門檻最低、又足夠真實(shí)用的數(shù)據(jù)庫數(shù)據(jù)庫選型這件事很多人一上來就被Oracle、SQL Server那些企業(yè)級(jí)名詞勸退。其實(shí)我的建議非常明確先從 SQLite 開始只要你跑通流程再切到 MySQL 或者 PostgreSQL 會(huì)非常順。SQLite 強(qiáng)在哪它是一個(gè)單文件數(shù)據(jù)庫不需要安裝服務(wù)端、不需要配置端口、不需要賬號(hào)密碼就是一個(gè)“.db”文件。你的程序讀寫這個(gè)文件數(shù)據(jù)就持久化了。對(duì)于學(xué)習(xí)“連接數(shù)據(jù)庫”這件事來說它把所有的外部噪音都剔除了。不過如果你要做的項(xiàng)目將來要真實(shí)部署給別人用、有并發(fā)訪問我更推薦直接上 PostgreSQL 或者 MySQL。這倆的區(qū)別對(duì)新手來說沒那么重要你只需要知道PostgreSQL 功能更豐富一些對(duì)復(fù)雜查詢和事務(wù)支持更嚴(yán)謹(jǐn)MySQL 使用人群廣教程多排障容易。我的習(xí)慣是個(gè)人項(xiàng)目用 PostgreSQL團(tuán)隊(duì)協(xié)作時(shí)看大家更熟哪個(gè)。以MySQL為例建庫建表的常用命令你可以直接抄CREATE DATABASE shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE shop; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );注意這里的utf8mb4和utf8mb4_unicode_ci這是給中文數(shù)據(jù)用的能避免絕大多數(shù)亂碼問題。后面我會(huì)專門再講這個(gè)坑。3.2 第二步寫一條穩(wěn)定的“連接通道”而不是每次現(xiàn)連連接數(shù)據(jù)庫最忌諱什么就是每要執(zhí)行一次查詢就現(xiàn)寫一段連接代碼、用完就斷開。連接本身是有開銷的頻繁開關(guān)連接在高并發(fā)場(chǎng)景下會(huì)把數(shù)據(jù)庫拖死。正確姿勢(shì)是把這個(gè)連接過程封裝成一個(gè)固定的“通道層”。我用Python舉例因?yàn)榉强瓢噢D(zhuǎn)行的朋友大多會(huì)選Python語法沒有壓迫感。import pymysql from dbutils.pooled_db import PooledDB DB_CONFIG { host: 127.0.0.1, port: 3306, user: shop_user, password: your_password, database: shop, charset: utf8mb4 } pool PooledDB(creatorpymysql, maxconnections10, **DB_CONFIG) def get_conn(): return pool.connection()用連接池之后每次要操作數(shù)據(jù)就調(diào)用get_conn()借一條連接用完歸還。這樣既不會(huì)把連接用完不還也不會(huì)每次都重復(fù)握手。學(xué)名叫做“數(shù)據(jù)庫連接池”聽起來很高級(jí)本質(zhì)就是一個(gè)幫你管理連接的工具箱。3.3 第三步把增刪改查封裝成服務(wù)業(yè)務(wù)代碼里別裸奔SQL很多新手容易犯的第二個(gè)錯(cuò)是在頁面邏輯里直接拼SQL字符串。這樣不但代碼四處重復(fù)更危險(xiǎn)的是容易出安全問題。你應(yīng)該把對(duì)每張表的操作收斂到一個(gè)文件里起個(gè)名字叫“數(shù)據(jù)訪問層”。比如用戶模塊你就封裝四個(gè)函數(shù)def get_user(user_id): ... def create_user(name, phone): ... def update_user(user_id, name, phone): ... def delete_user(user_id): ...業(yè)務(wù)代碼里只調(diào)用這些函數(shù)不再關(guān)心SQL長(zhǎng)什么樣。這不是什么高深的架構(gòu)思想純粹是為了讓你的代碼好維護(hù)、好測(cè)試、好替換。數(shù)據(jù)庫同步軟件、數(shù)據(jù)庫導(dǎo)出腳本這些工具將來要接進(jìn)來的時(shí)候也都以這個(gè)層為邊界。另一個(gè)很重要的點(diǎn)所有涉及用戶輸入的地方絕對(duì)不要用拼接字符串的方式寫SQL。要使用參數(shù)化查詢。你只要記住一個(gè)鐵律外部來的內(nèi)容永遠(yuǎn)只作為一個(gè)“值”傳進(jìn)去永遠(yuǎn)不要變成SQL語句的一部分。這是你項(xiàng)目上線之后不被搞垮的底線之一。4. 連接真實(shí)數(shù)據(jù)庫后的關(guān)鍵細(xì)節(jié)從能跑到能用的距離4.1 事務(wù)要么全成要么全不成企業(yè)級(jí)項(xiàng)目和Demo之間有一道分水嶺叫“事務(wù)”。舉個(gè)例子一個(gè)下單流程扣庫存、減余額、生成訂單、記錄日志。如果其中一步失敗了前面已經(jīng)執(zhí)行的操作必須全部回滾否則就會(huì)出現(xiàn)“錢扣了但訂單沒創(chuàng)建”的臟數(shù)據(jù)。SQLite、MySQL、PostgreSQL都支持事務(wù)。核心語法就是三條START TRANSACTION; UPDATE ...; INSERT ...; COMMIT;當(dāng)遇到異常時(shí)執(zhí)行ROLLBACK;就回到事務(wù)開始前的狀態(tài)。你在封裝數(shù)據(jù)訪問函數(shù)時(shí)要把多個(gè)操作放進(jìn)同一個(gè)事務(wù)里管理而不是每個(gè)SQL各自獨(dú)立提交。這個(gè)習(xí)慣是你未來寫任何帶金錢、庫存、狀態(tài)的業(yè)務(wù)都繞不開的。事務(wù)之外還要理解“并發(fā)”這個(gè)詞。你項(xiàng)目上線后同一時(shí)間可能有成百上千個(gè)人操作。兩個(gè)用戶同時(shí)搶最后一個(gè)庫存數(shù)據(jù)庫怎么保證不超賣這就靠事務(wù)隔離級(jí)別和行鎖來處理。對(duì)剛?cè)腴T的人來說你不需要一開始就精通但至少要知道MySQL默認(rèn)的REPEATABLE READ隔離級(jí)別在絕大多數(shù)業(yè)務(wù)場(chǎng)景下是安全可用的別沒事去“優(yōu)化”成其他級(jí)別。4.2 權(quán)限管理別用root賬號(hào)跑業(yè)務(wù)該建就建專號(hào)這個(gè)坑我見過太多次開發(fā)的時(shí)候圖省事全流程都用root或者管理員賬號(hào)然后項(xiàng)目要上線了用戶名密碼還寫死在配置里。一旦泄露別人就能把你的用戶表刪得干干凈凈。正確做法是為每個(gè)項(xiàng)目創(chuàng)建專用賬號(hào)并授予最小權(quán)限。還是以MySQL為例CREATE USER shop_user% IDENTIFIED BY a_strong_password; GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO shop_user%; FLUSH PRIVILEGES;你就給增刪改查四個(gè)權(quán)限就夠了絕對(duì)不要給DROP、ALTER這類危險(xiǎn)權(quán)限給業(yè)務(wù)賬號(hào)。萬一將來真出了安全事故損失的也是一部分?jǐn)?shù)據(jù)而不是整個(gè)庫。誰會(huì)負(fù)責(zé)修復(fù)這些說實(shí)話并不重要重要的是你自己別把大門鑰匙交出去。4.3 千萬別忽視備份數(shù)據(jù)是你項(xiàng)目唯一的“不動(dòng)產(chǎn)”代碼丟失了可以從Git倉庫拉回來數(shù)據(jù)丟了就真沒了。企業(yè)級(jí)項(xiàng)目意識(shí)里備份不是可選項(xiàng)是必需項(xiàng)。我建議至少做到兩條每天定時(shí)自動(dòng)備份一次備份文件保留至少7天每次做批量更新、刪除、表結(jié)構(gòu)變更之前手動(dòng)手動(dòng)備份一次。MySQL的備份命令很簡(jiǎn)單mysqldump -u shop_user -p shop backup_$(date %F).sql恢復(fù)也不復(fù)雜mysql -u shop_user -p shop backup_2025-01-01.sqlPostgreSQL對(duì)應(yīng)的是pg_dump和psql用法差不多。這件事前期花的心思極少但真到出問題時(shí)你會(huì)覺得它是你做過最值的一個(gè)決定。5. 文科生/非科班最容易栽的五個(gè)跟頭5.1 表結(jié)構(gòu)設(shè)計(jì)時(shí)的“拍腦袋”陷阱非科班同學(xué)做表結(jié)構(gòu)最常見的問題就是把Excel的習(xí)慣搬到數(shù)據(jù)庫。Excel里你可以一張表放所有信息數(shù)據(jù)庫不行或者說最好不要。比如你要做訂單系統(tǒng)列里塞“客戶姓名、客戶電話、客戶地址”看起來方便但從第二筆訂單開始你就會(huì)發(fā)現(xiàn)同一客戶在不同訂單里重復(fù)填了三遍改一個(gè)地址要改好幾行。這叫做“數(shù)據(jù)冗余”。解決辦法是拆表客戶一張表訂單一張表訂單里只放“客戶ID”。想要查訂單對(duì)應(yīng)的客戶信息時(shí)用關(guān)聯(lián)查詢把兩張表連起來。這個(gè)過程叫“規(guī)范化設(shè)計(jì)”說人話就是數(shù)據(jù)盡量只存一處其他地方只存它的ID。一開始別追求高深的第三范式你就記住“同一件事不重復(fù)存”這一條原則就已經(jīng)能避開一大半設(shè)計(jì)錯(cuò)誤。5.2 改數(shù)據(jù)前的備份與“先查后改”習(xí)慣我見過一個(gè)讓人哭笑不得的事故有人寫了個(gè)更新語句忘了加WHERE條件結(jié)果把全表所有訂單的金額都改成了一個(gè)數(shù)。這種事資深工程師也會(huì)遇到但老手和非新手的區(qū)別在于——敢不敢在沒備份的情況下執(zhí)行。所以給你一個(gè)我用了很多年的鐵律任何修改類語句先寫成SELECT跑一遍確認(rèn)影響的行數(shù)是你預(yù)期的再把它改寫成UPDATE執(zhí)行。執(zhí)行前保留一句備份哪怕就是導(dǎo)出一份CSV也好。這多出來的兩分鐘能救你無數(shù)次。5.3 中文亂碼的罪魁禍?zhǔn)鬃址灰恢隆盀槭裁次掖孢M(jìn)去的是中文查出來是問號(hào)”這個(gè)問題我回答了無數(shù)次。絕大多數(shù)原因是創(chuàng)建數(shù)據(jù)庫時(shí)用了默認(rèn)字符集通常在Linux環(huán)境下是latin1或utf8但有些老版本連utf8都不完整而程序連接時(shí)用的是utf8mb4。你只要從建庫開始就統(tǒng)一三處字符集就不會(huì)有亂碼問題數(shù)據(jù)庫本身CREATE DATABASE ... CHARACTER SET utf8mb4表本身CREATE TABLE ... DEFAULT CHARSETutf8mb4連接參數(shù)charsetutf8mb4三處一致中文隨便存。另外老項(xiàng)目要是已經(jīng)變成亂碼了不要指望“改個(gè)配置”能讓舊數(shù)據(jù)自己恢復(fù)亂碼往往是不可逆的你只能修復(fù)新數(shù)據(jù)舊數(shù)據(jù)得靠備份恢復(fù)。5.4 索引與慢查詢數(shù)據(jù)多了以后你們家系統(tǒng)變慢的原因項(xiàng)目剛上線數(shù)據(jù)幾十條查詢飛快。半年后數(shù)據(jù)量上萬、上十萬頁面開始轉(zhuǎn)圈SQL開始慢。這種情況90%是因?yàn)樵摷铀饕牡胤經(jīng)]加。索引是什么你可以把它理解成書后面的目錄。沒有目錄你要找“賣冰箱的訂單”只能從第一頁翻到最后一頁這叫做“全表掃描”。加了索引數(shù)據(jù)庫直接跳到對(duì)應(yīng)的那一塊區(qū)域速度自然快。常見的加索引場(chǎng)景是經(jīng)常出現(xiàn)在WHERE后面的字段、經(jīng)常用于排序的字段、以及外鍵字段。例如上面說的訂單表里customer_id就應(yīng)該加索引ALTER TABLE orders ADD INDEX idx_customer_id (customer_id);有一點(diǎn)要提醒你索引不是越多越好。每加一個(gè)索引寫入數(shù)據(jù)時(shí)都要額外維護(hù)一份“目錄”反而拖慢插入速度。只給真正查詢用的關(guān)鍵字段加索引就夠了。5.5 連接泄漏程序一會(huì)兒正常一會(huì)兒卡死八成是這個(gè)原因還有一種很常見的詭異故障程序剛啟動(dòng)時(shí)一切正常跑一段時(shí)間后突然變慢甚至報(bào)“too many connections”。重啟之后又好了過一會(huì)兒又犯病。這通常不是數(shù)據(jù)庫的問題而是你代碼里拿連接后沒歸還。如果你用的是連接池每次用完一定要把連接放回池里。Python里用with語句可以保證這一點(diǎn)with get_conn() as conn: cursor conn.cursor() cursor.execute(SELECT * FROM users)這樣無論中途有沒有報(bào)錯(cuò)連接都會(huì)自動(dòng)歸還。一個(gè)小細(xì)節(jié)能讓你的項(xiàng)目穩(wěn)定性和專業(yè)感提升一個(gè)檔次。6. 我的真實(shí)體會(huì)連接真庫不是技術(shù)升級(jí)是思維升級(jí)做這行越久我越覺得從“寫死數(shù)據(jù)”到“連接真實(shí)數(shù)據(jù)庫”最難的從來不是SQL語法、不是安裝數(shù)據(jù)庫軟件而是思維方式的轉(zhuǎn)變。寫死數(shù)據(jù)的時(shí)候你的腦子里只有“程序”連接到真實(shí)數(shù)據(jù)庫之后你的世界里多了一個(gè)長(zhǎng)線存在的“數(shù)據(jù)實(shí)體”。它會(huì)逼著你考慮數(shù)據(jù)怎么組織才合理、別人怎么訪問、權(quán)限怎么隔離、壞數(shù)據(jù)怎么防、丟了怎么恢復(fù)、并發(fā)沖突怎么處理。這些東西看起來很多但每解決一個(gè)你手里的項(xiàng)目就向“企業(yè)級(jí)”靠近一點(diǎn)。這么多年我?guī)н^的人里有些確實(shí)是文科生出身但他們現(xiàn)在維護(hù)的系統(tǒng)服務(wù)著上萬人。他們唯一的共同點(diǎn)就是敢在那個(gè)“看起來門檻很高”的地方邁出第一步。最后分享一個(gè)小習(xí)慣我給自己的學(xué)習(xí)項(xiàng)目設(shè)了一個(gè)規(guī)則凡是會(huì)重復(fù)看、需要跟別人同步的信息一律不寫死在代碼里。哪怕只是一個(gè)備忘錄列表我也寧愿丟進(jìn)SQLite文件里。這樣每一次練習(xí)都是在提前熟悉真實(shí)項(xiàng)目的操作流。等哪天真的要上一個(gè)大系統(tǒng)你不會(huì)覺得“連接數(shù)據(jù)庫”是你需要準(zhǔn)備的技能它早就是你習(xí)以為常的一部分了。