[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-gorm-advanced-query-helpers-guardrails-zh":3,"article-related-gorm-advanced-query-helpers-guardrails-zh":30,"series-tools-199c5c27-7d55-46b4-a59f-fa2a9d4d6340":73},{"id":4,"slug":5,"title":6,"content":7,"summary":8,"source":9,"source_url":10,"author":11,"image_url":12,"cover_image":12,"category":13,"language":14,"translated_content":11,"related_article_id":15,"keywords":16,"key_takeaways":22,"views":26,"created_at":27,"published_at":28,"topic_cluster_id":29},"199c5c27-7d55-46b4-a59f-fa2a9d4d6340","gorm-advanced-query-helpers-guardrails-zh","GORM 查詢助手把 SQL 變護欄","\u003Cp data-speakable=\"summary\">我把 GORM 進階查詢拆成一套可直接抄的安全寫法，適合拿來做精簡讀取、鎖列、子查詢和 upsert。\u003C\u002Fp>\u003Cp>我用 GORM 有一陣子了，越用越知道它哪裡開始煩。基本 CRUD 很順，大家都會寫；但\u003Ca href=\"\u002Fnews\u002Fopen-source-ai-projects-developers-2026-zh\">專案\u003C\u002Fa>一大，model 一肥，查詢就開始失控。不是選太多欄位，就是鎖得不夠，還有那種先查再寫、兩個 request 同時進來、最後資料互打臉的老問題。這些都不是 GORM 壞掉，是我以前太愛偷懶，覺得 query 先能跑再說。\u003C\u002Fp>\u003Cp>後來我一直回去看 \u003Ca href=\"https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html\">GORM Advanced Query\u003C\u002Fa> 這頁，因為它講的不是炫技，是怎麼把查詢寫成護欄。少拿資料、需要時鎖住、複雜條件別硬拼字串、找不到就初始化或建立，這些都很務實。你不會因為它而拍手，但你會少收到 bug report。\u003C\u002Fp>\u003Ch2>別再把整包 model 撈回來\u003C\u002Fh2>\u003Cblockquote>In GORM, you can efficiently select specific fields using the Select method.\u003C\u002Fblockquote>\u003Cp>翻譯一下就是：你根本不需要每次都把整個 table 的欄位全撈回來。很多 \u003Ca href=\"\u002Ftag\u002Fapi\">API\u003C\u002Fa> 只要 id、name、status，結果我以前還是直接 \u003Ccode>Find(&users)\u003C\u002Fcode>，把一堆根本沒人看的欄位一起掃進來，浪費效能也增加暴露風險。\u003C\u002Fp>\n\u003Cfigure class=\"my-6\">\u003Cimg src=\"https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782606801104-8e4w.png\" alt=\"GORM 查詢助手把 SQL 變護欄\" class=\"rounded-xl w-full\" loading=\"lazy\" \u002F>\u003C\u002Ffigure>\n\u003Cp>GORM 很適合拿小 struct 當讀模型。你查進 \u003Ccode>APIUser\u003C\u002Fcode> 這種 DTO，框架會自動只選對應欄位。這件事看起來很普通，但 table 一旦變寬，差異就很明顯。尤其是高流量 endpoint，少幾個欄位就是少一點掃描成本，少一點 payload，也少一點我半夜改欄位時手滑出事。\u003C\u002Fp>\u003Cp>我之前遇過一個後台列表頁，原本只是顯示帳號與狀態，後來 model 越長越像履歷表。前端只要三欄，後端卻每次都把十幾欄塞回去。那時我改成專門的 read struct 之後，整個 query 就乾淨很多，還順手把不該出現在 API 的欄位隔離掉。\u003C\u002Fp>\u003Cpre>\u003Ccode>type User struct {\n    ID uint\n    Name string\n    Age int\n    Gender string\n    \u002F\u002F 其他很多欄位\n}\n\ntype APIUser struct {\n    ID uint\n    Name string\n}\n\ndb.Model(&User{}).Limit(10).Find(&APIUser{})\n\u002F\u002F SQL: SELECT `id`, `name` FROM `users` LIMIT 10\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>實操寫法很簡單：先把「讀模型」和「寫模型」分開。列表頁、報表、背景任務，全部都可以用小 struct。真的要全欄位時再回頭用完整 model。這不是多此一舉，這是在幫未來的自己少收爛帳。\u003C\u002Fp>\u003Cul>\u003Cli>API 回傳只需要少數欄位，就別拿完整 model。\u003C\u002Fli>\u003Cli>用 \u003Ccode>Select\u003C\u002Fcode> 或小 struct，讓欄位意圖寫在程式碼裡。\u003C\u002Fli>\u003Cli>先看一次 SQL，再決定要不要繼續猜。\u003C\u002Fli>\u003C\u002Ful>\u003Ch2>鎖列不是多餘，是你在避免互搶資料\u003C\u002Fh2>\u003Cblockquote>GORM supports different types of locks, for example: db.Clauses(clause.Locking{Strength: \"UPDATE\"}).Find(&users)\u003C\u002Fblockquote>\u003Cp>也就是說，有些「讀」其實只是寫入前的前置動作。你先看一眼資料，再根據它做更新，這時候如果不鎖列，另一個 transaction 很可能在你眼前把同一筆資料改掉。你以為自己在做合理流程，資料庫其實在旁邊看你翻車。\u003C\u002Fp>\u003Cp>GORM 用 \u003Ccode>clause.Locking\u003C\u002Fcode> 把 \u003Ccode>FOR UPDATE\u003C\u002Fcode>、\u003Ccode>SHARE\u003C\u002Fcode>、\u003Ccode>NOWAIT\u003C\u002Fcode>、\u003Ccode>SKIP LOCKED\u003C\u002Fcode> 包起來。最常見的是 \u003Ccode>UPDATE\u003C\u002Fcode>，表示你要先把這些列鎖住，直到 transaction 結束。這在工作隊列、庫存扣減、任務認領這類流程很實用。兩個 worker 同時搶同一筆資料，這種事我真的看過太多次。\u003C\u002Fp>\u003Cp>我自己最常用的是 \u003Ccode>SKIP LOCKED\u003C\u002Fcode>。因為很多背景 worker 不該卡在那邊等，等到超時只會拖垮整批工作。你要嘛快點失敗，要嘛直接跳過別人已經拿走的列，繼續處理下一筆。這比「先查到再說」的寫法可靠太多。\u003C\u002Fp>\u003Cpre>\u003Ccode>db.Clauses(clause.Locking{Strength: \"UPDATE\"}).Find(&users)\n\u002F\u002F SQL: SELECT * FROM `users` FOR UPDATE\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>實操寫法：把鎖放進 transaction，鎖住後只做必要的 DB 操作，不要在裡面打 API、不要等外部服務、不要把 transaction 開著聊天。鎖列的重點不是「看起來很專業」，而是把會互撞的流程收斂成可預期的順序。\u003C\u002Fp>\u003Cul>\u003Cli>\u003Ccode>UPDATE\u003C\u002Fcode> 適合讀完就要改的流程。\u003C\u002Fli>\u003Cli>\u003Ccode>NOWAIT\u003C\u002Fcode> 適合你寧願失敗也不想等。\u003C\u002Fli>\u003Cli>\u003Ccode>SKIP LOCKED\u003C\u002Fcode> 適合多 worker 並行處理。\u003C\u002Fli>\u003C\u002Ful>\u003Ch2>子查詢比原始 SQL 亂拼好太多\u003C\u002Fh2>\u003Cblockquote>Subqueries are a powerful feature in SQL, allowing nested queries.\u003C\u002Fblockquote>\u003Cp>翻譯一下就是：你不用把 SQL 字串拼到像手工炸彈。GORM 允許你直接把一個 \u003Ccode>*gorm.DB\u003C\u002Fcode> 當成子查詢塞進去，這樣條件、括號、參數綁定都還是框架幫你處理，不用自己在那邊補引號、補括號、補到最後整段看不懂。\u003C\u002Fp>\n\u003Cfigure class=\"my-6\">\u003Cimg src=\"https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782606799364-zm58.png\" alt=\"GORM 查詢助手把 SQL 變護欄\" class=\"rounded-xl w-full\" loading=\"lazy\" \u002F>\u003C\u002Ffigure>\n\u003Cp>最典型的例子，就是拿某個欄位去比對平均值或群組結果。這種情況如果硬寫 raw SQL，通常會\u003Ca href=\"\u002Fnews\u002Fdesign-md-bridge-taste-to-ui-scaffolds-zh\">變成\u003C\u002Fa>一坨沒人想碰的字串。用 subquery 的話，意圖清楚很多：外層做過濾，內層做計算，兩邊分工明白。\u003C\u002Fp>\u003Cp>我以前寫報表時最常犯的錯，就是把複雜邏輯直接塞進一段 raw query。當下很快，半年後沒人敢改。後來我改成先定義子查詢，再把它丟進外層條件，維護成本就差很多。這種寫法不是炫技，是避免程式碼變成考古現場。\u003C\u002Fp>\u003Cpre>\u003Ccode>db.Where(\"amount > (?)\", db.Table(\"orders\").Select(\"AVG(amount)\")).Find(&orders)\n\u002F\u002F SQL: SELECT * FROM \"orders\" WHERE amount > (SELECT AVG(amount) FROM \"orders\");\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>你也可以把子查詢放進 \u003Ccode>FROM\u003C\u002Fcode>，把一段已經過濾或投影過的資料當成臨時表來用。這招很適合做中介資料集，不用真的去建 temp table，也不用把邏輯拆到三個地方。\u003C\u002Fp>\u003Cpre>\u003Ccode>db.Table(\"(?) as u\", db.Model(&User{}).Select(\"name\", \"age\")).Where(\"age = ?\", 18).Find(&User{})\n\u002F\u002F SQL: SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>實操寫法：只要你開始重複同一段過濾條件，或需要拿某個計算結果去比對另一層資料，就先想子查詢。真的太長就先幫它取名，不要把所有邏輯都塞在一行裡。那種寫法看起來很精簡，其實只是把麻煩延後。\u003C\u002Fp>\u003Ch2>條件一複雜，括號就會開始背叛你\u003C\u002Fh2>\u003Cblockquote>Group Conditions in GORM provide a more readable and maintainable way to write complex SQL queries involving multiple conditions.\u003C\u002Fblockquote>\u003Cp>也就是說，當 \u003Ccode>AND\u003C\u002Fcode>、\u003Ccode>OR\u003C\u002Fcode> 開始混在一起時，別再相信你手寫字串時的記憶力。你少一個括號，整個條件語意就會歪掉，而且通常是那種看起來「差不多」但結果完全不對的歪法，最難抓。\u003C\u002Fp>\u003Cp>GORM 的 group conditions 很像把 SQL 結構拆成積木。你用巢狀 \u003Ccode>db.Where(...)\u003C\u002Fcode> 去表達條件群組，程式碼雖然比較長，但邏輯會跟 SQL 一樣清楚。這對篩選器、進階搜尋、後台查詢最有用，因為使用者選項一多，條件就很容易炸開。\u003C\u002Fp>\u003Cp>我自己最討厭的就是那種「先寫一段 WHERE 字串，之後再補條件」的模式。每個人都覺得自己有記得原本的\u003Ca href=\"\u002Fnews\u002Flore-binary-first-version-control-scales-zh\">優先\u003C\u002Fa>順序，結果一改就壞。group conditions 至少把優先順序寫進結構裡，不用靠人腦硬撐。\u003C\u002Fp>\u003Cpre>\u003Ccode>db.Where(\n    db.Where(\"pizza = ?\", \"pepperoni\").Where(db.Where(\"size = ?\", \"small\").Or(\"size = ?\", \"medium\")),\n).Or(\n    db.Where(\"pizza = ?\", \"hawaiian\").Where(\"size = ?\", \"xlarge\"),\n).Find(&Pizza{})\n\u002F\u002F SQL: SELECT * FROM `pizzas` WHERE (pizza = \"pepperoni\" AND (size = \"small\" OR size = \"medium\")) OR (pizza = \"hawaiian\" AND size = \"xlarge\")\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>實操寫法：只要你看到一個 WHERE 裡面有兩層以上的 \u003Ccode>AND\u002FOR\u003C\u002Fcode>，就不要硬拼字串。改成巢狀條件，讓每一層對應一個業務規則。你會少掉很多「欸怎麼這筆也被查出來」的問號。\u003C\u002Fp>\u003Cul>\u003Cli>複雜條件先拆群組，不要先求短。\u003C\u002Fli>\u003Cli>每個群組對應一條業務規則，之後比較好改。\u003C\u002Fli>\u003Cli>你看得懂，接手的人才有機會看得懂。\u003C\u002Fli>\u003C\u002Ful>\u003Ch2>命名參數很無聊，但它真的比較不會出事\u003C\u002Fh2>\u003Cblockquote>GORM enhances the readability and maintainability of SQL queries by supporting named arguments.\u003C\u002Fblockquote>\u003Cp>翻譯一下就是：別再把一堆 \u003Ccode>?\u003C\u002Fcode> 當成樂高亂堆，然後自己數到眼花。命名參數不是花俏功能，它是讓查詢更容易讀，也比較不容易把值塞錯位置。尤其是同一個值要重複出現兩次以上時，這招很好用。\u003C\u002Fp>\u003Cp>GORM 支援 \u003Ccode>sql.Named\u003C\u002Fcode> 跟 \u003Ccode>map[string]interface{}\u003C\u002Fcode>。我通常看情境選。重點不是哪個比較潮，而是哪個在那段程式裡比較好掃一眼就懂。因為 query helper 的價值，本來就是讓人少猜。\u003C\u002Fp>\u003Cp>我以前很愛用位置參數，覺得簡單俐落。後來 helper 一層包一層，參數順序開始像在玩接龍。改成命名參數後，至少我不用再回頭翻函式簽名確認這個值到底是 name 還是 alias。這種小事，寫多了就知道有差。\u003C\u002Fp>\u003Cpre>\u003Ccode>db.Where(\"name1 = @name OR name2 = @name\", sql.Named(\"name\", \"jinzhu\")).Find(&user)\n\u002F\u002F SQL: SELECT * FROM `users` WHERE name1 = \"jinzhu\" OR name2 = \"jinzhu\"\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>實操寫法：有重複值、動態條件、helper 函式層級偏深的查詢，優先用 named args。小查詢還是可以用位置參數，但一旦開始長出分支，命名參數通常比較不會讓你自己看不懂。\u003C\u002Fp>\u003Cul>\u003Cli>\u003Ccode>sql.Named\u003C\u002Fcode> 適合讀起來像一般函式參數。\u003C\u002Fli>\u003Cli>\u003Ccode>map[string]interface{}\u003C\u002Fcode> 適合動態組裝值。\u003C\u002Fli>\u003Cli>選能讓下一個人最快理解的寫法。\u003C\u002Fli>\u003C\u002Ful>\u003Ch2>FirstOrInit 和 FirstOrCreate 才像真的 find-or-make\u003C\u002Fh2>\u003Cblockquote>FirstOrInit fetches the first record that matches given conditions, or initializes a new instance if no matching record is found.\u003C\u002Fblockquote>\u003Cp>也就是說，你可以把「先找，找不到就準備一個」這件事濃縮成一個呼叫。\u003Ca href=\"https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html#FirstOrInit\">FirstOrInit\u003C\u002Fa> 是先找資料，沒有就初始化 struct；\u003Ca href=\"https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html#FirstOrCreate\">FirstOrCreate\u003C\u002Fa> 則是沒有就直接建立。這兩個名字很像，但差很多。\u003C\u002Fp>\u003Cp>我看過太多人自己手寫三段式流程：查詢、判斷、初始化、可能再存一次。能跑，但很吵，而且很容易漏掉某個條件。GORM 這裡的重點，是讓你用 \u003Ccode>Attrs\u003C\u002Fcode> 跟 \u003Ccode>Assign\u003C\u002Fcode> 決定值怎麼進來。\u003Ccode>Attrs\u003C\u002Fcode> 只在沒找到時補預設值，\u003Ccode>Assign\u003C\u002Fcode> 則是找到與否都會套上去。\u003C\u002Fp>\u003Cp>這個差異很實用。你想要的是「如果沒有就給一個預設狀態」，那用 \u003Ccode>Attrs\u003C\u002Fcode>；你想要的是「不管有沒有找到，都先把 in-memory 物件更新成最新狀態」，那用 \u003Ccode>Assign\u003C\u002Fcode>。如果要真的寫回資料庫，再用 \u003Ccode>FirstOrCreate\u003C\u002Fcode>。這樣你的程式碼就不用再自己分兩條路。\u003C\u002Fp>\u003Cpre>\u003Ccode>db.Where(User{Name: \"non_existing\"}).Attrs(User{Age: 20}).FirstOrInit(&user)\n\u002F\u002F user -> User{Name: \"non_existing\", Age: 20} if not found\n\ndb.Where(User{Name: \"Jinzhu\"}).Assign(User{Age: 20}).FirstOrInit(&user)\n\u002F\u002F user -> User{ID: 111, Name: \"Jinzhu\", Age: 20} if found\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>我自己會把它當成「可控版 upsert 前置流程」。但要注意，這不是萬能解法。高併發下還是要靠唯一索引，不然兩個 request 同時找不到、同時建立，照樣會撞車。這種事我不想再用運氣賭。\u003C\u002Fp>\u003Cpre>\u003Ccode>result := db.FirstOrCreate(&user, User{Name: \"non_existing\"})\n\u002F\u002F result.RowsAffected => 1 when created\n\nresult = db.Where(User{Name: \"jinzhu\"}).FirstOrCreate(&user)\n\u002F\u002F result.RowsAffected => 0 when found\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>實操寫法：需要先準備資料但不急著寫入，就用 \u003Ccode>FirstOrInit\u003C\u002Fcode>。需要查不到就建立，就用 \u003Ccode>FirstOrCreate\u003C\u002Fcode>。如果這條路徑會被很多 worker 同時跑，先補唯一鍵，別把資料一致性寄託在程式碼運氣上。\u003C\u002Fp>\u003Ch2>動態結果就別硬塞回 struct\u003C\u002Fh2>\u003Cblockquote>GORM provides flexibility in querying data by allowing results to be scanned into a map[string]interface{} or []map[string]interface{}.\u003C\u002Fblockquote>\u003Cp>翻譯一下就是：有些查詢本來就不是給固定 schema 用的。像後台報表、匯出、探索性查詢，結果欄位可能會跟著設定變動。這時候硬做一個假 model 只是自找麻煩，GORM 直接掃進 \u003Ccode>map[string]interface{}\u003C\u002Fcode> 或 slice of maps 反而比較合理。\u003C\u002Fp>\u003Cp>不過這裡有個小雷點：你還是要先指定 \u003Ccode>Model\u003C\u002Fcode> 或 \u003Ccode>Table\u003C\u002Fcode>，不然 GORM 不知道要查哪裡。這種錯很低級，但很常發生，因為大家一看到 map 就以為可以亂來。\u003C\u002Fp>\u003Cp>我會把這個寫法留給真正動態的場景。比如管理後台要根據使用者選的欄位輸出結果，或是某些臨時分析工具。只要 schema 是固定的，我還是會回到 struct。因為 struct 比 map 更能保護你少打錯欄位名。\u003C\u002Fp>\u003Cpre>\u003Ccode>result := map[string]interface{}{}\ndb.Model(&User{}).First(&result, \"id = ?\", 1)\n\u002F\u002F SQL: SELECT * FROM `users` WHERE id = 1 LIMIT 1\n\nvar results []map[string]interface{}\ndb.Table(\"users\").Find(&results)\n\u002F\u002F SQL: SELECT * FROM `users`\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>實操寫法：如果輸出形狀會變，就用 map；如果輸出形狀固定，就用 struct。不要因為懶得定義型別就把所有查詢都變成 map，最後你會得到一堆難以維護的動態資料處理。\u003C\u002Fp>\u003Ch2>可抄的模板\u003C\u002Fh2>\u003Cpre>\u003Ccode>\u002F\u002F GORM advanced query playbook\n\u002F\u002F 直接複製到專案裡，再把 model 名稱改掉。\n\npackage data\n\nimport (\n    \"database\u002Fsql\"\n\n    \"gorm.io\u002Fgorm\"\n    \"gorm.io\u002Fgorm\u002Fclause\"\n)\n\ntype User struct {\n    ID     uint\n    Name   string\n    Age    int\n    Status string\n}\n\ntype APIUser struct {\n    ID   uint\n    Name string\n}\n\n\u002F\u002F 1) 只撈需要的欄位\nfunc ListAPIUsers(db *gorm.DB, limit int) ([]APIUser, error) {\n    var users []APIUser\n    err := db.Model(&User{}).\n        Limit(limit).\n        Find(&users).Error\n    return users, err\n}\n\n\u002F\u002F 2) 在 transaction 裡鎖列\nfunc ClaimUsers(db *gorm.DB) ([]User, error) {\n    var users []User\n    err := db.Transaction(func(tx *gorm.DB) error {\n        return tx.Clauses(clause.Locking{\n            Strength: \"UPDATE\",\n            Options:   \"SKIP LOCKED\",\n        }).Where(\"status = ?\", \"pending\").Find(&users).Error\n    })\n    return users, err\n}\n\n\u002F\u002F 3) 用子查詢，不要自己拼字串\nfunc UsersAboveAverageAge(db *gorm.DB) ([]User, error) {\n    var users []User\n    avgAge := db.Model(&User{}).Select(\"AVG(age)\")\n    err := db.Where(\"age > (?)\", avgAge).Find(&users).Error\n    return users, err\n}\n\n\u002F\u002F 4) 複雜條件用 group conditions\nfunc FindPizzaCandidates(db *gorm.DB) error {\n    return db.Where(\n        db.Where(\"pizza = ?\", \"pepperoni\").Where(\n            db.Where(\"size = ?\", \"small\").Or(\"size = ?\", \"medium\"),\n        ),\n    ).Or(\n        db.Where(\"pizza = ?\", \"hawaiian\").Where(\"size = ?\", \"xlarge\"),\n    ).Error\n}\n\n\u002F\u002F 5) 重複值用 named arguments\nfunc FindBySharedName(db *gorm.DB, name string) (*User, error) {\n    var user User\n    err := db.Where(\"name1 = @name OR name2 = @name\", sql.Named(\"name\", name)).First(&user).Error\n    return &user, err\n}\n\n\u002F\u002F 6) 只初始化，不立刻寫入\nfunc InitUser(db *gorm.DB, name string) (*User, error) {\n    var user User\n    err := db.Where(User{Name: name}).Attrs(User{Status: \"new\"}).FirstOrInit(&user).Error\n    return &user, err\n}\n\n\u002F\u002F 7) 找不到就建立，並回傳是否新建\nfunc UpsertUser(db *gorm.DB, name string) (*User, bool, error) {\n    var user User\n    result := db.Where(User{Name: name}).Assign(User{Status: \"active\"}).FirstOrCreate(&user)\n    created := result.RowsAffected == 1\n    return &user, created, result.Error\n}\n\n\u002F\u002F 8) 動態結果直接掃進 map\nfunc LoadDynamicUsers(db *gorm.DB) ([]map[string]interface{}, error) {\n    var rows []map[string]interface{}\n    err := db.Table(\"users\").Find(&rows).Error\n    return rows, err\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>這版模板是我會真的留在專案裡的那種，不是貼完就忘的範例。重點不是每一行都照抄，而是你下次寫 query 時，知道該先選欄位、再想鎖、再想子查詢，最後才考慮要不要 raw SQL。\u003C\u002Fp>\u003Cp>原始來源是 \u003Ca href=\"https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html\">https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html\u003C\u002Fa>，另外我也對照了 GORM 官方文件裡的 \u003Ca href=\"https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html#Locking\">Locking\u003C\u002Fa>、\u003Ca href=\"https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html#FirstOrInit\">FirstOrInit\u003C\u002Fa>、\u003Ca href=\"https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html#FirstOrCreate\">FirstOrCreate\u003C\u002Fa>。上面這篇是我自己把文件重組成比較好直接上手的版本，模板與解釋屬於衍生整理。","我拆 GORM 進階查詢，整理成可直接抄的模式，讓你少選欄位、少踩鎖、少寫原始 SQL。","gorm.io","https:\u002F\u002Fgorm.io\u002Fdocs\u002Fadvanced_query.html",null,"https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782606801104-8e4w.png","tools","zh","a8344911-b020-4892-ba6c-621df2dc11f8",[17,18,19,20,21],"GORM","advanced query","subquery","row locking","FirstOrCreate",[23,24,25],"少選欄位、分離讀寫模型，能直接降低掃描成本與暴露風險。","鎖列、子查詢、group conditions 讓複雜流程不必靠手寫 raw SQL 硬撐。","FirstOrInit \u002F FirstOrCreate 能把 find-or-make 收斂成單一路徑，但仍要搭配唯一索引。",0,"2026-06-28T00:32:58.617013+00:00","2026-06-28T00:32:58.606+00:00","ddbe17bf-4560-43f7-af76-3e7d6e08e601",{"tags":31,"relatedLang":32,"relatedPosts":36},[],{"id":15,"slug":33,"title":34,"language":35},"gorm-advanced-query-helpers-guardrails-en","GORM query helpers turn SQL into guardrails","en",[37,43,49,55,61,67],{"id":38,"slug":39,"title":40,"cover_image":41,"image_url":41,"created_at":42,"category":13},"04a7998f-745f-4a4d-851b-d5888aac1000","golangci-lint-faq-ci-policy-zh","FAQ 把 golangci-lint 變 CI 政策","https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782607697597-rh2b.png","2026-06-28T00:47:53.937018+00:00",{"id":44,"slug":45,"title":46,"cover_image":47,"image_url":47,"created_at":48,"category":13},"c81b891c-6bd0-48e7-b9b0-eacbc93560f0","golangci-lint-v2-5-0-revive-checks-zh","Golangci-lint v2.5.0 加入 8 項 revive 檢查","https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782605877403-srf2.png","2026-06-28T00:17:31.421555+00:00",{"id":50,"slug":51,"title":52,"cover_image":53,"image_url":53,"created_at":54,"category":13},"7dfeee82-47b0-41bb-8a47-0ad5c9203a23","open-source-ai-projects-developers-2026-zh","2026 開發者必備 7 個開源 AI 專案","https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782593290041-l5h1.png","2026-06-27T20:47:36.429404+00:00",{"id":56,"slug":57,"title":58,"cover_image":59,"image_url":59,"created_at":60,"category":13},"032a0b15-7665-4662-a3e2-ca7d5873f4d0","midjourney-review-2026-v8-worth-it-zh","Midjourney 2026 評測：V8 還值得嗎","https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782576173444-63qz.png","2026-06-27T16:02:30.078028+00:00",{"id":62,"slug":63,"title":64,"cover_image":65,"image_url":65,"created_at":66,"category":13},"12f76440-abb5-4e5f-9b98-d12c7468652d","midjourney-v81-faster-renders-pricing-video-zh","Midjourney V8.1 4-5倍加速上線","https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782573468737-8pyt.png","2026-06-27T15:17:23.186362+00:00",{"id":68,"slug":69,"title":70,"cover_image":71,"image_url":71,"created_at":72,"category":13},"91637501-ee43-4951-b43c-ce2ba3299d3a","mlops-roadmap-2026-turns-learning-into-delivery-zh","MLOps 路線圖把學習變交付","https:\u002F\u002Fxxdpdyhzhpamafnrdkyq.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fcovers\u002Finline-1782567219485-a9wf.png","2026-06-27T13:33:06.891797+00:00",[74,79,84,89,94,99,104,109,114,119],{"id":75,"slug":76,"title":77,"created_at":78},"855cd52f-6fab-46cc-a7c1-42195e8a0de4","surepath-real-time-mcp-policy-controls-zh","SurePath 推出即時 MCP 政策控管","2026-03-26T07:57:40.77233+00:00",{"id":80,"slug":81,"title":82,"created_at":83},"9b19ab54-edef-4dbd-9ce4-a51e4bae4ebb","mcp-in-2026-the-ai-tool-layer-teams-use-zh","2026 年 MCP：團隊真的在用的 AI 工具層","2026-03-26T08:01:46.589694+00:00",{"id":85,"slug":86,"title":87,"created_at":88},"af9c46c3-7a28-410b-9f04-32b3de30a68c","prompting-in-2026-what-actually-works-zh","2026 提示工程，真正有用的是什麼","2026-03-26T08:08:12.453028+00:00",{"id":90,"slug":91,"title":92,"created_at":93},"05553086-6ed0-4758-81fd-6cab24b575e0","garry-tan-open-sources-claude-code-toolkit-zh","Garry Tan 開源 Claude Code 工具包","2026-03-26T08:26:20.068737+00:00",{"id":95,"slug":96,"title":97,"created_at":98},"042a73a2-18a2-433d-9e8f-9802b9559aac","github-ai-projects-to-watch-in-2026-zh","2026 必看 20 個 GitHub AI 專案","2026-03-26T08:28:09.619964+00:00",{"id":100,"slug":101,"title":102,"created_at":103},"a5f94120-ac0d-4483-9a8b-63590071ac6a","claude-code-vs-cursor-2026-zh","Claude Code 與 Cursor 深度對比：202…","2026-03-26T13:27:14.279193+00:00",{"id":105,"slug":106,"title":107,"created_at":108},"0975afa1-e0c7-4130-a20d-d890eaed995e","practical-github-guide-learning-ml-2026-zh","2026 機器學習入門 GitHub 實用指南","2026-03-27T01:16:49.712576+00:00",{"id":110,"slug":111,"title":112,"created_at":113},"bfdb467a-290f-4a80-b3a9-6f081afb6dff","aiml-2026-student-ai-ml-lab-repo-review-zh","AIML-2026：像課綱的學生實驗 Repo","2026-03-27T01:21:51.467798+00:00",{"id":115,"slug":116,"title":117,"created_at":118},"80cabc3e-09fc-4ff5-8f07-b8d68f5ae545","ai-trending-github-repos-and-research-feeds-zh","AI Trending：把 AI 資源收成一張表","2026-03-27T01:31:35.262183+00:00",{"id":120,"slug":121,"title":122,"created_at":123},"3ce6e6e2-bac5-463e-9f8d-45caabcc61f7","awesome-ai-for-science-research-tools-map-zh","AI 科研工具清單，開始像地圖了","2026-03-27T01:46:50.521945+00:00"]