器人JPA實(shí)體映射與ddl-auto的利與弊)
03-JPA實(shí)體映射與ddl-auto的利與弊黒漂技術(shù)佬 · AI 伙伴AI-Partner「數(shù)據(jù)接口部署與二次開發(fā)」系列 03上一篇拆了建表 SQL這篇看 Java 側(cè)。AI 伙伴用 Spring Data JPA 做 ORM8 個(gè)實(shí)體類把 8 張表映射起來(lái)。這一篇講三件事注解是怎么翻譯成表結(jié)構(gòu)的ddl-autoupdate這個(gè)配置為什么開發(fā)時(shí)香、生產(chǎn)時(shí)險(xiǎn)以及open-in-viewfalse這行沒人注意的配置為什么值得表?yè)P(yáng)。一、先看一份真實(shí)的實(shí)體// 項(xiàng)目源碼entity/User.java節(jié)選DataEntityTable(namet_user,indexes{Index(nameidx_user_openid,columnListopenId,uniquetrue),Index(nameidx_user_phone,columnListphone)})publicclassUser{IdGeneratedValue(strategyGenerationType.IDENTITY)privateLongid;Column(nullablefalse,length64)privateStringopenId;Column(length16)privateStringplatform;Column(length512)privateStringavatarUrl;Column(length2000)privateStringpersonaSummary;privateIntegerstatus1;privateLocalDateTimecreatedAt;privateLocalDateTimeupdatedAt;PrePersistpublicvoidprePersist(){createdAtLocalDateTime.now();updatedAtcreatedAt;}PreUpdatepublicvoidpreUpdate(){updatedAtLocalDateTime.now();}}8 個(gè)實(shí)體全是這個(gè)套路沒有繼承體系、沒有關(guān)聯(lián)注解OneToMany一個(gè)都沒有——表間關(guān)系靠user_id/device_id字段實(shí)體間互不引用。這正好和上一篇無(wú)外鍵邏輯關(guān)聯(lián)的設(shè)計(jì)哲學(xué)閉環(huán)了。二、五個(gè)注解逐個(gè)講Entity聲明這是個(gè) JPA 實(shí)體會(huì)被 Hibernate 托管。類名默認(rèn)映射同名表所以必須配合下一個(gè)注解。Table(name t_user)顯式指定表名。這里能對(duì)上sql/init.sql里的t_前綴命名同時(shí)把索引也聲明在Table的indexes屬性里實(shí)體和 DDL 兩邊保持同步。注意一個(gè)小細(xì)節(jié)實(shí)體里聲明的索引名如idx_user_openid和 init.sql 里的名字uk_user_open_id并不一致——兩套真相的第一個(gè)裂縫后面細(xì)說。Id GeneratedValue(strategy GenerationType.IDENTITY)主鍵策略用數(shù)據(jù)庫(kù)自增。MySQL 8 的 InnoDB 自增主鍵是趨勢(shì)遞增的做 InnoDB 聚簇索引正好。缺點(diǎn)是分庫(kù)分表時(shí)不好用——但這個(gè)項(xiàng)目 8 張表顯然沒到那個(gè)體量IDENTITY 是最樸素正確的選擇。Columnnullablefalse對(duì)應(yīng) DDL 的 NOT NULLlength64對(duì)應(yīng) VARCHAR(64)特殊類型用columnDefinition直接寫死比如對(duì)話表的Column(columnDefinition TEXT)。注意這些屬性只影響建表時(shí)生成的 DDL不影響運(yùn)行時(shí)讀寫——運(yùn)行時(shí)它只負(fù)責(zé)這個(gè)字段映射這列。PrePersist / PreUpdateJPA 生命周期回調(diào)。PrePersist在 INSERT 前觸發(fā)填 createdAt 和 updatedAtPreUpdate在 UPDATE 前觸發(fā)刷新 updatedAt。這就是上一篇說的時(shí)間字段無(wú)數(shù)據(jù)庫(kù)默認(rèn)值的配套方案——時(shí)間由應(yīng)用層統(tǒng)一維護(hù)。好處是時(shí)間戳和業(yè)務(wù)代碼在同一時(shí)區(qū)、同一時(shí)鐘壞處也說過繞開 JPA 直接寫 SQL時(shí)間就是 NULL。三、駝峰轉(zhuǎn)下劃線全局命名策略實(shí)體字段寫openId數(shù)據(jù)庫(kù)列是open_id中間沒有Column(nameopen_id)這種逐個(gè)標(biāo)注——靠的是 Spring Boot 的默認(rèn)命名策略SpringPhysicalNamingStrategy駝峰自動(dòng)轉(zhuǎn)小寫下劃線。deviceCode→device_code、remindTime→remind_time、lastHeartbeatAt→last_heartbeat_at全自動(dòng)對(duì)齊。這帶來(lái)一個(gè)隱性約定實(shí)體字段名和列名必須滿足這條轉(zhuǎn)換規(guī)則。如果你起了個(gè)userOpenID連續(xù)大寫之類的怪名轉(zhuǎn)出來(lái)可能和 init.sql 對(duì)不上就會(huì)出現(xiàn)實(shí)體以為列叫 A、數(shù)據(jù)庫(kù)里叫 B的運(yùn)行時(shí)錯(cuò)誤。所以二次開發(fā)時(shí)實(shí)體字段請(qǐng)老老實(shí)實(shí)用標(biāo)準(zhǔn)駝峰。再補(bǔ)一張實(shí)體注解 → 生成 DDL的翻譯對(duì)照表寫實(shí)體時(shí)對(duì)著查實(shí)體注解/屬性生成的 DDL 片段本項(xiàng)目實(shí)例Column(nullable false, length 64)VARCHAR(64) NOT NULLUser.openIdColumn(length 2000)VARCHAR(2000)User.personaSummaryColumn(columnDefinition TEXT)TEXTConversation 的兩個(gè)消息字段Column(unique true)在 Index 里聲明UNIQUE KEYUser.openId、Device.deviceCodeGeneratedValue(IDENTITY)AUTO_INCREMENT全部 8 張表主鍵Boolean字段TINYINT(1)Memory.active、Health.abnormalLocalDateTime字段DATETIME所有時(shí)間字段LocalDate字段DATEUser.birthday這張表還有個(gè)隱藏用法當(dāng)你懷疑線上表結(jié)構(gòu)和實(shí)體不一致時(shí)可以拿SHOW CREATE TABLE的輸出和這張對(duì)照表逐列核對(duì)——update 模式只加不刪的特性造成的漂移就是這么被揪出來(lái)的。四、ddl-autoupdate甜蜜的陷阱# 項(xiàng)目源碼application.ymlspring:jpa:hibernate:ddl-auto:updateformat_sql:trueshow-sql:falseopen-in-view:falseddl-autoupdate的意思是每次啟動(dòng)Hibernate 對(duì)比實(shí)體和數(shù)據(jù)庫(kù)結(jié)構(gòu)缺的建、少列的加。它的好處實(shí)實(shí)在在——第一次啟動(dòng)后端8 張表全自動(dòng)建出來(lái)零 SQL 手工活本地起個(gè)空 MySQL 就能跑通整個(gè)項(xiàng)目。這對(duì)教學(xué)項(xiàng)目和二次開發(fā)者非常友好。但update的語(yǔ)義有四個(gè)坑一個(gè)比一個(gè)隱蔽不會(huì)刪列但會(huì)悄悄加列。你把實(shí)體字段nickname改名成nickNameHibernate 不會(huì) ALTER 原列而是新增一列nick_name原列nickname連同里面的數(shù)據(jù)原地不動(dòng)。數(shù)據(jù)看起來(lái)丟了實(shí)際是躺在舊列里新代碼卻讀不到了。與 init.sql 成為兩份真相。數(shù)據(jù)庫(kù)結(jié)構(gòu)一半來(lái)自 init.sql、一半來(lái)自 Hibernate 自動(dòng)變更誰(shuí)也不敢說哪份是全的。前面提到的索引名不一致就是活例子init.sql 叫uk_user_open_id實(shí)體聲明叫idx_user_openid——如果是 init.sql 先建了表實(shí)體聲明的索引到底建沒建成取決于 update 模式怎么處理已存在表通常它不會(huì)重建已有索引于是實(shí)體里的索引聲明形同虛設(shè)。約束可能比你想象的弱。update 模式對(duì)已有列的類型變更、非空約束收緊等操作非常保守時(shí)間長(zhǎng)了實(shí)體和真實(shí)表結(jié)構(gòu)的偏差會(huì)越積越大。生產(chǎn)環(huán)境跑 update 是事故高發(fā)區(qū)。多個(gè)實(shí)例同時(shí)啟動(dòng)可能并發(fā)做 DDL一次不小心的實(shí)體重構(gòu)可能觸發(fā)大表 ALTER把線上業(yè)務(wù)卡死。所以行業(yè)標(biāo)準(zhǔn)做法是開發(fā)用 update測(cè)試可用 update 或 validate生產(chǎn)必須 validate 或 none。五、三套環(huán)境配置對(duì)照表配置項(xiàng)開發(fā)環(huán)境測(cè)試環(huán)境生產(chǎn)環(huán)境說明ddl-autoupdateupdate或validatevalidate/none生產(chǎn)表結(jié)構(gòu)變更走人工審核的 SQL 腳本show-sqltrue調(diào)試期falsefalse生產(chǎn)開 show-sql 刷日志且拖性能format_sqltruefalsefalse只影響日志可讀性open-in-viewfalsefalsefalse三套都關(guān)理由見下節(jié)表結(jié)構(gòu)來(lái)源Hibernate 自動(dòng)建初始化腳本 updateinit.sql 變更腳本人工讓 init.sql 成為唯一真相連接參數(shù)本地 127.0.0.1:3306測(cè)試庫(kù)獨(dú)立賬號(hào)最小權(quán)限別拿 root 直連生產(chǎn)生產(chǎn)用validate的含義啟動(dòng)時(shí)只校驗(yàn)實(shí)體和表結(jié)構(gòu)是否匹配不匹配直接啟動(dòng)失敗——把結(jié)構(gòu)漂移扼殺在發(fā)布環(huán)節(jié)而不是運(yùn)行到一半報(bào) SQL 錯(cuò)。none則連校驗(yàn)都省了適合表結(jié)構(gòu)完全由 DBA 團(tuán)隊(duì)管控的場(chǎng)景。六、open-in-viewfalse一行配置的清醒spring.jpa.open-in-view默認(rèn)是trueHTTP 請(qǐng)求進(jìn)來(lái)就開一個(gè)數(shù)據(jù)庫(kù)事務(wù)視圖直到響應(yīng)渲染完成才關(guān)。聽起來(lái)方便——Controller、View 層都能隨手訪問實(shí)體的懶加載屬性——實(shí)際上有三個(gè)代價(jià)數(shù)據(jù)庫(kù)連接被整個(gè)請(qǐng)求周期占用高并發(fā)下連接池見底、事務(wù)邊界模糊一半查詢?cè)谑聞?wù)里、一半在事務(wù)外、懶加載把性能問題藏到視圖層才爆。AI 伙伴把它顯式關(guān)成false意味著數(shù)據(jù)庫(kù)操作被壓縮在 Service 層內(nèi)完成Controller 拿到的是已經(jīng)查詢完整的實(shí)體或 DTO。代價(jià)是要警惕懶加載坑如果實(shí)體之間有OneToMany之類的關(guān)聯(lián)本項(xiàng)目沒有在事務(wù)外訪問集合屬性會(huì)拋LazyInitializationException。本項(xiàng)目因?yàn)閷?shí)體零關(guān)聯(lián)這個(gè)坑等于自動(dòng)繞過了——但二次開發(fā)加關(guān)聯(lián)時(shí)請(qǐng)記住它。七、合規(guī)提醒實(shí)體即數(shù)據(jù)資產(chǎn)的地圖。給 User、HealthRecord 這類含個(gè)人信息和健康數(shù)據(jù)的實(shí)體加字段時(shí)先過三問**這個(gè)字段是最小必要嗎采集前有授權(quán)依據(jù)嗎用戶注銷后這個(gè)字段的數(shù)據(jù)能刪干凈嗎**尤其注意ddl-autoupdate的只加不刪特性意味著你刪掉實(shí)體里的敏感字段數(shù)據(jù)庫(kù)列和列里的歷史數(shù)據(jù)并不會(huì)消失——真要下線某類敏感數(shù)據(jù)得手動(dòng)寫清理腳本別讓刪了停留在代碼層面。小結(jié)JPA 注解是把雙刃劍——注解即文檔、啟動(dòng)即建表代價(jià)是ddl-autoupdate造成的兩份真相和悄悄加列的暗坑。記住口訣開發(fā)求快用 update生產(chǎn)求穩(wěn)用 validate表結(jié)構(gòu)真相永遠(yuǎn)以人工管理的 init.sql 和變更腳本為準(zhǔn)。下一篇我們看這 8 張表之上8 個(gè) Repository 接口是怎么做到一行 SQL 都不寫的。