[{"data":1,"prerenderedAt":3893},["ShallowReactive",2],{"post:\u002Fposts\u002Fjavaspring\u002Fspring-notes-03":3,"posts:ordered":3614},{"id":4,"title":5,"body":6,"category":3598,"cover":3599,"date":3600,"description":3601,"extension":3602,"featured":3603,"meta":3604,"navigation":130,"path":3605,"seo":3606,"series":3607,"seriesOrder":134,"stem":3608,"tags":3609,"__hash__":3613},"posts\u002Fposts\u002Fjavaspring\u002Fspring-notes-03.md","Spring 学习笔记 03：JPA 查询——派生方法、JPQL 与分页排序",{"type":7,"value":8,"toc":3573},"minimark",[9,16,21,33,49,71,75,78,364,371,376,459,463,785,805,809,819,823,995,999,1042,1049,1053,1056,1102,1115,1119,1221,1225,1285,1330,1336,1369,1431,1438,1442,1446,1509,1533,1537,1603,1622,1680,1700,1777,1823,1832,1836,1882,1892,1906,1910,1913,1918,2049,2057,2328,2341,2348,2415,2422,2688,2697,2704,2816,2838,2843,2890,3001,3017,3034,3038,3053,3105,3108,3144,3174,3179,3183,3240,3255,3271,3289,3293,3485,3489,3552,3569],[10,11,12],"blockquote",{},[13,14,15],"p",{},"这是一份个人学习笔记：概念部分是我按自己的理解写的，示例代码都是自己敲的，参考的是\nSpring 官方文档与 Jakarta Persistence 规范。理解有偏差的地方欢迎指出。",[17,18,20],"h2",{"id":19},"_0-只有-save-和-findbyid-是不够的","0 只有 save 和 findById 是不够的",[13,22,23,24,28,29,32],{},"上一篇的 ",[25,26,27],"code",{},"BookRepository"," 继承了 ",[25,30,31],{},"JpaRepository","，白拿到一堆方法，但真实需求很快会超出它们：",[34,35,36,40,43,46],"ul",{},[37,38,39],"li",{},"按作者查这个人的书；",[37,41,42],{},"按书名模糊搜索；",[37,44,45],{},"查某年之后出版的、且状态是「可借」的书；",[37,47,48],{},"分页展示，每页 10 条按书名排序。",[13,50,51,52,54,55,59,60,66,67,70],{},"这些方法 ",[25,53,31],{}," 都没有。有三条路可走：",[56,57,58],"strong",{},"方法名派生查询","、",[56,61,62,65],{},[25,63,64],{},"@Query"," 写 JPQL","、\n",[56,68,69],{},"原生 SQL","。优先顺序也基本是这个顺序——能用方法名表达的，不要写查询语句。",[17,72,74],{"id":73},"_1-派生查询把意图写进方法名","1 派生查询：把意图写进方法名",[13,76,77],{},"Spring Data JPA 会解析方法名，从中拆出查询意图。只写一个方法签名就够了：",[79,80,85],"pre",{"className":81,"code":82,"language":83,"meta":84,"style":84},"language-java shiki shiki-themes github-light github-dark","public interface BookRepository extends JpaRepository\u003CBook, Long> {\n\n    List\u003CBook> findByAuthor(String author);\n\n    List\u003CBook> findByAuthorIgnoreCase(String author);\n\n    List\u003CBook> findByPublishedYearGreaterThanEqual(int year);\n\n    List\u003CBook> findByTitleContainingIgnoreCaseOrderByTitleAsc(String keyword);\n\n    long countByStatus(BookStatus status);\n\n    boolean existsByIsbn(String isbn);\n\n    List\u003CBook> findByAuthorIn(Collection\u003CString> authors);\n\n    List\u003CBook> findByPublishedYearBetween(int from, int to);\n\n    Optional\u003CBook> findFirstByOrderByPublishedYearDesc();\n}\n","java","",[25,86,87,125,132,156,161,179,184,207,212,231,236,253,258,274,279,304,309,337,342,358],{"__ignoreMap":84},[88,89,92,96,99,103,106,109,113,116,119,122],"span",{"class":90,"line":91},"line",1,[88,93,95],{"class":94},"szBVR","public",[88,97,98],{"class":94}," interface",[88,100,102],{"class":101},"sScJk"," BookRepository",[88,104,105],{"class":94}," extends",[88,107,108],{"class":101}," JpaRepository",[88,110,112],{"class":111},"sVt8B","\u003C",[88,114,115],{"class":94},"Book",[88,117,118],{"class":111},", ",[88,120,121],{"class":94},"Long",[88,123,124],{"class":111},"> {\n",[88,126,128],{"class":90,"line":127},2,[88,129,131],{"emptyLinePlaceholder":130},true,"\n",[88,133,135,138,140,143,146,149,153],{"class":90,"line":134},3,[88,136,137],{"class":111},"    List\u003C",[88,139,115],{"class":94},[88,141,142],{"class":111},"> ",[88,144,145],{"class":101},"findByAuthor",[88,147,148],{"class":111},"(String ",[88,150,152],{"class":151},"s4XuR","author",[88,154,155],{"class":111},");\n",[88,157,159],{"class":90,"line":158},4,[88,160,131],{"emptyLinePlaceholder":130},[88,162,164,166,168,170,173,175,177],{"class":90,"line":163},5,[88,165,137],{"class":111},[88,167,115],{"class":94},[88,169,142],{"class":111},[88,171,172],{"class":101},"findByAuthorIgnoreCase",[88,174,148],{"class":111},[88,176,152],{"class":151},[88,178,155],{"class":111},[88,180,182],{"class":90,"line":181},6,[88,183,131],{"emptyLinePlaceholder":130},[88,185,187,189,191,193,196,199,202,205],{"class":90,"line":186},7,[88,188,137],{"class":111},[88,190,115],{"class":94},[88,192,142],{"class":111},[88,194,195],{"class":101},"findByPublishedYearGreaterThanEqual",[88,197,198],{"class":111},"(",[88,200,201],{"class":94},"int",[88,203,204],{"class":151}," year",[88,206,155],{"class":111},[88,208,210],{"class":90,"line":209},8,[88,211,131],{"emptyLinePlaceholder":130},[88,213,215,217,219,221,224,226,229],{"class":90,"line":214},9,[88,216,137],{"class":111},[88,218,115],{"class":94},[88,220,142],{"class":111},[88,222,223],{"class":101},"findByTitleContainingIgnoreCaseOrderByTitleAsc",[88,225,148],{"class":111},[88,227,228],{"class":151},"keyword",[88,230,155],{"class":111},[88,232,234],{"class":90,"line":233},10,[88,235,131],{"emptyLinePlaceholder":130},[88,237,239,242,245,248,251],{"class":90,"line":238},11,[88,240,241],{"class":94},"    long",[88,243,244],{"class":101}," countByStatus",[88,246,247],{"class":111},"(BookStatus ",[88,249,250],{"class":151},"status",[88,252,155],{"class":111},[88,254,256],{"class":90,"line":255},12,[88,257,131],{"emptyLinePlaceholder":130},[88,259,261,264,267,269,272],{"class":90,"line":260},13,[88,262,263],{"class":94},"    boolean",[88,265,266],{"class":101}," existsByIsbn",[88,268,148],{"class":111},[88,270,271],{"class":151},"isbn",[88,273,155],{"class":111},[88,275,277],{"class":90,"line":276},14,[88,278,131],{"emptyLinePlaceholder":130},[88,280,282,284,286,288,291,294,297,299,302],{"class":90,"line":281},15,[88,283,137],{"class":111},[88,285,115],{"class":94},[88,287,142],{"class":111},[88,289,290],{"class":101},"findByAuthorIn",[88,292,293],{"class":111},"(Collection\u003C",[88,295,296],{"class":94},"String",[88,298,142],{"class":111},[88,300,301],{"class":151},"authors",[88,303,155],{"class":111},[88,305,307],{"class":90,"line":306},16,[88,308,131],{"emptyLinePlaceholder":130},[88,310,312,314,316,318,321,323,325,328,330,332,335],{"class":90,"line":311},17,[88,313,137],{"class":111},[88,315,115],{"class":94},[88,317,142],{"class":111},[88,319,320],{"class":101},"findByPublishedYearBetween",[88,322,198],{"class":111},[88,324,201],{"class":94},[88,326,327],{"class":151}," from",[88,329,118],{"class":111},[88,331,201],{"class":94},[88,333,334],{"class":151}," to",[88,336,155],{"class":111},[88,338,340],{"class":90,"line":339},18,[88,341,131],{"emptyLinePlaceholder":130},[88,343,345,348,350,352,355],{"class":90,"line":344},19,[88,346,347],{"class":111},"    Optional\u003C",[88,349,115],{"class":94},[88,351,142],{"class":111},[88,353,354],{"class":101},"findFirstByOrderByPublishedYearDesc",[88,356,357],{"class":111},"();\n",[88,359,361],{"class":90,"line":360},20,[88,362,363],{"class":111},"}\n",[13,365,366,367,370],{},"方法名由「",[56,368,369],{},"前缀 + 条件 + 排序\u002F限制","」组成。",[372,373,375],"h3",{"id":374},"_11-前缀","1.1 前缀",[377,378,379,392],"table",{},[380,381,382],"thead",{},[383,384,385,389],"tr",{},[386,387,388],"th",{},"前缀",[386,390,391],{},"作用",[393,394,395,419,433,446],"tbody",{},[383,396,397,413],{},[398,399,400,403,404,403,407,403,410],"td",{},[25,401,402],{},"findBy"," \u002F ",[25,405,406],{},"readBy",[25,408,409],{},"queryBy",[25,411,412],{},"getBy",[398,414,415,416,418],{},"查询，返回实体或集合（",[25,417,412],{}," 是 Boot 3 新增的别名）",[383,420,421,426],{},[398,422,423],{},[25,424,425],{},"countBy",[398,427,428,429,432],{},"返回条数（",[25,430,431],{},"long","）",[383,434,435,440],{},[398,436,437],{},[25,438,439],{},"existsBy",[398,441,442,443,432],{},"返回是否存在（",[25,444,445],{},"boolean",[383,447,448,456],{},[398,449,450,403,453],{},[25,451,452],{},"deleteBy",[25,454,455],{},"removeBy",[398,457,458],{},"删除匹配的记录",[372,460,462],{"id":461},"_12-条件关键字","1.2 条件关键字",[377,464,465,478],{},[380,466,467],{},[383,468,469,472,475],{},[386,470,471],{},"关键字",[386,473,474],{},"生成的 SQL 片段",[386,476,477],{},"例子",[393,479,480,503,523,543,565,586,603,627,651,671,694,712,727,748,767],{},[383,481,482,490,498],{},[398,483,484,403,487],{},[25,485,486],{},"And",[25,488,489],{},"Or",[398,491,492,403,495],{},[25,493,494],{},"and",[25,496,497],{},"or",[398,499,500],{},[25,501,502],{},"findByAuthorAndStatus(...)",[383,504,505,513,518],{},[398,506,507,403,510],{},[25,508,509],{},"Is",[25,511,512],{},"Equals",[398,514,515],{},[25,516,517],{},"=",[398,519,520],{},[25,521,522],{},"findByStatus(...)",[383,524,525,533,538],{},[398,526,527,403,530],{},[25,528,529],{},"IsNot",[25,531,532],{},"Not",[398,534,535],{},[25,536,537],{},"\u003C>",[398,539,540],{},[25,541,542],{},"findByStatusNot(...)",[383,544,545,553,560],{},[398,546,547,403,550],{},[25,548,549],{},"LessThan",[25,551,552],{},"LessThanEqual",[398,554,555,403,557],{},[25,556,112],{},[25,558,559],{},"\u003C=",[398,561,562],{},[25,563,564],{},"findByPublishedYearLessThan(...)",[383,566,567,575,583],{},[398,568,569,403,572],{},[25,570,571],{},"GreaterThan",[25,573,574],{},"GreaterThanEqual",[398,576,577,403,580],{},[25,578,579],{},">",[25,581,582],{},">=",[398,584,585],{},"同上",[383,587,588,593,598],{},[398,589,590],{},[25,591,592],{},"Between",[398,594,595],{},[25,596,597],{},"between ? and ?",[398,599,600],{},[25,601,602],{},"findByPublishedYearBetween(...)",[383,604,605,613,622],{},[398,606,607,403,610],{},[25,608,609],{},"Like",[25,611,612],{},"NotLike",[398,614,615,618,619,432],{},[25,616,617],{},"like","（要自己带 ",[25,620,621],{},"%",[398,623,624],{},[25,625,626],{},"findByTitleLike(\"重构%\")",[383,628,629,640,646],{},[398,630,631,403,634,403,637],{},[25,632,633],{},"Containing",[25,635,636],{},"StartingWith",[25,638,639],{},"EndingWith",[398,641,642,643,645],{},"自动补 ",[25,644,621],{},"，更省事",[398,647,648],{},[25,649,650],{},"findByTitleContaining(\"重构\")",[383,652,653,661,666],{},[398,654,655,403,658],{},[25,656,657],{},"In",[25,659,660],{},"NotIn",[398,662,663],{},[25,664,665],{},"in (...)",[398,667,668],{},[25,669,670],{},"findByAuthorIn(List.of(...))",[383,672,673,681,689],{},[398,674,675,403,678],{},[25,676,677],{},"IsNull",[25,679,680],{},"IsNotNull",[398,682,683,403,686],{},[25,684,685],{},"is null",[25,687,688],{},"is not null",[398,690,691],{},[25,692,693],{},"findByAuthorIsNull()",[383,695,696,704,707],{},[398,697,698,403,701],{},[25,699,700],{},"True",[25,702,703],{},"False",[398,705,706],{},"布尔字段",[398,708,709],{},[25,710,711],{},"findByAvailableTrue()",[383,713,714,719,722],{},[398,715,716],{},[25,717,718],{},"IgnoreCase",[398,720,721],{},"忽略大小写（后缀）",[398,723,724],{},[25,725,726],{},"findByAuthorIgnoreCase(...)",[383,728,729,734,743],{},[398,730,731],{},[25,732,733],{},"OrderBy",[398,735,736,737,403,740],{},"排序，后面可跟 ",[25,738,739],{},"Asc",[25,741,742],{},"Desc",[398,744,745],{},[25,746,747],{},"...OrderByTitleAsc()",[383,749,750,755,762],{},[398,751,752],{},[25,753,754],{},"Distinct",[398,756,757,758,761],{},"去重（放在 ",[25,759,760],{},"find"," 后面）",[398,763,764],{},[25,765,766],{},"findDistinctBy...",[383,768,769,777,780],{},[398,770,771,403,774],{},[25,772,773],{},"First",[25,775,776],{},"Top",[398,778,779],{},"限制条数，可带数字",[398,781,782],{},[25,783,784],{},"findTop3By...",[13,786,787,795,796,798,799,801,802,804],{},[56,788,789,791,792,794],{},[25,790,633],{}," 和 ",[25,793,609],{}," 的区别最容易搞混","：",[25,797,633],{}," 由框架帮你补 ",[25,800,621],{},"，",[25,803,609],{}," 要求参数里\n自带通配符。用错了会查不到数据，但不报错——这类问题最费时间。",[372,806,808],{"id":807},"_13-边界在哪","1.3 边界在哪",[13,810,811,812,815,816,818],{},"派生查询适合简单条件。方法名一旦长成\n",[25,813,814],{},"findByAuthorAndStatusAndPublishedYearGreaterThanEqualAndIsbnIsNotNull","，就已经比 SQL 更难读了。\n而且它表达不了分组、聚合、多表连接、子查询。这时候换 ",[25,817,64],{},"。",[17,820,822],{"id":821},"_2-query自己写-jpql","2 @Query：自己写 JPQL",[79,824,826],{"className":81,"code":825,"language":83,"meta":84,"style":84},"public interface BookRepository extends JpaRepository\u003CBook, Long> {\n\n    @Query(\"select b from Book b where lower(b.title) like lower(concat('%', :keyword, '%'))\")\n    List\u003CBook> searchByTitle(@Param(\"keyword\") String keyword);\n\n    @Query(\"select b from Book b where b.publishedYear >= :year and b.status = :status order by b.title asc\")\n    List\u003CBook> findByYearAndStatus(@Param(\"year\") int year, @Param(\"status\") BookStatus status);\n\n    @Query(\"select count(b) from Book b where b.author = ?1\")\n    long countByAuthor(String author);\n}\n",[25,827,828,850,854,871,900,904,917,961,965,978,991],{"__ignoreMap":84},[88,829,830,832,834,836,838,840,842,844,846,848],{"class":90,"line":91},[88,831,95],{"class":94},[88,833,98],{"class":94},[88,835,102],{"class":101},[88,837,105],{"class":94},[88,839,108],{"class":101},[88,841,112],{"class":111},[88,843,115],{"class":94},[88,845,118],{"class":111},[88,847,121],{"class":94},[88,849,124],{"class":111},[88,851,852],{"class":90,"line":127},[88,853,131],{"emptyLinePlaceholder":130},[88,855,856,859,862,864,868],{"class":90,"line":134},[88,857,858],{"class":111},"    @",[88,860,861],{"class":94},"Query",[88,863,198],{"class":111},[88,865,867],{"class":866},"sZZnC","\"select b from Book b where lower(b.title) like lower(concat('%', :keyword, '%'))\"",[88,869,870],{"class":111},")\n",[88,872,873,875,877,879,882,885,888,890,893,896,898],{"class":90,"line":158},[88,874,137],{"class":111},[88,876,115],{"class":94},[88,878,142],{"class":111},[88,880,881],{"class":101},"searchByTitle",[88,883,884],{"class":111},"(@",[88,886,887],{"class":94},"Param",[88,889,198],{"class":111},[88,891,892],{"class":866},"\"keyword\"",[88,894,895],{"class":111},") String ",[88,897,228],{"class":151},[88,899,155],{"class":111},[88,901,902],{"class":90,"line":163},[88,903,131],{"emptyLinePlaceholder":130},[88,905,906,908,910,912,915],{"class":90,"line":181},[88,907,858],{"class":111},[88,909,861],{"class":94},[88,911,198],{"class":111},[88,913,914],{"class":866},"\"select b from Book b where b.publishedYear >= :year and b.status = :status order by b.title asc\"",[88,916,870],{"class":111},[88,918,919,921,923,925,928,930,932,934,937,940,942,944,947,949,951,954,957,959],{"class":90,"line":186},[88,920,137],{"class":111},[88,922,115],{"class":94},[88,924,142],{"class":111},[88,926,927],{"class":101},"findByYearAndStatus",[88,929,884],{"class":111},[88,931,887],{"class":94},[88,933,198],{"class":111},[88,935,936],{"class":866},"\"year\"",[88,938,939],{"class":111},") ",[88,941,201],{"class":94},[88,943,204],{"class":151},[88,945,946],{"class":111},", @",[88,948,887],{"class":94},[88,950,198],{"class":111},[88,952,953],{"class":866},"\"status\"",[88,955,956],{"class":111},") BookStatus ",[88,958,250],{"class":151},[88,960,155],{"class":111},[88,962,963],{"class":90,"line":209},[88,964,131],{"emptyLinePlaceholder":130},[88,966,967,969,971,973,976],{"class":90,"line":214},[88,968,858],{"class":111},[88,970,861],{"class":94},[88,972,198],{"class":111},[88,974,975],{"class":866},"\"select count(b) from Book b where b.author = ?1\"",[88,977,870],{"class":111},[88,979,980,982,985,987,989],{"class":90,"line":233},[88,981,241],{"class":94},[88,983,984],{"class":101}," countByAuthor",[88,986,148],{"class":111},[88,988,152],{"class":151},[88,990,155],{"class":111},[88,992,993],{"class":90,"line":238},[88,994,363],{"class":111},[372,996,998],{"id":997},"_21-jpql-和-sql-的两个关键差别","2.1 JPQL 和 SQL 的两个关键差别",[34,1000,1001,1026],{},[37,1002,1003,1006,1007,1010,1011,1013,1014,1017,1018,1021,1022,1025],{},[56,1004,1005],{},"JPQL 操作的是实体和字段","（",[25,1008,1009],{},"Book b where b.title = ...","），不是表和列。所以这里写 ",[25,1012,115],{},"\n和 ",[25,1015,1016],{},"title","，即使表名叫 ",[25,1019,1020],{},"books","、列名被 ",[25,1023,1024],{},"@Column"," 改过；",[37,1027,1028,1038,1039,818],{},[56,1029,1030,1031,1034,1035],{},"不能 ",[25,1032,1033],{},"select *","，要写 ",[25,1036,1037],{},"select b","。查询「所有字段」是 ",[25,1040,1041],{},"select b from Book b",[13,1043,1044,1045,1048],{},"这两条是初学时写得最多的错，报错信息通常很长但不难懂——看到 ",[25,1046,1047],{},"books is not mapped"," 之类的提示，\n先检查是不是把表名当实体名用了。",[372,1050,1052],{"id":1051},"_22-参数绑定","2.2 参数绑定",[13,1054,1055],{},"两种写法：",[377,1057,1058,1070],{},[380,1059,1060],{},[383,1061,1062,1065,1067],{},[386,1063,1064],{},"写法",[386,1066,477],{},[386,1068,1069],{},"特点",[393,1071,1072,1085],{},[383,1073,1074,1077,1082],{},[398,1075,1076],{},"位置参数",[398,1078,1079],{},[25,1080,1081],{},"where b.author = ?1",[398,1083,1084],{},"简洁，但参数一多就对不上号",[383,1086,1087,1090,1095],{},[398,1088,1089],{},"命名参数（推荐）",[398,1091,1092],{},[25,1093,1094],{},"where b.author = :author",[398,1096,1097,1098,1101],{},"配合 ",[25,1099,1100],{},"@Param(\"author\")","，可读性好，增删参数不易错",[13,1103,1104,1110,1111,1114],{},[56,1105,1106,1107],{},"命名参数建议一律加 ",[25,1108,1109],{},"@Param","。虽然编译时保留了参数名（Boot 的 parent 已开启 ",[25,1112,1113],{},"-parameters","）\n可以省略，但依赖编译配置能让代码「突然找不到参数」，显式写最稳。",[17,1116,1118],{"id":1117},"_3-返回类型怎么选","3 返回类型怎么选",[377,1120,1121,1134],{},[380,1122,1123],{},[383,1124,1125,1128,1131],{},[386,1126,1127],{},"场景",[386,1129,1130],{},"返回类型",[386,1132,1133],{},"说明",[393,1135,1136,1149,1164,1180,1197,1208],{},[383,1137,1138,1141,1146],{},[398,1139,1140],{},"固定查一条",[398,1142,1143],{},[25,1144,1145],{},"Optional\u003CBook>",[398,1147,1148],{},"查不到是正常情况时用，调用方显式处理空值",[383,1150,1151,1154,1158],{},[398,1152,1153],{},"确定存在（否则是 bug）",[398,1155,1156],{},[25,1157,115],{},[398,1159,1160,1161],{},"查不到会抛 ",[25,1162,1163],{},"EmptyResultDataAccessException",[383,1165,1166,1169,1177],{},[398,1167,1168],{},"多条",[398,1170,1171,59,1174],{},[25,1172,1173],{},"List\u003CBook>",[25,1175,1176],{},"Set\u003CBook>",[398,1178,1179],{},"最常用",[383,1181,1182,1185,1189],{},[398,1183,1184],{},"只要条数",[398,1186,1187],{},[25,1188,431],{},[398,1190,1097,1191,1193,1194],{},[25,1192,425],{}," 或 ",[25,1195,1196],{},"select count(b)",[383,1198,1199,1202,1205],{},[398,1200,1201],{},"只要部分字段",[398,1203,1204],{},"投影接口 \u002F DTO",[398,1206,1207],{},"见下",[383,1209,1210,1213,1218],{},[398,1211,1212],{},"分页",[398,1214,1215],{},[25,1216,1217],{},"Page\u003CBook>",[398,1219,1220],{},"见第 4 节",[372,1222,1224],{"id":1223},"_31-投影只要两个字段就别查整行","3.1 投影：只要两个字段就别查整行",[79,1226,1228],{"className":81,"code":1227,"language":83,"meta":84,"style":84},"package com.example.library.repository;\n\npublic interface BookSummary {\n\n    String getTitle();\n\n    String getAuthor();\n}\n",[25,1229,1230,1238,1242,1254,1258,1268,1272,1281],{"__ignoreMap":84},[88,1231,1232,1235],{"class":90,"line":91},[88,1233,1234],{"class":94},"package",[88,1236,1237],{"class":111}," com.example.library.repository;\n",[88,1239,1240],{"class":90,"line":127},[88,1241,131],{"emptyLinePlaceholder":130},[88,1243,1244,1246,1248,1251],{"class":90,"line":134},[88,1245,95],{"class":94},[88,1247,98],{"class":94},[88,1249,1250],{"class":101}," BookSummary",[88,1252,1253],{"class":111}," {\n",[88,1255,1256],{"class":90,"line":158},[88,1257,131],{"emptyLinePlaceholder":130},[88,1259,1260,1263,1266],{"class":90,"line":163},[88,1261,1262],{"class":111},"    String ",[88,1264,1265],{"class":101},"getTitle",[88,1267,357],{"class":111},[88,1269,1270],{"class":90,"line":181},[88,1271,131],{"emptyLinePlaceholder":130},[88,1273,1274,1276,1279],{"class":90,"line":186},[88,1275,1262],{"class":111},[88,1277,1278],{"class":101},"getAuthor",[88,1280,357],{"class":111},[88,1282,1283],{"class":90,"line":209},[88,1284,363],{"class":111},[79,1286,1288],{"className":81,"code":1287,"language":83,"meta":84,"style":84},"@Query(\"select b.title as title, b.author as author from Book b where b.status = :status\")\nList\u003CBookSummary> findSummaries(@Param(\"status\") BookStatus status);\n",[25,1289,1290,1304],{"__ignoreMap":84},[88,1291,1292,1295,1297,1299,1302],{"class":90,"line":91},[88,1293,1294],{"class":111},"@",[88,1296,861],{"class":94},[88,1298,198],{"class":111},[88,1300,1301],{"class":866},"\"select b.title as title, b.author as author from Book b where b.status = :status\"",[88,1303,870],{"class":111},[88,1305,1306,1309,1311,1314,1316,1319,1321,1323,1325,1327],{"class":90,"line":127},[88,1307,1308],{"class":111},"List",[88,1310,112],{"class":94},[88,1312,1313],{"class":111},"BookSummary",[88,1315,579],{"class":94},[88,1317,1318],{"class":101}," findSummaries",[88,1320,884],{"class":111},[88,1322,887],{"class":94},[88,1324,198],{"class":111},[88,1326,953],{"class":866},[88,1328,1329],{"class":111},") BookStatus status);\n",[13,1331,1332,1333,795],{},"接口式投影靠方法名匹配别名，写起来最省事。另一种是 DTO 构造表达式，",[56,1334,1335],{},"必须写全限定类名",[79,1337,1339],{"className":81,"code":1338,"language":83,"meta":84,"style":84},"package com.example.library.dto;\n\npublic record BookTitleDto(String title, String author) {\n}\n",[25,1340,1341,1348,1352,1365],{"__ignoreMap":84},[88,1342,1343,1345],{"class":90,"line":91},[88,1344,1234],{"class":94},[88,1346,1347],{"class":111}," com.example.library.dto;\n",[88,1349,1350],{"class":90,"line":127},[88,1351,131],{"emptyLinePlaceholder":130},[88,1353,1354,1356,1359,1362],{"class":90,"line":134},[88,1355,95],{"class":94},[88,1357,1358],{"class":94}," record",[88,1360,1361],{"class":101}," BookTitleDto",[88,1363,1364],{"class":111},"(String title, String author) {\n",[88,1366,1367],{"class":90,"line":158},[88,1368,363],{"class":111},[79,1370,1372],{"className":81,"code":1371,"language":83,"meta":84,"style":84},"@Query(\"\"\"\n       select new com.example.library.dto.BookTitleDto(b.title, b.author)\n       from Book b\n       where b.status = :status\n       \"\"\")\nList\u003CBookTitleDto> findDtoByStatus(@Param(\"status\") BookStatus status);\n",[25,1373,1374,1385,1390,1395,1400,1407],{"__ignoreMap":84},[88,1375,1376,1378,1380,1382],{"class":90,"line":91},[88,1377,1294],{"class":111},[88,1379,861],{"class":94},[88,1381,198],{"class":111},[88,1383,1384],{"class":866},"\"\"\"\n",[88,1386,1387],{"class":90,"line":127},[88,1388,1389],{"class":866},"       select new com.example.library.dto.BookTitleDto(b.title, b.author)\n",[88,1391,1392],{"class":90,"line":134},[88,1393,1394],{"class":866},"       from Book b\n",[88,1396,1397],{"class":90,"line":158},[88,1398,1399],{"class":866},"       where b.status = :status\n",[88,1401,1402,1405],{"class":90,"line":163},[88,1403,1404],{"class":866},"       \"\"\"",[88,1406,870],{"class":111},[88,1408,1409,1411,1413,1416,1418,1421,1423,1425,1427,1429],{"class":90,"line":181},[88,1410,1308],{"class":111},[88,1412,112],{"class":94},[88,1414,1415],{"class":111},"BookTitleDto",[88,1417,579],{"class":94},[88,1419,1420],{"class":101}," findDtoByStatus",[88,1422,884],{"class":111},[88,1424,887],{"class":94},[88,1426,198],{"class":111},[88,1428,953],{"class":866},[88,1430,1329],{"class":111},[13,1432,1433,1434,1437],{},"投影的价值不只是少查几列，还在于",[56,1435,1436],{},"避免把实体交给前端","——实体带着延迟加载的关联，序列化时\n很容易踩坑。",[17,1439,1441],{"id":1440},"_4-排序与分页","4 排序与分页",[372,1443,1445],{"id":1444},"_41-sort只排序不分页","4.1 Sort：只排序，不分页",[79,1447,1449],{"className":81,"code":1448,"language":83,"meta":84,"style":84},"\u002F\u002F bookRepository 是注入进来的 BookRepository（第 02 篇那个接口）\nList\u003CBook> books = bookRepository.findAll(Sort.by(\"publishedYear\").descending().and(Sort.by(\"title\")));\n",[25,1450,1451,1457],{"__ignoreMap":84},[88,1452,1453],{"class":90,"line":91},[88,1454,1456],{"class":1455},"sJ8bj","\u002F\u002F bookRepository 是注入进来的 BookRepository（第 02 篇那个接口）\n",[88,1458,1459,1462,1464,1467,1469,1472,1475,1478,1481,1483,1486,1489,1492,1495,1497,1499,1501,1503,1506],{"class":90,"line":127},[88,1460,1461],{"class":111},"List\u003C",[88,1463,115],{"class":94},[88,1465,1466],{"class":111},"> books ",[88,1468,517],{"class":94},[88,1470,1471],{"class":111}," bookRepository.",[88,1473,1474],{"class":101},"findAll",[88,1476,1477],{"class":111},"(Sort.",[88,1479,1480],{"class":101},"by",[88,1482,198],{"class":111},[88,1484,1485],{"class":866},"\"publishedYear\"",[88,1487,1488],{"class":111},").",[88,1490,1491],{"class":101},"descending",[88,1493,1494],{"class":111},"().",[88,1496,494],{"class":101},[88,1498,1477],{"class":111},[88,1500,1480],{"class":101},[88,1502,198],{"class":111},[88,1504,1505],{"class":866},"\"title\"",[88,1507,1508],{"class":111},")));\n",[13,1510,1511,1512,1517,1518,1521,1522,1524,1525,1528,1529,1532],{},"这里用的是 ",[56,1513,1514,1516],{},[25,1515,31],{}," 自带的方法"," ",[25,1519,1520],{},"findAll(Sort)","，返回 ",[25,1523,1308],{},"，所以接口里不用写任何声明。\n",[25,1526,1527],{},"Sort.by"," 里的名称是",[56,1530,1531],{},"实体字段名","，不是列名。",[372,1534,1536],{"id":1535},"_42-pageable-与-page","4.2 Pageable 与 Page",[79,1538,1540],{"className":81,"code":1539,"language":83,"meta":84,"style":84},"Pageable pageable = PageRequest.of(0, 10, Sort.by(\"title\").ascending());\nPage\u003CBook> page = bookRepository.findByStatus(BookStatus.AVAILABLE, pageable);\n",[25,1541,1542,1583],{"__ignoreMap":84},[88,1543,1544,1547,1549,1552,1555,1557,1561,1563,1566,1569,1571,1573,1575,1577,1580],{"class":90,"line":91},[88,1545,1546],{"class":111},"Pageable pageable ",[88,1548,517],{"class":94},[88,1550,1551],{"class":111}," PageRequest.",[88,1553,1554],{"class":101},"of",[88,1556,198],{"class":111},[88,1558,1560],{"class":1559},"sj4cs","0",[88,1562,118],{"class":111},[88,1564,1565],{"class":1559},"10",[88,1567,1568],{"class":111},", Sort.",[88,1570,1480],{"class":101},[88,1572,198],{"class":111},[88,1574,1505],{"class":866},[88,1576,1488],{"class":111},[88,1578,1579],{"class":101},"ascending",[88,1581,1582],{"class":111},"());\n",[88,1584,1585,1588,1590,1593,1595,1597,1600],{"class":90,"line":127},[88,1586,1587],{"class":111},"Page\u003C",[88,1589,115],{"class":94},[88,1591,1592],{"class":111},"> page ",[88,1594,517],{"class":94},[88,1596,1471],{"class":111},[88,1598,1599],{"class":101},"findByStatus",[88,1601,1602],{"class":111},"(BookStatus.AVAILABLE, pageable);\n",[13,1604,1605,1606,1517,1608,1611,1612,1614,1615,1618,1619,795],{},"注意 ",[25,1607,1599],{},[56,1609,1610],{},"不是自带方法","，要在 ",[25,1613,27],{}," 里自己声明——规则还是派生查询，\n只是多了一个 ",[25,1616,1617],{},"Pageable"," 参数、返回类型换成 ",[25,1620,1621],{},"Page",[79,1623,1625],{"className":81,"code":1624,"language":83,"meta":84,"style":84},"public interface BookRepository extends JpaRepository\u003CBook, Long> {\n\n    Page\u003CBook> findByStatus(BookStatus status, Pageable pageable);\n}\n",[25,1626,1627,1649,1653,1676],{"__ignoreMap":84},[88,1628,1629,1631,1633,1635,1637,1639,1641,1643,1645,1647],{"class":90,"line":91},[88,1630,95],{"class":94},[88,1632,98],{"class":94},[88,1634,102],{"class":101},[88,1636,105],{"class":94},[88,1638,108],{"class":101},[88,1640,112],{"class":111},[88,1642,115],{"class":94},[88,1644,118],{"class":111},[88,1646,121],{"class":94},[88,1648,124],{"class":111},[88,1650,1651],{"class":90,"line":127},[88,1652,131],{"emptyLinePlaceholder":130},[88,1654,1655,1658,1660,1662,1664,1666,1668,1671,1674],{"class":90,"line":134},[88,1656,1657],{"class":111},"    Page\u003C",[88,1659,115],{"class":94},[88,1661,142],{"class":111},[88,1663,1599],{"class":101},[88,1665,247],{"class":111},[88,1667,250],{"class":151},[88,1669,1670],{"class":111},", Pageable ",[88,1672,1673],{"class":151},"pageable",[88,1675,155],{"class":111},[88,1677,1678],{"class":90,"line":158},[88,1679,363],{"class":111},[13,1681,1682,1683,1686,1687,1689,1690,1692,1693,1696,1697,1699],{},"页码",[56,1684,1685],{},"从 0 开始","，第二个参数是每页条数，排序信息也装在 ",[25,1688,1617],{}," 里（所以带 ",[25,1691,1617],{}," 的方法\n里不用再写 ",[25,1694,1695],{},"order by","）。",[25,1698,1621],{}," 拿到手后常用这些：",[377,1701,1702,1712],{},[380,1703,1704],{},[383,1705,1706,1709],{},[386,1707,1708],{},"方法",[386,1710,1711],{},"含义",[393,1713,1714,1724,1734,1744,1754,1767],{},[383,1715,1716,1721],{},[398,1717,1718],{},[25,1719,1720],{},"getContent()",[398,1722,1723],{},"当前页的数据",[383,1725,1726,1731],{},[398,1727,1728],{},[25,1729,1730],{},"getTotalElements()",[398,1732,1733],{},"总条数",[383,1735,1736,1741],{},[398,1737,1738],{},[25,1739,1740],{},"getTotalPages()",[398,1742,1743],{},"总页数",[383,1745,1746,1751],{},[398,1747,1748],{},[25,1749,1750],{},"getNumber()",[398,1752,1753],{},"当前页码",[383,1755,1756,1764],{},[398,1757,1758,403,1761],{},[25,1759,1760],{},"hasNext()",[25,1762,1763],{},"hasPrevious()",[398,1765,1766],{},"有没有下一页\u002F上一页",[383,1768,1769,1774],{},[398,1770,1771],{},[25,1772,1773],{},"map(Function)",[398,1775,1776],{},"把当前页实体映射成 DTO，翻页信息保留",[79,1778,1780],{"className":81,"code":1779,"language":83,"meta":84,"style":84},"Page\u003CBookTitleDto> dtoPage = page.map(b -> new BookTitleDto(b.getTitle(), b.getAuthor()));\n",[25,1781,1782],{"__ignoreMap":84},[88,1783,1784,1786,1788,1791,1793,1796,1799,1802,1805,1808,1810,1813,1815,1818,1820],{"class":90,"line":91},[88,1785,1587],{"class":111},[88,1787,1415],{"class":94},[88,1789,1790],{"class":111},"> dtoPage ",[88,1792,517],{"class":94},[88,1794,1795],{"class":111}," page.",[88,1797,1798],{"class":101},"map",[88,1800,1801],{"class":111},"(b ",[88,1803,1804],{"class":94},"->",[88,1806,1807],{"class":94}," new",[88,1809,1361],{"class":101},[88,1811,1812],{"class":111},"(b.",[88,1814,1265],{"class":101},[88,1816,1817],{"class":111},"(), b.",[88,1819,1278],{"class":101},[88,1821,1822],{"class":111},"()));\n",[13,1824,1825,1827,1828,1831],{},[25,1826,1798],{}," 这个方法很实用：",[56,1829,1830],{},"翻页信息（总页数、页码）自动带过来","，不用自己算。",[372,1833,1835],{"id":1834},"_43-page-与-slice-的取舍","4.3 Page 与 Slice 的取舍",[377,1837,1838,1850],{},[380,1839,1840],{},[383,1841,1842,1844,1847],{},[386,1843,1130],{},[386,1845,1846],{},"额外做的事",[386,1848,1849],{},"适用场景",[393,1851,1852,1869],{},[383,1853,1854,1859,1866],{},[398,1855,1856],{},[25,1857,1858],{},"Page\u003CT>",[398,1860,1861,1862,1865],{},"额外跑一条 ",[25,1863,1864],{},"count"," 查询算总条数",[398,1867,1868],{},"需要「第 3 页 \u002F 共 12 页」",[383,1870,1871,1876,1879],{},[398,1872,1873],{},[25,1874,1875],{},"Slice\u003CT>",[398,1877,1878],{},"不多查，靠「多取一条」判断还有没有下一页",[398,1880,1881],{},"无限滚动、只要上下页",[13,1883,1884,1887,1888,1891],{},[56,1885,1886],{},"总数查询不便宜","。数据量大又不需要精确总页数时，",[25,1889,1890],{},"Slice"," 能省掉一次全表 count。",[13,1893,1894,1895,1898,1899,1902,1903,1905],{},"分页参数直接来自请求时，别自己从 ",[25,1896,1897],{},"page","\u002F",[25,1900,1901],{},"size"," 字符串拼，Spring 的 ",[25,1904,1617],{}," 参数可以被 MVC\n自动装配——这种写法的好处与风险见 4.4 节。",[372,1907,1909],{"id":1908},"_44-分页在三层里各写什么","4.4 分页在三层里各写什么",[13,1911,1912],{},"前面几段都站在数据层看，容易看不出全貌。把同一件事按 Repository → Service → 展示层摊开，\n是这样一条链（展示层有页面型和 REST 型两种写法，各写一段）。",[13,1914,1915],{},[56,1916,1917],{},"① Repository：只声明方法，实现由框架生成",[79,1919,1921],{"className":81,"code":1920,"language":83,"meta":84,"style":84},"public interface BookRepository extends JpaRepository\u003CBook, Long> {\n\n    \u002F\u002F 派生查询 + 分页：关键词模糊匹配，排序交给 Pageable\n    Page\u003CBook> findByTitleContainingIgnoreCase(String keyword, Pageable pageable);\n\n    \u002F\u002F JPQL + 分页：返回 DTO 投影\n    @Query(\"\"\"\n           select new com.example.library.dto.BookTitleDto(b.title, b.author)\n           from Book b\n           where (:keyword is null or lower(b.title) like lower(concat('%', :keyword, '%')))\n           \"\"\")\n    Page\u003CBookTitleDto> searchTitlesAsDto(@Param(\"keyword\") String keyword, Pageable pageable);\n}\n",[25,1922,1923,1945,1949,1954,1975,1979,1984,1994,1999,2004,2009,2016,2045],{"__ignoreMap":84},[88,1924,1925,1927,1929,1931,1933,1935,1937,1939,1941,1943],{"class":90,"line":91},[88,1926,95],{"class":94},[88,1928,98],{"class":94},[88,1930,102],{"class":101},[88,1932,105],{"class":94},[88,1934,108],{"class":101},[88,1936,112],{"class":111},[88,1938,115],{"class":94},[88,1940,118],{"class":111},[88,1942,121],{"class":94},[88,1944,124],{"class":111},[88,1946,1947],{"class":90,"line":127},[88,1948,131],{"emptyLinePlaceholder":130},[88,1950,1951],{"class":90,"line":134},[88,1952,1953],{"class":1455},"    \u002F\u002F 派生查询 + 分页：关键词模糊匹配，排序交给 Pageable\n",[88,1955,1956,1958,1960,1962,1965,1967,1969,1971,1973],{"class":90,"line":158},[88,1957,1657],{"class":111},[88,1959,115],{"class":94},[88,1961,142],{"class":111},[88,1963,1964],{"class":101},"findByTitleContainingIgnoreCase",[88,1966,148],{"class":111},[88,1968,228],{"class":151},[88,1970,1670],{"class":111},[88,1972,1673],{"class":151},[88,1974,155],{"class":111},[88,1976,1977],{"class":90,"line":163},[88,1978,131],{"emptyLinePlaceholder":130},[88,1980,1981],{"class":90,"line":181},[88,1982,1983],{"class":1455},"    \u002F\u002F JPQL + 分页：返回 DTO 投影\n",[88,1985,1986,1988,1990,1992],{"class":90,"line":186},[88,1987,858],{"class":111},[88,1989,861],{"class":94},[88,1991,198],{"class":111},[88,1993,1384],{"class":866},[88,1995,1996],{"class":90,"line":209},[88,1997,1998],{"class":866},"           select new com.example.library.dto.BookTitleDto(b.title, b.author)\n",[88,2000,2001],{"class":90,"line":214},[88,2002,2003],{"class":866},"           from Book b\n",[88,2005,2006],{"class":90,"line":233},[88,2007,2008],{"class":866},"           where (:keyword is null or lower(b.title) like lower(concat('%', :keyword, '%')))\n",[88,2010,2011,2014],{"class":90,"line":238},[88,2012,2013],{"class":866},"           \"\"\"",[88,2015,870],{"class":111},[88,2017,2018,2020,2022,2024,2027,2029,2031,2033,2035,2037,2039,2041,2043],{"class":90,"line":255},[88,2019,1657],{"class":111},[88,2021,1415],{"class":94},[88,2023,142],{"class":111},[88,2025,2026],{"class":101},"searchTitlesAsDto",[88,2028,884],{"class":111},[88,2030,887],{"class":94},[88,2032,198],{"class":111},[88,2034,892],{"class":866},[88,2036,895],{"class":111},[88,2038,228],{"class":151},[88,2040,1670],{"class":111},[88,2042,1673],{"class":151},[88,2044,155],{"class":111},[88,2046,2047],{"class":90,"line":260},[88,2048,363],{"class":111},[13,2050,2051],{},[56,2052,2053,2054,2056],{},"② Service：透传 ",[25,2055,1617],{},"，查询发生在事务里",[79,2058,2060],{"className":81,"code":2059,"language":83,"meta":84,"style":84},"@Service\n@Transactional(readOnly = true)\npublic class BookServiceImpl implements BookService {\n\n    private final BookRepository bookRepository;\n\n    public BookServiceImpl(BookRepository bookRepository) {\n        this.bookRepository = bookRepository;\n    }\n\n    @Override\n    public Page\u003CBook> search(String keyword, Pageable pageable) {\n        if (keyword == null || keyword.isBlank()) {\n            return bookRepository.findAll(pageable);          \u002F\u002F 自带方法同样接受 Pageable\n        }\n        return bookRepository.findByTitleContainingIgnoreCase(keyword, pageable);\n    }\n\n    @Override\n    public Page\u003CBookTitleDto> searchTitles(String keyword, Pageable pageable) {\n        \u002F\u002F 走 JPQL 的 DTO 投影，REST 接口用这一版，避免把实体交给前端\n        return bookRepository.searchTitlesAsDto(keyword, pageable);\n    }\n\n    \u002F\u002F 其余方法见第 02、06 篇\n}\n",[25,2061,2062,2069,2089,2107,2111,2122,2126,2142,2155,2160,2164,2171,2195,2221,2236,2241,2253,2257,2261,2267,2290,2296,2307,2312,2317,2323],{"__ignoreMap":84},[88,2063,2064,2066],{"class":90,"line":91},[88,2065,1294],{"class":111},[88,2067,2068],{"class":94},"Service\n",[88,2070,2071,2073,2076,2078,2081,2084,2087],{"class":90,"line":127},[88,2072,1294],{"class":111},[88,2074,2075],{"class":94},"Transactional",[88,2077,198],{"class":111},[88,2079,2080],{"class":1559},"readOnly",[88,2082,2083],{"class":94}," =",[88,2085,2086],{"class":1559}," true",[88,2088,870],{"class":111},[88,2090,2091,2093,2096,2099,2102,2105],{"class":90,"line":134},[88,2092,95],{"class":94},[88,2094,2095],{"class":94}," class",[88,2097,2098],{"class":101}," BookServiceImpl",[88,2100,2101],{"class":94}," implements",[88,2103,2104],{"class":101}," BookService",[88,2106,1253],{"class":111},[88,2108,2109],{"class":90,"line":158},[88,2110,131],{"emptyLinePlaceholder":130},[88,2112,2113,2116,2119],{"class":90,"line":163},[88,2114,2115],{"class":94},"    private",[88,2117,2118],{"class":94}," final",[88,2120,2121],{"class":111}," BookRepository bookRepository;\n",[88,2123,2124],{"class":90,"line":181},[88,2125,131],{"emptyLinePlaceholder":130},[88,2127,2128,2131,2133,2136,2139],{"class":90,"line":186},[88,2129,2130],{"class":94},"    public",[88,2132,2098],{"class":101},[88,2134,2135],{"class":111},"(BookRepository ",[88,2137,2138],{"class":151},"bookRepository",[88,2140,2141],{"class":111},") {\n",[88,2143,2144,2147,2150,2152],{"class":90,"line":209},[88,2145,2146],{"class":1559},"        this",[88,2148,2149],{"class":111},".bookRepository ",[88,2151,517],{"class":94},[88,2153,2154],{"class":111}," bookRepository;\n",[88,2156,2157],{"class":90,"line":214},[88,2158,2159],{"class":111},"    }\n",[88,2161,2162],{"class":90,"line":233},[88,2163,131],{"emptyLinePlaceholder":130},[88,2165,2166,2168],{"class":90,"line":238},[88,2167,858],{"class":111},[88,2169,2170],{"class":94},"Override\n",[88,2172,2173,2175,2178,2180,2182,2185,2187,2189,2191,2193],{"class":90,"line":255},[88,2174,2130],{"class":94},[88,2176,2177],{"class":111}," Page\u003C",[88,2179,115],{"class":94},[88,2181,142],{"class":111},[88,2183,2184],{"class":101},"search",[88,2186,148],{"class":111},[88,2188,228],{"class":151},[88,2190,1670],{"class":111},[88,2192,1673],{"class":151},[88,2194,2141],{"class":111},[88,2196,2197,2200,2203,2206,2209,2212,2215,2218],{"class":90,"line":260},[88,2198,2199],{"class":94},"        if",[88,2201,2202],{"class":111}," (keyword ",[88,2204,2205],{"class":94},"==",[88,2207,2208],{"class":1559}," null",[88,2210,2211],{"class":94}," ||",[88,2213,2214],{"class":111}," keyword.",[88,2216,2217],{"class":101},"isBlank",[88,2219,2220],{"class":111},"()) {\n",[88,2222,2223,2226,2228,2230,2233],{"class":90,"line":276},[88,2224,2225],{"class":94},"            return",[88,2227,1471],{"class":111},[88,2229,1474],{"class":101},[88,2231,2232],{"class":111},"(pageable);          ",[88,2234,2235],{"class":1455},"\u002F\u002F 自带方法同样接受 Pageable\n",[88,2237,2238],{"class":90,"line":281},[88,2239,2240],{"class":111},"        }\n",[88,2242,2243,2246,2248,2250],{"class":90,"line":306},[88,2244,2245],{"class":94},"        return",[88,2247,1471],{"class":111},[88,2249,1964],{"class":101},[88,2251,2252],{"class":111},"(keyword, pageable);\n",[88,2254,2255],{"class":90,"line":311},[88,2256,2159],{"class":111},[88,2258,2259],{"class":90,"line":339},[88,2260,131],{"emptyLinePlaceholder":130},[88,2262,2263,2265],{"class":90,"line":344},[88,2264,858],{"class":111},[88,2266,2170],{"class":94},[88,2268,2269,2271,2273,2275,2277,2280,2282,2284,2286,2288],{"class":90,"line":360},[88,2270,2130],{"class":94},[88,2272,2177],{"class":111},[88,2274,1415],{"class":94},[88,2276,142],{"class":111},[88,2278,2279],{"class":101},"searchTitles",[88,2281,148],{"class":111},[88,2283,228],{"class":151},[88,2285,1670],{"class":111},[88,2287,1673],{"class":151},[88,2289,2141],{"class":111},[88,2291,2293],{"class":90,"line":2292},21,[88,2294,2295],{"class":1455},"        \u002F\u002F 走 JPQL 的 DTO 投影，REST 接口用这一版，避免把实体交给前端\n",[88,2297,2299,2301,2303,2305],{"class":90,"line":2298},22,[88,2300,2245],{"class":94},[88,2302,1471],{"class":111},[88,2304,2026],{"class":101},[88,2306,2252],{"class":111},[88,2308,2310],{"class":90,"line":2309},23,[88,2311,2159],{"class":111},[88,2313,2315],{"class":90,"line":2314},24,[88,2316,131],{"emptyLinePlaceholder":130},[88,2318,2320],{"class":90,"line":2319},25,[88,2321,2322],{"class":1455},"    \u002F\u002F 其余方法见第 02、06 篇\n",[88,2324,2326],{"class":90,"line":2325},26,[88,2327,363],{"class":111},[13,2329,2330,2331,2334,2335,2337,2338,1696],{},"Service ",[56,2332,2333],{},"不决定页码和每页条数","——那是展示层的选择；它只负责把参数传给仓储，并保证查询在事务\n里完成（",[25,2336,1621],{}," 里的实体可能带着延迟加载的关联，出了事务再访问就会抛\n",[25,2339,2340],{},"LazyInitializationException",[13,2342,2343,2344,2347],{},"上面两个方法对应接口里的两行声明（",[25,2345,2346],{},"BookService"," 在 01 篇定义，这里把分页相关的补进去）：",[79,2349,2351],{"className":81,"code":2350,"language":83,"meta":84,"style":84},"public interface BookService {\n\n    Page\u003CBook> search(String keyword, Pageable pageable);\n\n    Page\u003CBookTitleDto> searchTitles(String keyword, Pageable pageable);\n}\n",[25,2352,2353,2363,2367,2387,2391,2411],{"__ignoreMap":84},[88,2354,2355,2357,2359,2361],{"class":90,"line":91},[88,2356,95],{"class":94},[88,2358,98],{"class":94},[88,2360,2104],{"class":101},[88,2362,1253],{"class":111},[88,2364,2365],{"class":90,"line":127},[88,2366,131],{"emptyLinePlaceholder":130},[88,2368,2369,2371,2373,2375,2377,2379,2381,2383,2385],{"class":90,"line":134},[88,2370,1657],{"class":111},[88,2372,115],{"class":94},[88,2374,142],{"class":111},[88,2376,2184],{"class":101},[88,2378,148],{"class":111},[88,2380,228],{"class":151},[88,2382,1670],{"class":111},[88,2384,1673],{"class":151},[88,2386,155],{"class":111},[88,2388,2389],{"class":90,"line":158},[88,2390,131],{"emptyLinePlaceholder":130},[88,2392,2393,2395,2397,2399,2401,2403,2405,2407,2409],{"class":90,"line":163},[88,2394,1657],{"class":111},[88,2396,1415],{"class":94},[88,2398,142],{"class":111},[88,2400,2279],{"class":101},[88,2402,148],{"class":111},[88,2404,228],{"class":151},[88,2406,1670],{"class":111},[88,2408,1673],{"class":151},[88,2410,155],{"class":111},[88,2412,2413],{"class":90,"line":181},[88,2414,363],{"class":111},[13,2416,2417],{},[56,2418,2419,2420],{},"③ Controller（页面型）：把「第几页」翻译成 ",[25,2421,1617],{},[79,2423,2425],{"className":81,"code":2424,"language":83,"meta":84,"style":84},"@Controller\n@RequestMapping(\"\u002Fbooks\")\npublic class BookViewController {\n\n    private final BookService bookService;\n\n    public BookViewController(BookService bookService) {\n        this.bookService = bookService;\n    }\n\n    @GetMapping\n    public String list(@RequestParam(name = \"keyword\", required = false) String keyword,\n                       @RequestParam(defaultValue = \"0\") int page,\n                       Model model) {\n        Pageable pageable = PageRequest.of(Math.max(page, 0), 10, Sort.by(\"title\").ascending());\n        model.addAttribute(\"page\", bookService.search(keyword, pageable));\n        model.addAttribute(\"keyword\", keyword);\n        return \"books\u002Flist\";\n    }\n}\n",[25,2426,2427,2434,2448,2459,2463,2472,2476,2490,2502,2506,2510,2517,2559,2585,2595,2636,2657,2670,2680,2684],{"__ignoreMap":84},[88,2428,2429,2431],{"class":90,"line":91},[88,2430,1294],{"class":111},[88,2432,2433],{"class":94},"Controller\n",[88,2435,2436,2438,2441,2443,2446],{"class":90,"line":127},[88,2437,1294],{"class":111},[88,2439,2440],{"class":94},"RequestMapping",[88,2442,198],{"class":111},[88,2444,2445],{"class":866},"\"\u002Fbooks\"",[88,2447,870],{"class":111},[88,2449,2450,2452,2454,2457],{"class":90,"line":134},[88,2451,95],{"class":94},[88,2453,2095],{"class":94},[88,2455,2456],{"class":101}," BookViewController",[88,2458,1253],{"class":111},[88,2460,2461],{"class":90,"line":158},[88,2462,131],{"emptyLinePlaceholder":130},[88,2464,2465,2467,2469],{"class":90,"line":163},[88,2466,2115],{"class":94},[88,2468,2118],{"class":94},[88,2470,2471],{"class":111}," BookService bookService;\n",[88,2473,2474],{"class":90,"line":181},[88,2475,131],{"emptyLinePlaceholder":130},[88,2477,2478,2480,2482,2485,2488],{"class":90,"line":186},[88,2479,2130],{"class":94},[88,2481,2456],{"class":101},[88,2483,2484],{"class":111},"(BookService ",[88,2486,2487],{"class":151},"bookService",[88,2489,2141],{"class":111},[88,2491,2492,2494,2497,2499],{"class":90,"line":209},[88,2493,2146],{"class":1559},[88,2495,2496],{"class":111},".bookService ",[88,2498,517],{"class":94},[88,2500,2501],{"class":111}," bookService;\n",[88,2503,2504],{"class":90,"line":214},[88,2505,2159],{"class":111},[88,2507,2508],{"class":90,"line":233},[88,2509,131],{"emptyLinePlaceholder":130},[88,2511,2512,2514],{"class":90,"line":238},[88,2513,858],{"class":111},[88,2515,2516],{"class":94},"GetMapping\n",[88,2518,2519,2521,2524,2527,2529,2532,2534,2537,2539,2542,2544,2547,2549,2552,2554,2556],{"class":90,"line":255},[88,2520,2130],{"class":94},[88,2522,2523],{"class":111}," String ",[88,2525,2526],{"class":101},"list",[88,2528,884],{"class":111},[88,2530,2531],{"class":94},"RequestParam",[88,2533,198],{"class":111},[88,2535,2536],{"class":1559},"name",[88,2538,2083],{"class":94},[88,2540,2541],{"class":866}," \"keyword\"",[88,2543,118],{"class":111},[88,2545,2546],{"class":1559},"required",[88,2548,2083],{"class":94},[88,2550,2551],{"class":1559}," false",[88,2553,895],{"class":111},[88,2555,228],{"class":151},[88,2557,2558],{"class":111},",\n",[88,2560,2561,2564,2566,2568,2571,2573,2576,2578,2580,2583],{"class":90,"line":260},[88,2562,2563],{"class":111},"                       @",[88,2565,2531],{"class":94},[88,2567,198],{"class":111},[88,2569,2570],{"class":1559},"defaultValue",[88,2572,2083],{"class":94},[88,2574,2575],{"class":866}," \"0\"",[88,2577,939],{"class":111},[88,2579,201],{"class":94},[88,2581,2582],{"class":151}," page",[88,2584,2558],{"class":111},[88,2586,2587,2590,2593],{"class":90,"line":276},[88,2588,2589],{"class":111},"                       Model ",[88,2591,2592],{"class":151},"model",[88,2594,2141],{"class":111},[88,2596,2597,2600,2602,2604,2606,2609,2612,2615,2617,2620,2622,2624,2626,2628,2630,2632,2634],{"class":90,"line":281},[88,2598,2599],{"class":111},"        Pageable pageable ",[88,2601,517],{"class":94},[88,2603,1551],{"class":111},[88,2605,1554],{"class":101},[88,2607,2608],{"class":111},"(Math.",[88,2610,2611],{"class":101},"max",[88,2613,2614],{"class":111},"(page, ",[88,2616,1560],{"class":1559},[88,2618,2619],{"class":111},"), ",[88,2621,1565],{"class":1559},[88,2623,1568],{"class":111},[88,2625,1480],{"class":101},[88,2627,198],{"class":111},[88,2629,1505],{"class":866},[88,2631,1488],{"class":111},[88,2633,1579],{"class":101},[88,2635,1582],{"class":111},[88,2637,2638,2641,2644,2646,2649,2652,2654],{"class":90,"line":306},[88,2639,2640],{"class":111},"        model.",[88,2642,2643],{"class":101},"addAttribute",[88,2645,198],{"class":111},[88,2647,2648],{"class":866},"\"page\"",[88,2650,2651],{"class":111},", bookService.",[88,2653,2184],{"class":101},[88,2655,2656],{"class":111},"(keyword, pageable));\n",[88,2658,2659,2661,2663,2665,2667],{"class":90,"line":311},[88,2660,2640],{"class":111},[88,2662,2643],{"class":101},[88,2664,198],{"class":111},[88,2666,892],{"class":866},[88,2668,2669],{"class":111},", keyword);\n",[88,2671,2672,2674,2677],{"class":90,"line":339},[88,2673,2245],{"class":94},[88,2675,2676],{"class":866}," \"books\u002Flist\"",[88,2678,2679],{"class":111},";\n",[88,2681,2682],{"class":90,"line":344},[88,2683,2159],{"class":111},[88,2685,2686],{"class":90,"line":360},[88,2687,363],{"class":111},[13,2689,2690,2693,2694,818],{},[56,2691,2692],{},"「每页 10 条、按书名排」这条规则写在这里","，客户端只能改页码和关键词，改不了条数与排序字段。\nURL 形如 ",[25,2695,2696],{},"\u002Fbooks?keyword=重构&page=1",[13,2698,2699],{},[56,2700,2701,2702],{},"④ Controller（REST 型）：让框架装配 ",[25,2703,1617],{},[79,2705,2707],{"className":81,"code":2706,"language":83,"meta":84,"style":84},"@RestController\n@RequestMapping(\"\u002Fapi\u002Fbooks\")\npublic class BookController {\n\n    @GetMapping\n    public Page\u003CBookTitleDto> list(@RequestParam(name = \"keyword\", required = false) String keyword,\n                                   Pageable pageable) {\n        return bookService.searchTitles(keyword, pageable);\n    }\n}\n",[25,2708,2709,2716,2729,2740,2744,2750,2788,2797,2808,2812],{"__ignoreMap":84},[88,2710,2711,2713],{"class":90,"line":91},[88,2712,1294],{"class":111},[88,2714,2715],{"class":94},"RestController\n",[88,2717,2718,2720,2722,2724,2727],{"class":90,"line":127},[88,2719,1294],{"class":111},[88,2721,2440],{"class":94},[88,2723,198],{"class":111},[88,2725,2726],{"class":866},"\"\u002Fapi\u002Fbooks\"",[88,2728,870],{"class":111},[88,2730,2731,2733,2735,2738],{"class":90,"line":134},[88,2732,95],{"class":94},[88,2734,2095],{"class":94},[88,2736,2737],{"class":101}," BookController",[88,2739,1253],{"class":111},[88,2741,2742],{"class":90,"line":158},[88,2743,131],{"emptyLinePlaceholder":130},[88,2745,2746,2748],{"class":90,"line":163},[88,2747,858],{"class":111},[88,2749,2516],{"class":94},[88,2751,2752,2754,2756,2758,2760,2762,2764,2766,2768,2770,2772,2774,2776,2778,2780,2782,2784,2786],{"class":90,"line":181},[88,2753,2130],{"class":94},[88,2755,2177],{"class":111},[88,2757,1415],{"class":94},[88,2759,142],{"class":111},[88,2761,2526],{"class":101},[88,2763,884],{"class":111},[88,2765,2531],{"class":94},[88,2767,198],{"class":111},[88,2769,2536],{"class":1559},[88,2771,2083],{"class":94},[88,2773,2541],{"class":866},[88,2775,118],{"class":111},[88,2777,2546],{"class":1559},[88,2779,2083],{"class":94},[88,2781,2551],{"class":1559},[88,2783,895],{"class":111},[88,2785,228],{"class":151},[88,2787,2558],{"class":111},[88,2789,2790,2793,2795],{"class":90,"line":186},[88,2791,2792],{"class":111},"                                   Pageable ",[88,2794,1673],{"class":151},[88,2796,2141],{"class":111},[88,2798,2799,2801,2804,2806],{"class":90,"line":209},[88,2800,2245],{"class":94},[88,2802,2803],{"class":111}," bookService.",[88,2805,2279],{"class":101},[88,2807,2252],{"class":111},[88,2809,2810],{"class":90,"line":214},[88,2811,2159],{"class":111},[88,2813,2814],{"class":90,"line":233},[88,2815,363],{"class":111},[13,2817,2818,2819,2821,2822,2825,2826,2829,2830,2833,2834,2837],{},"参数类型写成 ",[25,2820,1617],{},"，Spring Data 的 ",[25,2823,2824],{},"PageableHandlerMethodArgumentResolver"," 会从查询串装配\n它：",[25,2827,2828],{},"\u002Fapi\u002Fbooks?page=1&size=20&sort=title,desc","。方便，但",[56,2831,2832],{},"每页条数与排序字段都变成客户端可控","，\n必须配两道闸（",[25,2835,2836],{},"spring.data.web.pageable.max-page-size"," 与排序字段白名单，第 12 篇）。",[13,2839,2840],{},[56,2841,2842],{},"⑤ 消费结果：模板或 JSON",[79,2844,2848],{"className":2845,"code":2846,"language":2847,"meta":84,"style":84},"language-html shiki shiki-themes github-light github-dark","\u003Ctr th:each=\"book, stat : ${page.content}\">\n    \u003Ctd th:text=\"${stat.count}\">1\u003C\u002Ftd>\n","html",[25,2849,2850,2868],{"__ignoreMap":84},[88,2851,2852,2854,2857,2860,2862,2865],{"class":90,"line":91},[88,2853,112],{"class":111},[88,2855,383],{"class":2856},"s9eBZ",[88,2858,2859],{"class":101}," th:each",[88,2861,517],{"class":111},[88,2863,2864],{"class":866},"\"book, stat : ${page.content}\"",[88,2866,2867],{"class":111},">\n",[88,2869,2870,2873,2875,2878,2880,2883,2886,2888],{"class":90,"line":127},[88,2871,2872],{"class":111},"    \u003C",[88,2874,398],{"class":2856},[88,2876,2877],{"class":101}," th:text",[88,2879,517],{"class":111},[88,2881,2882],{"class":866},"\"${stat.count}\"",[88,2884,2885],{"class":111},">1\u003C\u002F",[88,2887,398],{"class":2856},[88,2889,2867],{"class":111},[79,2891,2893],{"className":2845,"code":2892,"language":2847,"meta":84,"style":84},"\u003Cdiv th:if=\"${page.totalPages > 1}\">\n    \u003Ca th:if=\"${page.hasPrevious()}\"\n       th:href=\"@{\u002Fbooks(page=${page.number - 1}, keyword=${keyword})}\">上一页\u003C\u002Fa>\n    \u003Cspan th:text=\"|${page.number + 1} \u002F ${page.totalPages}|\">1 \u002F 3\u003C\u002Fspan>\n    \u003Ca th:if=\"${page.hasNext()}\"\n       th:href=\"@{\u002Fbooks(page=${page.number + 1}, keyword=${keyword})}\">下一页\u003C\u002Fa>\n\u003C\u002Fdiv>\n",[25,2894,2895,2912,2926,2943,2963,2976,2992],{"__ignoreMap":84},[88,2896,2897,2899,2902,2905,2907,2910],{"class":90,"line":91},[88,2898,112],{"class":111},[88,2900,2901],{"class":2856},"div",[88,2903,2904],{"class":101}," th:if",[88,2906,517],{"class":111},[88,2908,2909],{"class":866},"\"${page.totalPages > 1}\"",[88,2911,2867],{"class":111},[88,2913,2914,2916,2919,2921,2923],{"class":90,"line":127},[88,2915,2872],{"class":111},[88,2917,2918],{"class":2856},"a",[88,2920,2904],{"class":101},[88,2922,517],{"class":111},[88,2924,2925],{"class":866},"\"${page.hasPrevious()}\"\n",[88,2927,2928,2931,2933,2936,2939,2941],{"class":90,"line":134},[88,2929,2930],{"class":101},"       th:href",[88,2932,517],{"class":111},[88,2934,2935],{"class":866},"\"@{\u002Fbooks(page=${page.number - 1}, keyword=${keyword})}\"",[88,2937,2938],{"class":111},">上一页\u003C\u002F",[88,2940,2918],{"class":2856},[88,2942,2867],{"class":111},[88,2944,2945,2947,2949,2951,2953,2956,2959,2961],{"class":90,"line":158},[88,2946,2872],{"class":111},[88,2948,88],{"class":2856},[88,2950,2877],{"class":101},[88,2952,517],{"class":111},[88,2954,2955],{"class":866},"\"|${page.number + 1} \u002F ${page.totalPages}|\"",[88,2957,2958],{"class":111},">1 \u002F 3\u003C\u002F",[88,2960,88],{"class":2856},[88,2962,2867],{"class":111},[88,2964,2965,2967,2969,2971,2973],{"class":90,"line":163},[88,2966,2872],{"class":111},[88,2968,2918],{"class":2856},[88,2970,2904],{"class":101},[88,2972,517],{"class":111},[88,2974,2975],{"class":866},"\"${page.hasNext()}\"\n",[88,2977,2978,2980,2982,2985,2988,2990],{"class":90,"line":181},[88,2979,2930],{"class":101},[88,2981,517],{"class":111},[88,2983,2984],{"class":866},"\"@{\u002Fbooks(page=${page.number + 1}, keyword=${keyword})}\"",[88,2986,2987],{"class":111},">下一页\u003C\u002F",[88,2989,2918],{"class":2856},[88,2991,2867],{"class":111},[88,2993,2994,2997,2999],{"class":90,"line":186},[88,2995,2996],{"class":111},"\u003C\u002F",[88,2998,2901],{"class":2856},[88,3000,2867],{"class":111},[13,3002,3003,3004,3006,3007,403,3010,403,3013,3016],{},"返回给接口客户端时，",[25,3005,1621],{}," 会被序列化成 ",[25,3008,3009],{},"content",[25,3011,3012],{},"totalElements",[25,3014,3015],{},"totalPages"," 这些字段\n（真实响应样例在第 12 篇的接口约定一节）。",[13,3018,3019,3020,3033],{},"一句话总结这条链：",[56,3021,3022,3023,3025,3026,3028,3029,3032],{},"Repository 声明「带 ",[25,3024,1617],{},"、返回 ",[25,3027,1621],{},"」的方法 → Service 透传并保证\n事务 → Controller 决定每页多少、怎么排 → 模板\u002FJSON 用 ",[25,3030,3031],{},"page.content"," 与翻页信息渲染。","\n页面型控制器的完整代码在第 05 篇，REST 版在第 11 篇。",[17,3035,3037],{"id":3036},"_5-modifying更新与删除","5 @Modifying：更新与删除",[13,3039,3040,3042,3043,403,3046,3049,3050,795],{},[25,3041,64],{}," 默认只读。要写 ",[25,3044,3045],{},"update",[25,3047,3048],{},"delete","，得加 ",[25,3051,3052],{},"@Modifying",[79,3054,3056],{"className":81,"code":3055,"language":83,"meta":84,"style":84},"@Modifying\n@Query(\"update Book b set b.status = :status where b.id = :id\")\nint markStatus(@Param(\"id\") Long id, @Param(\"status\") BookStatus status);\n",[25,3057,3058,3065,3078],{"__ignoreMap":84},[88,3059,3060,3062],{"class":90,"line":91},[88,3061,1294],{"class":111},[88,3063,3064],{"class":94},"Modifying\n",[88,3066,3067,3069,3071,3073,3076],{"class":90,"line":127},[88,3068,1294],{"class":111},[88,3070,861],{"class":94},[88,3072,198],{"class":111},[88,3074,3075],{"class":866},"\"update Book b set b.status = :status where b.id = :id\"",[88,3077,870],{"class":111},[88,3079,3080,3082,3085,3087,3089,3091,3094,3097,3099,3101,3103],{"class":90,"line":134},[88,3081,201],{"class":94},[88,3083,3084],{"class":101}," markStatus",[88,3086,884],{"class":111},[88,3088,887],{"class":94},[88,3090,198],{"class":111},[88,3092,3093],{"class":866},"\"id\"",[88,3095,3096],{"class":111},") Long id, @",[88,3098,887],{"class":94},[88,3100,198],{"class":111},[88,3102,953],{"class":866},[88,3104,1329],{"class":111},[13,3106,3107],{},"两个必须注意的点，都是在真实测试里撞出来的：",[3109,3110,3111,3133],"ol",{},[37,3112,3113,3116,3117,3120,3121,3124,3125,3128,3129,3132],{},[56,3114,3115],{},"它必须在事务里调用","。少了事务会直接抛\n",[25,3118,3119],{},"TransactionRequiredException: No EntityManager with actual transaction available","。\n典型场景是在测试方法里直接调仓储：",[25,3122,3123],{},"@DataJpaTest"," 默认带事务所以没事，而 ",[25,3126,3127],{},"@SpringBootTest","\n的测试方法没有事务，同样的调用就会失败；生产代码里要把 ",[25,3130,3131],{},"@Transactional"," 加在调用它的\nService 方法上（第 10 篇展开）；",[37,3134,3135,3143],{},[56,3136,3137,403,3140],{},[25,3138,3139],{},"clearAutomatically",[25,3141,3142],{},"flushAutomatically","：这类更新会绕过持久化上下文，同一个事务里\n再查同一批数据可能拿到旧值。",[79,3145,3147],{"className":81,"code":3146,"language":83,"meta":84,"style":84},"@Modifying(clearAutomatically = true, flushAutomatically = true)\n",[25,3148,3149],{"__ignoreMap":84},[88,3150,3151,3153,3156,3158,3160,3162,3164,3166,3168,3170,3172],{"class":90,"line":91},[88,3152,1294],{"class":111},[88,3154,3155],{"class":94},"Modifying",[88,3157,198],{"class":111},[88,3159,3139],{"class":1559},[88,3161,2083],{"class":94},[88,3163,2086],{"class":1559},[88,3165,118],{"class":111},[88,3167,3142],{"class":1559},[88,3169,2083],{"class":94},[88,3171,2086],{"class":1559},[88,3173,870],{"class":111},[13,3175,3176,3178],{},[25,3177,452],{}," 这类派生删除方法同理。",[17,3180,3182],{"id":3181},"_6-什么时候才用原生-sql","6 什么时候才用原生 SQL",[79,3184,3186],{"className":81,"code":3185,"language":83,"meta":84,"style":84},"@Query(value = \"select * from books where author = :author\", nativeQuery = true)\nList\u003CBook> findByAuthorNative(@Param(\"author\") String author);\n",[25,3187,3188,3215],{"__ignoreMap":84},[88,3189,3190,3192,3194,3196,3199,3201,3204,3206,3209,3211,3213],{"class":90,"line":91},[88,3191,1294],{"class":111},[88,3193,861],{"class":94},[88,3195,198],{"class":111},[88,3197,3198],{"class":1559},"value",[88,3200,2083],{"class":94},[88,3202,3203],{"class":866}," \"select * from books where author = :author\"",[88,3205,118],{"class":111},[88,3207,3208],{"class":1559},"nativeQuery",[88,3210,2083],{"class":94},[88,3212,2086],{"class":1559},[88,3214,870],{"class":111},[88,3216,3217,3219,3221,3223,3225,3228,3230,3232,3234,3237],{"class":90,"line":127},[88,3218,1308],{"class":111},[88,3220,112],{"class":94},[88,3222,115],{"class":111},[88,3224,579],{"class":94},[88,3226,3227],{"class":101}," findByAuthorNative",[88,3229,884],{"class":111},[88,3231,887],{"class":94},[88,3233,198],{"class":111},[88,3235,3236],{"class":866},"\"author\"",[88,3238,3239],{"class":111},") String author);\n",[13,3241,3242,3245,3246,3249,3250,59,3252,3254],{},[25,3243,3244],{},"nativeQuery = true"," 之后写的就是数据库方言的 SQL，",[56,3247,3248],{},"表名、列名都要用数据库里的真实名字","\n（上面这段里 ",[25,3251,1020],{},[25,3253,152],{}," 都是表\u002F列名，而不是属性路径）。它适合：",[34,3256,3257,3268],{},[37,3258,3259,3260,3263,3264,3267],{},"数据库特有语法：全文索引（MySQL 的 ",[25,3261,3262],{},"MATCH ... AGAINST","）、窗口函数、",[25,3265,3266],{},"ON DUPLICATE KEY UPDATE","；",[37,3269,3270],{},"已经手工调优过的复杂报表查询。",[13,3272,3273,3274,3277,3278,3281,3282,3284,3285,3288],{},"代价有两条：",[56,3275,3276],{},"丢掉数据库无关性","（换库就得改 SQL），以及",[56,3279,3280],{},"丢掉启动期校验","——JPQL 写错启动\n就报，原生 SQL 写错要等运行时才知道。另外原生查询若要分页，",[25,3283,1864],{}," 语句得用 ",[25,3286,3287],{},"countQuery","\n单独给，框架推断不出来。",[17,3290,3292],{"id":3291},"_7-容易踩的坑","7 容易踩的坑",[377,3294,3295,3308],{},[380,3296,3297],{},[383,3298,3299,3302,3305],{},[386,3300,3301],{},"现象",[386,3303,3304],{},"原因",[386,3306,3307],{},"处理",[393,3309,3310,3331,3344,3362,3387,3405,3423,3436,3453,3474],{},[383,3311,3312,3318,3321],{},[398,3313,3314,3315],{},"启动直接失败，报 ",[25,3316,3317],{},"No property xxx found for type Book",[398,3319,3320],{},"方法名里的字段拼错了",[398,3322,3323,3324,3326,3327,3330],{},"按实体字段名逐字核对（",[25,3325,1016],{}," 写成 ",[25,3328,3329],{},"titel"," 也会这样）",[383,3332,3333,3338,3341],{},[398,3334,3335,3336],{},"JPQL 报 ",[25,3337,1047],{},[398,3339,3340],{},"把表名当实体名写了",[398,3342,3343],{},"JPQL 里用实体名与字段名",[383,3345,3346,3352,3357],{},[398,3347,3348,3349,3351],{},"JPQL 里 ",[25,3350,1033],{}," 报错",[398,3353,3354,3355],{},"JPQL 不支持 ",[25,3356,1033],{},[398,3358,3359,3360],{},"写 ",[25,3361,1041],{},[383,3363,3364,3369,3379],{},[398,3365,3366,3368],{},[25,3367,633],{}," 查不出结果",[398,3370,3371,3372,3374,3375,3378],{},"参数里又自己加了 ",[25,3373,621],{},"，变成 ",[25,3376,3377],{},"%%重构%%"," 之外的情况",[398,3380,3381,3382,3384,3385],{},"用 ",[25,3383,633],{}," 时参数保持纯文本；要自己控制通配符就用 ",[25,3386,609],{},[383,3388,3389,3392,3399],{},[398,3390,3391],{},"命名参数报找不到",[398,3393,3394,3395,3398],{},"参数名与 ",[25,3396,3397],{},":xxx"," 对不上，或编译没保留参数名",[398,3400,3401,3402],{},"一律显式加 ",[25,3403,3404],{},"@Param(\"xxx\")",[383,3406,3407,3415,3418],{},[398,3408,3409,3411,3412],{},[25,3410,3052],{}," 调用时抛 ",[25,3413,3414],{},"TransactionRequiredException",[398,3416,3417],{},"没有事务上下文",[398,3419,3420,3421],{},"在 Service 方法上加 ",[25,3422,3131],{},[383,3424,3425,3428,3431],{},[398,3426,3427],{},"更新后同一事务里查到旧数据",[398,3429,3430],{},"持久化上下文里还是旧对象",[398,3432,3433],{},[25,3434,3435],{},"@Modifying(clearAutomatically = true, flushAutomatically = true)",[383,3437,3438,3444,3448],{},[398,3439,3440,3441,3443],{},"只返回一条却用 ",[25,3442,115],{}," 接，数据不存在时抛异常",[398,3445,3446],{},[25,3447,1163],{},[398,3449,3450,3451],{},"换成 ",[25,3452,1145],{},[383,3454,3455,3458,3463],{},[398,3456,3457],{},"分页时 SQL 执行了两条，一条 count 很慢",[398,3459,3460,3462],{},[25,3461,1621],{}," 需要总数",[398,3464,3465,3466,3468,3469,3471,3472],{},"不需要总数就用 ",[25,3467,1890],{},"，或给 ",[25,3470,64],{}," 提供精简的 ",[25,3473,3287],{},[383,3475,3476,3479,3482],{},[398,3477,3478],{},"生产分页接口被改成按大字段排序导致慢查询",[398,3480,3481],{},"排序字段由客户端传入且未校验",[398,3483,3484],{},"只允许白名单字段",[17,3486,3488],{"id":3487},"_8-小结","8 小结",[34,3490,3491,3500,3509,3520,3526,3537,3545],{},[37,3492,3493,3496,3497,3499],{},[56,3494,3495],{},"三种查询方式，按需升级","：方法名派生 → JPQL（",[25,3498,64],{},"）→ 原生 SQL；",[37,3501,3502,3503,3505,3506,3508],{},"派生查询的关键字要背一些，其中 ",[25,3504,633],{}," 与 ",[25,3507,609],{}," 的区别最容易踩；",[37,3510,3511,801,3514,3517,3518,3267],{},[56,3512,3513],{},"JPQL 用实体名和字段名",[25,3515,3516],{},"select"," 要写 ",[25,3519,1037],{},[37,3521,3522,3523,3525],{},"命名参数 + ",[25,3524,1109],{}," 最稳；",[37,3527,3528,3529,801,3532,3534,3535,3267],{},"分页用 ",[25,3530,3531],{},"PageRequest.of(页码从 0 开始, 每页条数, Sort)",[25,3533,1621],{}," 会多跑一次 count，不需要总数\n时用 ",[25,3536,1890],{},[37,3538,3539,3540,3542,3543,3267],{},"写操作要 ",[25,3541,3052],{},"（配合事务），并考虑 ",[25,3544,3139],{},[37,3546,3547,3548,3551],{},"只要部分字段就",[56,3549,3550],{},"用投影或 DTO","，别把实体直接吐给前端。",[13,3553,3554,3555,3558,3559,3505,3562,3565,3566,3568],{},"现在所有实体还是孤立的。下一篇讲 JPA 里最费思量的部分：",[56,3556,3557],{},"关联映射","——一对多、多对多怎么写，\n谁是「维护方」，",[25,3560,3561],{},"FetchType",[25,3563,3564],{},"CascadeType"," 又该怎么选，顺带把测试（",[25,3567,3123],{},"）补上。",[3570,3571,3572],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":84,"searchDepth":134,"depth":134,"links":3574},[3575,3576,3581,3585,3588,3594,3595,3596,3597],{"id":19,"depth":127,"text":20},{"id":73,"depth":127,"text":74,"children":3577},[3578,3579,3580],{"id":374,"depth":134,"text":375},{"id":461,"depth":134,"text":462},{"id":807,"depth":134,"text":808},{"id":821,"depth":127,"text":822,"children":3582},[3583,3584],{"id":997,"depth":134,"text":998},{"id":1051,"depth":134,"text":1052},{"id":1117,"depth":127,"text":1118,"children":3586},[3587],{"id":1223,"depth":134,"text":1224},{"id":1440,"depth":127,"text":1441,"children":3589},[3590,3591,3592,3593],{"id":1444,"depth":134,"text":1445},{"id":1535,"depth":134,"text":1536},{"id":1834,"depth":134,"text":1835},{"id":1908,"depth":134,"text":1909},{"id":3036,"depth":127,"text":3037},{"id":3181,"depth":127,"text":3182},{"id":3291,"depth":127,"text":3292},{"id":3487,"depth":127,"text":3488},"学习",null,"2026-10-15","Repository 自带的方法只够按主键查，这篇讲三种自己造查询的方式：方法名派生、@Query 写 JPQL、原生 SQL，以及 Pageable 分页排序和投影返回。","md",false,{},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-03",{"title":5,"description":3601},"Spring 学习笔记","posts\u002Fjavaspring\u002Fspring-notes-03",[3610,3611,3612],"Spring","Spring Data JPA","学习笔记","affpzxCDPEhovFK1Kg-67tWHmI3_1Tlv8HjxApXF9wc",[3615,3622,3628,3633,3638,3643,3648,3653,3658,3663,3668,3673,3678,3683,3688,3693,3698,3703,3708,3713,3718,3723,3728,3733,3738,3743,3748,3753,3758,3762,3766,3767,3771,3775,3779,3783,3787,3791,3795,3799,3803,3809,3813,3819,3823,3827,3831,3835,3839,3843,3847,3852,3856,3860,3865,3870,3876,3880,3885,3889],{"path":3616,"title":3617,"description":3618,"date":3619,"series":3620,"seriesOrder":3621},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-28","软件分析与设计笔记 28：draw.io 作图手册：五张图的符号位置、落笔顺序与自检","不模拟软件界面，只讲清三件事：每个 UML 符号在 draw.io 里搜什么词、一张图该按什么顺序落笔、画完拿什么标准自查。覆盖用例图、活动图、顺序图、类图、界面草图五种图纸，末尾附「最容易画反的十二笔」总检表。","2026-11-19","软件分析与设计笔记",28,{"path":3623,"title":3624,"description":3625,"date":3626,"series":3620,"seriesOrder":3627},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-27","软件分析与设计笔记 27：Paper 1 应试：从读案例到交卷的一次完整演算","拿一份完整的案例，从头到尾走一遍：怎么读案例列角色、怎么在四个小问之间分配时间、每一张图从哪一句话推出来、两张表怎么填、交卷前拿什么清单自查。附 C25 真题与样卷的题型分值对照。","2026-11-18",27,{"path":3629,"title":3630,"description":3631,"date":3632,"series":3620,"seriesOrder":2325},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-26","软件分析与设计笔记 26：总结：把整条链路串起来","系列收尾：从需求到代码的完整链路回顾、每一环的产出与验收方式、六条贯穿全系列的原则，以及一份可以直接拿去做项目自查的清单。","2026-11-17",{"path":3634,"title":3635,"description":3636,"date":3637,"series":3620,"seriesOrder":2319},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-25","软件分析与设计笔记 25：代码重构：坏味道与手法","重构不改变外部行为、只改善内部结构：四种常见坏味道与对应手法、为什么动手之前必须先有测试、什么时候该重构，以及重构和前面那些设计模型是什么关系。","2026-11-16",{"path":3639,"title":3640,"description":3641,"date":3642,"series":3620,"seriesOrder":2314},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-24","软件分析与设计笔记 24：设计原则：内聚、耦合与多态","好设计怎么衡量：内聚与耦合分别在说什么、一次从「差设计」到「多态重设计」的完整对照、抽象类与接口的选择依据，以及复用为什么会从优势变成风险。","2026-11-15",{"path":3644,"title":3645,"description":3646,"date":3647,"series":3620,"seriesOrder":2309},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-23","软件分析与设计笔记 23：设计建模：从对象模型到能实现的方案","分析模型能用但还不能直接开工：对象模型怎么映射到关系模型、继承映射的三种方案怎么选、数据访问层与 Data Broker 解决什么问题，以及三层架构与 MVC 的分工。","2026-11-14",{"path":3649,"title":3650,"description":3651,"date":3652,"series":3620,"seriesOrder":2298},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-22","软件分析与设计笔记 22：分析建模：边界、实体与控制对象","不经过分析直接写代码会踩什么坑、三种分析对象各管一段、怎么用顺序图完成一次用例实现，以及从顺序图导出类的属性、操作与关系的方法。","2026-11-13",{"path":3654,"title":3655,"description":3656,"date":3657,"series":3620,"seriesOrder":2292},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-21","软件分析与设计笔记 21：UML：类图与顺序图","为什么需要一种共同的画法：类符号三格怎么画、五种关系的符号与设计含义、每种关系落到 Java 是什么写法，顺序图的三要素怎么摆；以及这三种最容易找不到的符号在 draw.io 里各搜什么词。","2026-11-12",{"path":3659,"title":3660,"description":3661,"date":3662,"series":3620,"seriesOrder":360},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-20","软件分析与设计笔记 20：面向对象概念：对象、关系与多态","面向对象不是「用某个语言写代码」：对象与类的区别、封装真正在保护什么、五种关系（依赖、关联、聚合、组合、继承）各自何时用，以及多态为什么能减少条件判断。","2026-11-11",{"path":3664,"title":3665,"description":3666,"date":3667,"series":3620,"seriesOrder":344},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-19","软件分析与设计笔记 19：规范化与逻辑数据模型","一次改地址引发的数据不一致，讲清规范化的真正动机；UNF 到 3NF 每一步在解决什么问题，派生字段为什么不能存，优化为什么和规范化方向相反，以及逻辑数据模型在 3NF 之上补了什么。","2026-11-10",{"path":3669,"title":3670,"description":3671,"date":3672,"series":3620,"seriesOrder":339},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-18","软件分析与设计笔记 18：数据库分析：实体、关系与数据字典","为什么不能拿到需求就动手建表：实体与关系怎么判定、基数为什么必须由业务方确认、数据命名要遵守什么规则，以及数据字典四列里最容易被空着的那一列。","2026-11-09",{"path":3674,"title":3675,"description":3676,"date":3677,"series":3620,"seriesOrder":311},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-17","软件分析与设计笔记 17：用生成式 AI 做设计与开发：能用的场景与要防的坑","生成式 AI 在工作流里真正站得住的三个场景 —— 造示例数据、解释陌生代码、起草初稿；它为什么会复现社会偏见、为什么会编得像真的，以及哪些内容绝不能直接采用。","2026-11-08",{"path":3679,"title":3680,"description":3681,"date":3682,"series":3620,"seriesOrder":306},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-16","软件分析与设计笔记 16：从 persona 到 URS：一份交付物的完整骨架","把前面十几篇串成一份能交出去的东西：用户需求规格说明书的九个部分、每一章来自哪个环节、由谁确认，以及面向决策者的提案该讲清哪三件事。","2026-11-07",{"path":3684,"title":3685,"description":3686,"date":3687,"series":3620,"seriesOrder":281},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-15","软件分析与设计笔记 15：非功能需求：把「快、稳、安全」变成数字","为什么「系统要快」不是一条需求：非功能需求怎么量化，决定性能的用户量、交易量、数据量三张表怎么估，安全需求要写清什么，以及可靠性的「不可用」该怎么定义。","2026-11-06",{"path":3689,"title":3690,"description":3691,"date":3692,"series":3620,"seriesOrder":276},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-14","软件分析与设计笔记 14：软件测试：层级、方法与等价类划分","「我们测过了」这句话问题在哪：为什么静态评审和动态测试缺一不可，五个测试层级各自验证什么，白盒与黑盒的差别，以及用等价类划分和边界值把无限输入变成有限用例的方法。","2026-11-05",{"path":3694,"title":3695,"description":3696,"date":3697,"series":3620,"seriesOrder":260},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-13","软件分析与设计笔记 13：需求获取：五种调查方法与一场访谈","需求从哪里来：五种事实调查方法各自适合回答什么、代价是什么，访谈从准备到记录的全过程，问卷设计里最容易犯的错，以及访谈记录必须写下来的那几项信息。","2026-11-04",{"path":3699,"title":3700,"description":3701,"date":3702,"series":3620,"seriesOrder":255},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-12","软件分析与设计笔记 12：前端设计：把流程变成屏幕","界面设计不是「画得好看」：从活动图走到屏幕的三步，CRUD 任务流怎么当检查表，界面必须回答的三个问题，信息架构与五种导航类型，视觉一致性与留白，以及移动端与 Web 端的差别。","2026-11-03",{"path":3704,"title":3705,"description":3706,"date":3707,"series":3620,"seriesOrder":238},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-11","软件分析与设计笔记 11：Agentic AI 与业务流程自动化","AI agent 到底是什么：五项能力与四个核心组件，什么场景该用定时任务、什么该用程序、什么才值得用 agent，两种实现路径的差别，必须提前防住的四类失败模式，以及设计方法从提示词到编排的演进。","2026-11-02",{"path":3709,"title":3710,"description":3711,"date":3712,"series":3620,"seriesOrder":233},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-10","软件分析与设计笔记 10：精炼用例模型：include、extend 与泛化","三种精炼用例关系的判断标准：include 什么时候用、它和 extend 的区别到底在哪、参与者泛化与用例泛化各解决什么问题，以及精炼到什么程度就该停手。","2026-11-01",{"path":3714,"title":3715,"description":3716,"date":3717,"series":3620,"seriesOrder":214},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-09","软件分析与设计笔记 09：功能需求（二）：从流程到业务实体","活动图画完之后，怎么从流程里筛出真正的业务实体：三条判断标准、哪些名词其实不该建实体、属性从哪里来，以及领域模型和后面的数据模型分别负责什么。","2026-10-31",{"path":3719,"title":3720,"description":3721,"date":3722,"series":3620,"seriesOrder":209},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-08","软件分析与设计笔记 08：功能需求（一）：用例模型与活动图","从用户故事到用例的翻译过程：什么才算「好」的参与者，系统边界怎么划，用例描述必须写清哪四件事，活动图怎么把分支、并行与泳道画清楚，以及这两个图在 draw.io 里各用哪个形状、最容易画错哪一笔。","2026-10-30",{"path":3724,"title":3725,"description":3726,"date":3727,"series":3620,"seriesOrder":186},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-07","软件分析与设计笔记 07：用户故事：把需求写成可验收的一句话","用户故事的三段结构，怎么从用户画像和数据地图里把它提取出来，一条故事多大算合适、时序为什么也是需求的一部分，以及验收标准怎么写才算真的可验收。","2026-10-29",{"path":3729,"title":3730,"description":3731,"date":3732,"series":3620,"seriesOrder":181},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-06","软件分析与设计笔记 06：范围界定（数据侧）：用数据校验你的判断","访谈结论和数字对不上时该怎么办：分析的四个层次分别在回答什么问题、为什么不能跳级、数据准备与搜集怎么做成一张应用地图，以及数据最容易在哪几个地方骗你。","2026-10-28",{"path":3734,"title":3735,"description":3736,"date":3737,"series":3620,"seriesOrder":163},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-05","软件分析与设计笔记 05：范围界定（用户侧）：用户画像与客户旅程图","为什么「所有用户」是一个陷阱，项目边界该怎么划，用户画像要写哪五个要素、最容易写成什么废纸，客户旅程图怎么把阶段、想法和情绪画在同一张纸上，以及怎么从这两样东西一步步推到可验证的需求。","2026-10-27",{"path":3739,"title":3740,"description":3741,"date":3742,"series":3620,"seriesOrder":158},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-04","软件分析与设计笔记 04：接手陌生案例的第一步：三问与一张表","拿到一个完全陌生的业务案例，怎么从零开始不跑偏：只问 Who、What、Why 三个问题，把「问题—事实—假设—理由」记成一张四列表，说清为什么假设必须写明理由，以及带着问题进访谈为什么比空手去有效得多。","2026-10-26",{"path":3744,"title":3745,"description":3746,"date":3747,"series":3620,"seriesOrder":134},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-03","软件分析与设计笔记 03：Agile 与 Scrum：迭代、角色与产出物","从「三周做完交付，客户说这不是我要的」讲起，说清瀑布式与迭代式的根本差别，Scrum 的三个角色和三种产出物，一个 sprint 里四次固定活动各自在解决什么问题，以及敏捷里最常见的几个误解。","2026-10-25",{"path":3749,"title":3750,"description":3751,"date":3752,"series":3620,"seriesOrder":127},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-02","软件分析与设计笔记 02：设计思维、用户驱动与数据驱动","从「客服要一个能一次看完所有排期的界面」这个翻车案例讲起，说清设计思维到底是什么、用户驱动与数据驱动各自能看见什么又看不见什么、为什么真正的落点是把两者合起来的 data-informed design，以及访谈中怎么问出有用的信息。","2026-10-24",{"path":3754,"title":3755,"description":3756,"date":3757,"series":3620,"seriesOrder":91},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-01","软件分析与设计笔记 01：课程地图与从需求到设计的主干","这一篇不写具体方法，先把整条链路铺开：软件分析与设计到底要解决什么问题，从需求到设计分成哪两段，中间有哪十几个环节，一份完整的交付物里应该有什么，以及后面 27 篇笔记怎么排、新人该按什么顺序读。","2026-10-23",{"path":3759,"title":3760,"description":3761,"date":3600,"series":3607,"seriesOrder":91},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-01","Spring 学习笔记 01：IoC、依赖注入与第一个 Spring Boot 应用","从「手写 new 出来的三层结构」的痛点讲起，说清 IoC 容器、Bean 与依赖注入是怎么回事，再用 Maven 与 Spring Initializr 跑起第一个能返回 JSON 的应用。",{"path":3763,"title":3764,"description":3765,"date":3600,"series":3607,"seriesOrder":127},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-02","Spring 学习笔记 02：Spring Data JPA 基础——实体映射与 Repository","用 ORM 把类映射到表：@Entity、@Id、@GeneratedValue 怎么用，CrudRepository 三层接口怎么选，H2 与 MySQL 怎么配，以及 ddl-auto 到底该写哪个值。",{"path":3605,"title":5,"description":3601,"date":3600,"series":3607,"seriesOrder":134},{"path":3768,"title":3769,"description":3770,"date":3600,"series":3607,"seriesOrder":158},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-04","Spring 学习笔记 04：JPA 关联映射与仓储测试","一对多、多对多怎么映射，谁负责维护外键，FetchType 与 CascadeType 该怎么选，顺带用 @DataJpaTest 与 AssertJ 把仓储层测起来。",{"path":3772,"title":3773,"description":3774,"date":3600,"series":3607,"seriesOrder":163},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-05","Spring 学习笔记 05：Spring MVC 与 Thymeleaf 入门","一次页面请求在 Spring MVC 里走了哪几步，@Controller 怎么把数据交给模板，@PathVariable 与 @RequestParam 怎么取参数，以及控制器为什么必须是无状态的。",{"path":3776,"title":3777,"description":3778,"date":3600,"series":3607,"seriesOrder":181},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-06","Spring 学习笔记 06：表单绑定与服务端校验","把表单字段绑到对象上，用 Bean Validation 注解做服务端校验，把错误信息显示回用户刚填的那一页，再补上自定义校验注解与全局异常处理。",{"path":3780,"title":3781,"description":3782,"date":3600,"series":3607,"seriesOrder":186},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-07","Spring 学习笔记 07：Thymeleaf 片段复用与国际化","用 th:fragment 与 th:insert\u002Fth:replace 把导航栏、页脚抽成片段，页面套用统一布局，再用 messages.properties 与 LocaleResolver 做中英文切换。",{"path":3784,"title":3785,"description":3786,"date":3600,"series":3607,"seriesOrder":209},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-08","Spring 学习笔记 08：HTTP Session 与会话状态管理","HTTP 无状态带来的问题，Cookie 与 Session 怎么分工，Spring 里读写会话的几种方式，以及会话固定、超时、多实例共享这些必须想清楚的事。",{"path":3788,"title":3789,"description":3790,"date":3600,"series":3607,"seriesOrder":214},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-09","Spring 学习笔记 09：拦截器 HandlerInterceptor","登录检查、权限判断、请求耗时统计这类横切逻辑该放哪：HandlerInterceptor 的三个回调各在什么时机执行，怎么注册与排除路径，以及它和过滤器的分工。",{"path":3792,"title":3793,"description":3794,"date":3600,"series":3607,"seriesOrder":233},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-10","Spring 学习笔记 10：Service 层与事务","一组数据库操作要么全成功要么全失败，靠 @Transactional 怎么做到；传播行为、隔离级别、回滚规则怎么选，以及自调用失效、乐观锁悲观锁这些实战里的坑。",{"path":3796,"title":3797,"description":3798,"date":3600,"series":3607,"seriesOrder":238},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-11","Spring 学习笔记 11：REST API 基础——把 Service 暴露成接口","同一套 Service 怎么同时服务页面和客户端：@RestController 的路由与方法语义、状态码怎么给、DTO 怎么定，以及 MockMvc 端到端和切片测试的两种写法与各自的坑。",{"path":3800,"title":3801,"description":3802,"date":3600,"series":3607,"seriesOrder":255},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-12","Spring 学习笔记 12：REST 进阶——版本、更新、跨域与文档化","接口能跑通之后要处理的几件事：URL 与响应结构怎么定、版本怎么演进、更新怎么做到可安全重试、CORS 怎么配、HATEOAS 与 Spring Data REST 该不该用。",{"path":3804,"title":3805,"description":3806,"date":3807,"series":3808,"seriesOrder":158},"\u002Fposts\u002Fjava\u002Fjava-functionalprogramming-notes","Java 函数式编程笔记：Lambda、函数式接口与 Streams","学习 Java 函数式编程时的个人笔记：Lambda 表达式的五步简写与方法引用、常见函数式接口、Stream 流水线与惰性求值，以及创建流、中间操作、终止操作与收集器的用法。示例代码均为自写，运行结果来自本地实测。","2026-10-13","Java 学习笔记",{"path":3810,"title":3811,"description":3812,"date":3807,"series":3808,"seriesOrder":134},"\u002Fposts\u002Fjava\u002Fjava-generics-notes","Java 泛型笔记：泛型方法、有界类型与泛型类","学习 Java 泛型时的个人笔记：从重复的重载方法与多态尝试讲起，再到泛型方法与类型推断、有界泛型类型、Comparable 与 Number 边界，以及泛型类在集合中的用法。示例代码均为自写，运行结果来自本地实测。",{"path":3814,"title":3815,"description":3816,"date":3817,"series":3818,"seriesOrder":158},"\u002Fposts\u002Fmysql\u002Fmysql-learning-04","MySQL 学习笔记 04：数据操纵（增删改）","MySQL 学习笔记第四篇：INSERT 插入（含批量插入与 INSERT ... SELECT）、UPDATE 更新、DELETE 删除，以及各自的坑。","2026-10-10","SQL 学习笔记",{"path":3820,"title":3821,"description":3822,"date":3817,"series":3818,"seriesOrder":163},"\u002Fposts\u002Fmysql\u002Fmysql-learning-05","MySQL 学习笔记 05：DDL 概览","MySQL 学习笔记第五篇：数据定义语言概览——Schema、Table、View、Index、Constraints 这几类对象分别对应哪些 CREATE \u002F ALTER \u002F DROP 命令。",{"path":3824,"title":3825,"description":3826,"date":3817,"series":3818,"seriesOrder":181},"\u002Fposts\u002Fmysql\u002Fmysql-learning-06","MySQL 学习笔记 06：CREATE TABLE 与数据类型","MySQL 学习笔记第六篇：CREATE TABLE 的完整语法、常用数据类型的选型建议，以及外键的两种定义方式。",{"path":3828,"title":3829,"description":3830,"date":3817,"series":3818,"seriesOrder":186},"\u002Fposts\u002Fmysql\u002Fmysql-learning-07","MySQL 学习笔记 07：索引","MySQL 学习笔记第七篇：索引的作用与 B+ 树结构、单列与复合索引的创建、索引的代价与适用场景。",{"path":3832,"title":3833,"description":3834,"date":3817,"series":3818,"seriesOrder":209},"\u002Fposts\u002Fmysql\u002Fmysql-learning-08","MySQL 学习笔记 08：DROP 与 ALTER","MySQL 学习笔记第八篇：DROP 删除数据库对象、ALTER 修改表结构，以及常见的坑。",{"path":3836,"title":3837,"description":3838,"date":3817,"series":3818,"seriesOrder":214},"\u002Fposts\u002Fmysql\u002Fmysql-learning-09","MySQL 学习笔记 09：约束与数据完整性","MySQL 学习笔记第九篇：SQL 约束——NOT NULL、CHECK、PRIMARY KEY、FOREIGN KEY 等如何保证数据完整性，以及它们的对比。",{"path":3840,"title":3841,"description":3842,"date":3817,"series":3818,"seriesOrder":233},"\u002Fposts\u002Fmysql\u002Fmysql-learning-10","MySQL 学习笔记 10：视图","MySQL 学习笔记第十篇：视图（Views）的定义与特性、和表的对比、创建与使用，以及可更新视图的限制。",{"path":3844,"title":3845,"description":3846,"date":3817,"series":3818,"seriesOrder":238},"\u002Fposts\u002Fmysql\u002Fmysql-learning-11","MySQL 学习笔记 11：存储过程","MySQL 学习笔记第十一篇：存储过程（Stored Procedures）的概念、创建与调用、带参数的写法，以及优缺点。",{"path":3848,"title":3849,"description":3850,"date":3851,"series":3818,"seriesOrder":91},"\u002Fposts\u002Fmysql\u002Fmysql-learning-01","MySQL 学习笔记 01：MySQL 基础知识","MySQL 学习笔记第一篇：MySQL Server 与 Workbench 的安装，数据库与 DBMS 的基本概念，关系模型、ACID 事务、常见数据类型，以及后续的学习路线。","2026-10-09",{"path":3853,"title":3854,"description":3855,"date":3851,"series":3818,"seriesOrder":127},"\u002Fposts\u002Fmysql\u002Fmysql-learning-02","MySQL 学习笔记 02：SELECT 查询与过滤","MySQL 学习笔记第二篇：SELECT 的基本语法与逻辑执行顺序，WHERE 过滤（比较、LIKE、IN、BETWEEN、NULL）、各种 JOIN、UNION 与子查询。",{"path":3857,"title":3858,"description":3859,"date":3851,"series":3818,"seriesOrder":134},"\u002Fposts\u002Fmysql\u002Fmysql-learning-03","MySQL 学习笔记 03：聚合、分组与输出控制","MySQL 学习笔记第三篇：COUNT\u002FSUM\u002FAVG 等聚合函数、GROUP BY 分组、HAVING 过滤、ORDER BY 与 LIMIT，以及一条 SELECT 的完整逻辑执行顺序。",{"path":3861,"title":3862,"description":3863,"date":3864,"series":3808,"seriesOrder":127},"\u002Fposts\u002Fjava\u002Fjava-oop-notes","Java 面向对象笔记：类、继承、多态与异常","学习 Java 面向对象编程时的个人笔记：类与对象、封装与访问修饰符、继承与多态、集合、抽象类与接口、异常处理。示例代码均为自写，运行结果来自本地实测。","2026-10-06",{"path":3866,"title":3867,"description":3868,"date":3869,"series":3808,"seriesOrder":91},"\u002Fposts\u002Fjava\u002Fjava-basics-notes","Java 基础语法笔记：从环境安装到方法与数组","学习 Java 基础语法时的个人笔记：JDK 安装、输入输出、变量与运算符、条件与循环、字符串、数组与方法。示例代码均为自写，运行结果来自本地实测。","2026-10-05",{"path":3871,"title":3872,"description":3873,"date":3874,"series":3875,"seriesOrder":91},"\u002Fposts\u002Fgit\u002Fgit-install-and-basics","Git 安装与基本使用","从装好 Git 到跑通日常提交流程，把最常用的几条命令和它们背后的三个区域讲清楚。","2026-10-04","Git 入门",{"path":3877,"title":3878,"description":3879,"date":3874,"series":3875,"seriesOrder":127},"\u002Fposts\u002Fgit\u002Flocal-remote-repository","本地远程仓库的部署","用自己的机器当 Git 服务器：建一个裸仓库、走 SSH 连上去，把本地提交推上去。",{"path":3881,"title":3882,"description":3883,"date":3874,"series":3884,"seriesOrder":91},"\u002Fposts\u002Fguide\u002Fguide-file-layout","文件都放在哪","文章、图片、封面分别放在仓库的什么位置，哪些目录可以放手改、哪些碰了会出问题。","博客使用指南",{"path":3886,"title":3887,"description":3888,"date":3874,"series":3884,"seriesOrder":127},"\u002Fposts\u002Fguide\u002Fguide-frontmatter","frontmatter 每个字段是什么意思","文章开头那段被横线包住的字段，逐个说明作用、默认行为和写错的后果。",{"path":3890,"title":3891,"description":3892,"date":3874,"series":3884,"seriesOrder":134},"\u002Fposts\u002Fguide\u002Fguide-markdown","正文怎么写 —— Markdown 与排版效果","标题、链接、图片、代码、表格分别怎么写，以及本站对它们做了哪些自定义处理。",1793268152478]