[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"\u002Fresource\u002Fdocument\u002Flist?undefined":3,"\u002Fresource\u002Fdocument\u002Fquery\u002Fijay2hay19kn1k031?undefined":462,"\u002Fresource\u002Fadvertise\u002Flist?type=all?undefined":467},{"data":4,"status":460,"success":461},[5,148,202,291,332,370,420],{"books":6,"desc":145,"id":8,"image":146,"title":147},[7,40,63,78,93,105,117],{"cateId":8,"chapters":9,"desc":36,"id":11,"time":37,"title":38,"video":39},1,[10,15,18,21,24,27,30,33],{"bookId":11,"id":12,"indexOrder":13,"name":14},24,"8egfulw98v3h680j",0,"JavaSE 笔记（一）走进Java语言",{"bookId":11,"id":16,"indexOrder":13,"name":17},"pew6po6wrou23pk3","JavaSE 笔记（二）面向过程编程",{"bookId":11,"id":19,"indexOrder":13,"name":20},"eldst1fgrbdkmfs7","JavaSE 笔记（三）面向对象基础",{"bookId":11,"id":22,"indexOrder":13,"name":23},"48zphgkpjto8cath","JavaSE 笔记（四）面向对象高级篇",{"bookId":11,"id":25,"indexOrder":13,"name":26},"6r4llai92yc15j98","JavaSE 笔记（五）泛型程序设计",{"bookId":11,"id":28,"indexOrder":13,"name":29},"k6fmxd6qabgkwm9i","JavaSE 笔记（六）集合类与IO",{"bookId":11,"id":31,"indexOrder":13,"name":32},"qrd0xfttsz32gpqg","JavaSE 笔记（七）多线程与反射",{"bookId":11,"id":34,"indexOrder":13,"name":35},"td5tgn04nqmkrryt","JavaSE 笔记（八）GUI程序开发","基于Java25全新录制的SE课程",2025,"JavaSE 核心内容","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV163GGz2E8c",{"cateId":8,"chapters":41,"desc":59,"id":8,"time":60,"title":61,"video":62},[42,44,46,49,51,53,55,57],{"bookId":8,"id":43,"indexOrder":13,"name":14},"ibeeuwsbbi00undq",{"bookId":8,"id":45,"indexOrder":13,"name":17},"dncxjecdv4wciqcp",{"bookId":8,"id":47,"indexOrder":13,"name":48},"jviyz2hsht9ete5k","JavaSE 笔记（三）面向对象基础篇",{"bookId":8,"id":50,"indexOrder":13,"name":23},"qb9i6q9fap7bg1cc",{"bookId":8,"id":52,"indexOrder":13,"name":26},"hnkrjrkm3hjzeq6s",{"bookId":8,"id":54,"indexOrder":13,"name":29},"erpm32wduoaaqmrx",{"bookId":8,"id":56,"indexOrder":13,"name":32},"lfqtvxr7azumcwja",{"bookId":8,"id":58,"indexOrder":13,"name":35},"qs7gqok56gzc6idr","2022年制作的JavaSE版本",2022,"JavaSE 22年旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1YP4y1o75f\u002F",{"cateId":8,"chapters":64,"desc":75,"id":66,"time":60,"title":76,"video":77},[65,69,72],{"bookId":66,"id":67,"indexOrder":13,"name":68},2,"g96k66kczovvbm1i","JVM 笔记（一）走进JVM",{"bookId":66,"id":70,"indexOrder":13,"name":71},"ydd7n3jg8unc3clg","JVM 笔记（二）内存管理",{"bookId":66,"id":73,"indexOrder":13,"name":74},"r9dq37de0kaeauoi","JVM 笔记（三）类与类加载","了解Java的底层运作机制","Java JVM 虚拟机","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Er4y1r7as\u002F",{"cateId":8,"chapters":79,"desc":90,"id":81,"time":60,"title":91,"video":92},[80,84,87],{"bookId":81,"id":82,"indexOrder":13,"name":83},3,"asncyye9ya18gfar","JUC 笔记（一）再谈多线程",{"bookId":81,"id":85,"indexOrder":13,"name":86},"5tr1sm4ho6ygpt9q","JUC 笔记（二）并发编程核心",{"bookId":81,"id":88,"indexOrder":13,"name":89},"1scf51z5300mzxkh","JUC 笔记（三）并发编程进阶","你也可以成为多线程的主宰者","Java JUC 并发编程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1JT4y1S7K8\u002F",{"cateId":8,"chapters":94,"desc":102,"id":96,"time":60,"title":103,"video":104},[95,99],{"bookId":96,"id":97,"indexOrder":13,"name":98},4,"eedesc445ygiqhil","NIO 笔记（一）基础内容",{"bookId":96,"id":100,"indexOrder":13,"name":101},"ndz9t0uunrmfmv4n","NIO 笔记（二）Netty框架专题","编写畅快的高性能网络服务器","Java NIO 网络编程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1ar4y1J7mC\u002F",{"cateId":8,"chapters":106,"desc":114,"id":108,"time":60,"title":115,"video":116},[107,111],{"bookId":108,"id":109,"indexOrder":13,"name":110},5,"9890i8ofuadpwy2b","[扩展篇] Java 9-17新特性介绍",{"bookId":108,"id":112,"indexOrder":13,"name":113},"tsrkqvb6zpmtwh0n","[扩展篇] JavaSE关键字总结 笔记","精彩仍在继续，不要停止脚步","其他内容","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1tU4y1y7Fg\u002F",{"cateId":8,"chapters":118,"desc":141,"id":120,"time":142,"title":143,"video":144},[119,123,126,129,132,135,138],{"bookId":120,"id":121,"indexOrder":13,"name":122},6,"4db9h32opv7imszh","JavaSE 笔记（一）面向过程编程",{"bookId":120,"id":124,"indexOrder":13,"name":125},"c93u3v37br7hgn1q","JavaSE 笔记（二）面向对象基础篇",{"bookId":120,"id":127,"indexOrder":13,"name":128},"yglsjde9gi1jxkcb","JavaSE 笔记（三）泛型与集合类",{"bookId":120,"id":130,"indexOrder":13,"name":131},"ilhi987n986rmvo3","JavaSE 笔记（四）异常机制",{"bookId":120,"id":133,"indexOrder":13,"name":134},"pqv38vexmenglk4k","JavaSE 笔记（五）IO",{"bookId":120,"id":136,"indexOrder":13,"name":137},"jiq41n87i9ia7ilw","JavaSE 笔记（六）多线程",{"bookId":120,"id":139,"indexOrder":13,"name":140},"wn7x2mge9ws79zps","JavaSE 笔记（七）反射","此版本为早期录制的旧版本",2021,"JavaSE 21年旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Gv411T7pi\u002F","包含JavaSE基础路线全部教程笔记，打下坚实的基础","https:\u002F\u002Fpic2.zhimg.com\u002F80\u002Fv2-bf1a927f037a79f4d57d9ae543430a0d_1440w.webp","JavaSE 系列笔记 ☕️",{"books":149,"desc":199,"id":66,"image":200,"title":201},[150,166,178],{"cateId":66,"chapters":151,"desc":162,"id":153,"time":163,"title":164,"video":165},[152,156,159],{"bookId":153,"id":154,"indexOrder":13,"name":155},21,"iqbc2haub31bwqtz","Lombok 极速上手",{"bookId":153,"id":157,"indexOrder":13,"name":158},"ijay2hay19kn1k031","Mybatis 快速上手",{"bookId":153,"id":160,"indexOrder":13,"name":161},"ru4ogh2waocpn4jo","Maven 快速上手","JavaWeb阶段必须扩展知识点",2024,"常用知识讲解","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1gb421J7ok\u002F",{"cateId":66,"chapters":167,"desc":175,"id":169,"time":163,"title":176,"video":177},[168,172],{"bookId":169,"id":170,"indexOrder":13,"name":171},22,"ek20yvb6huhxizx7","JavaWeb 笔记（一）计算机网络基础",{"bookId":169,"id":173,"indexOrder":13,"name":174},"pgevws6w2krkffa4","JavaWeb笔记（二）Java与数据库","全面升级的JavaWeb课程","JavaWeb 网站开发","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1kS421X7rq\u002F",{"cateId":66,"chapters":179,"desc":196,"id":181,"time":142,"title":197,"video":198},[180,184,187,190,193],{"bookId":181,"id":182,"indexOrder":13,"name":183},7,"ggwwj09j2vkfftvd","JavaWeb 笔记（一）Java网络编程",{"bookId":181,"id":185,"indexOrder":13,"name":186},"sauvq105istskjaz","JavaWeb 笔记（二）数据库基础",{"bookId":181,"id":188,"indexOrder":13,"name":189},"xgbeasmvrhxx9tn4","JavaWeb 笔记（三）Java与数据库",{"bookId":181,"id":191,"indexOrder":13,"name":192},"k7dfwua3bsezvw9q","JavaWeb 笔记（四）前端基础",{"bookId":181,"id":194,"indexOrder":13,"name":195},"ycpagby2v7j4p728","JavaWeb 笔记（五）后端开发","搭建属于自己的Web网站","JavaWeb 旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1CL4y1i7qR\u002F","包含JavaWeb路线全套笔记，从零开始搭建自己的网站！","https:\u002F\u002Fpic3.zhimg.com\u002F80\u002Fv2-df3b38e3012258ed70c23b586309e3f6_1440w.webp","JavaWeb 系列笔记 🚛",{"books":203,"desc":288,"id":81,"image":289,"title":290},[204,220,235,255,273],{"cateId":81,"chapters":205,"desc":216,"id":207,"time":217,"title":218,"video":219},[206,210,213],{"bookId":207,"id":208,"indexOrder":13,"name":209},8,"h7sjo5oy0l03607e","SSM笔记（一）Spring基础",{"bookId":207,"id":211,"indexOrder":13,"name":212},"eve8gq72qmdb46sg","SSM笔记（二）SpringMvc基础",{"bookId":207,"id":214,"indexOrder":13,"name":215},"63v73g0zh1qlr6fk","SSM笔记（三）SpringSecurity基础","Spring的探索之路从这里开始",2023,"JavaSSM 基础部分","[\"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Kv4y1x7is\u002F\", \"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Lh4y1M7kx\u002F\", \"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1fV411M7aS\u002F\"]",{"cateId":81,"chapters":221,"desc":232,"id":223,"time":217,"title":233,"video":234},[222,226,229],{"bookId":223,"id":224,"indexOrder":13,"name":225},16,"0k66v5r6slsfuog4","SpringBoot笔记（一）核心内容",{"bookId":223,"id":227,"indexOrder":13,"name":228},"bqlrnc2yvkaxo8s1","SpringBoot笔记（二）数据交互",{"bookId":223,"id":230,"indexOrder":13,"name":231},"wci9lb9tgea866jt","SpringBoot笔记（三）前后端分离","SpringBoot全新重制版","SpringBoot 新版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1xu4y1m7UP\u002F",{"cateId":81,"chapters":236,"desc":252,"id":238,"time":60,"title":253,"video":254},[237,240,243,246,249],{"bookId":238,"id":239,"indexOrder":13,"name":225},9,"e43gl1ilygps032v",{"bookId":238,"id":241,"indexOrder":13,"name":242},"emnmd8nzfdb3hr50","SpringBoot笔记（二）Git版本控制",{"bookId":238,"id":244,"indexOrder":13,"name":245},"jjlolj5igvttvyhv","SpringBoot笔记（三）Redis数据库",{"bookId":238,"id":247,"indexOrder":13,"name":248},"skgr4ivb5curdoux","SpringBoot笔记（四）其他框架介绍",{"bookId":238,"id":250,"indexOrder":13,"name":251},"le91fqhu4dqui1k4","SpringBoot笔记（五）Linux系统","逐步走向企业级开发","SpringBoot 旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1UL411V7f3\u002F",{"cateId":81,"chapters":256,"desc":270,"id":258,"time":60,"title":271,"video":272},[257,261,264,267],{"bookId":258,"id":259,"indexOrder":13,"name":260},10,"oejzo0l77zeb6a7e","SpringCloud笔记（一）微服务基础",{"bookId":258,"id":262,"indexOrder":13,"name":263},"f6eya9taaelsl35p","SpringCloud笔记（二）微服务进阶",{"bookId":258,"id":265,"indexOrder":13,"name":266},"35v1hbsfcdgagdnw","SpringCloud笔记（三）微服务应用",{"bookId":258,"id":268,"indexOrder":13,"name":269},"a782u84512tyuo1m","SpringCloud笔记（四）消息队列","体验微服务架构带来的魅力","SpringCloud 进阶","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1AL4y1j7RY\u002F",{"cateId":81,"chapters":274,"desc":285,"id":276,"time":142,"title":286,"video":287},[275,278,280,282],{"bookId":276,"id":277,"indexOrder":13,"name":209},11,"efjw75u8a251qxk5",{"bookId":276,"id":279,"indexOrder":13,"name":212},"guc134xb7sl78vju",{"bookId":276,"id":281,"indexOrder":13,"name":215},"u8ekxxucowr2b1tm",{"bookId":276,"id":283,"indexOrder":13,"name":284},"vkpmw9wbej21nei6","SSM笔记（四）MySQL进阶","此教程为2021年旧版教程","JavaSSM 旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1xL4y1H7Tq\u002F","包含Spring全套框架笔记，从开始到Spring Boot，以及众多运维小知识。","https:\u002F\u002Fpic4.zhimg.com\u002F80\u002Fv2-28c3144421220d7c048703281bc34f63_1440w.webp","Spring 系列笔记 🍏",{"books":292,"desc":329,"id":96,"image":330,"title":331},[293,308],{"cateId":96,"chapters":294,"desc":305,"id":296,"time":60,"title":306,"video":307},[295,299,302],{"bookId":296,"id":297,"indexOrder":13,"name":298},12,"jd3e8u5cmvx5gco6","C语言（一）计算机思维导论",{"bookId":296,"id":300,"indexOrder":13,"name":301},"lqv77apvx82nkkio","C语言（二）基础语法",{"bookId":296,"id":303,"indexOrder":13,"name":304},"xb0b9t37gyv96xns","C语言（三）高级特性","包含高等院校需要教授的全部内容","C语言程序设计","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Cr4y137os\u002F",{"cateId":96,"chapters":309,"desc":326,"id":311,"time":60,"title":327,"video":328},[310,314,317,320,323],{"bookId":311,"id":312,"indexOrder":13,"name":313},13,"8a046ps2e4w6k4py","数据结构与算法（一）线性结构篇",{"bookId":311,"id":315,"indexOrder":13,"name":316},"3ma8db91f9zrnkja","数据结构与算法（二）树形结构篇",{"bookId":311,"id":318,"indexOrder":13,"name":319},"0lsjm59k7cgu4tpr","数据结构与算法（三）散列表篇",{"bookId":311,"id":321,"indexOrder":13,"name":322},"0qzy7bogo0g2pusa","数据结构与算法（四）图结构篇",{"bookId":311,"id":324,"indexOrder":13,"name":325},"6gmcxcikcilyxblj","数据结构与算法（五）排序算法篇","虽然很难，但是它是考研必学科目","数据结构与算法","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV13W4y127Ey\u002F","你的内心一直有一个坚定的声音在告诉你，一定要考上一名研究生，向着未来前进吧！","https:\u002F\u002Fpic2.zhimg.com\u002F80\u002Fv2-ac128404efb29ce1c9d1ccc61024f1d1_1440w.webp","C语言 系列笔记 🥬",{"books":333,"desc":367,"id":108,"image":368,"title":369},[334,349,358],{"cateId":108,"chapters":335,"desc":346,"id":337,"time":163,"title":347,"video":348},[336,340,343],{"bookId":337,"id":338,"indexOrder":13,"name":339},17,"urw2e6gg1lprv65w","Kotlin（一）基础语法",{"bookId":337,"id":341,"indexOrder":13,"name":342},"t7lnl87f74f3v1ju","Kotlin（二）类与对象",{"bookId":337,"id":344,"indexOrder":13,"name":345},"v1zzvki0knb1xvml","Kotlin（三）高级特性","包含Kotlin语言完整基础部分","Kotlin程序设计基础","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1P94y1c7tV\u002F",{"cateId":108,"chapters":350,"desc":355,"id":352,"time":163,"title":356,"video":357},[351],{"bookId":352,"id":353,"indexOrder":13,"name":354},18,"ovbzpe7065bye1st","Kotlin扩展（一）","包含Kotlin额外扩展知识","Kotlin扩展篇","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Hg4y1m7Ca\u002F",{"cateId":108,"chapters":359,"desc":364,"id":361,"time":163,"title":365,"video":366},[360],{"bookId":361,"id":362,"indexOrder":13,"name":363},19,"3at7ybv04dmjc0wp","Gradle基础教程","Gradle配置教程（Kotlin）","Gradle教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Fc411x7xF\u002F","Kotlin让JVM平台焕发新的生机，让语言的表达更加优美","https:\u002F\u002Fpic2.zhimg.com\u002F80\u002Fv2-be815568f7c79c64cdaa171b0409786d_1440w.webp","Kotlin 系列笔记 ☘️",{"books":371,"desc":418,"id":120,"title":419},[372,391,403],{"cateId":120,"chapters":373,"desc":387,"id":375,"time":388,"title":389,"video":390},[374,378,381,384],{"bookId":375,"id":376,"indexOrder":13,"name":377},26,"zjf5qapwqtqiohcn","JavaScript笔记（一）基础语法",{"bookId":375,"id":379,"indexOrder":13,"name":380},"95jc6sjyjwcp9pvp","JavaScript笔记（二）核心知识",{"bookId":375,"id":382,"indexOrder":13,"name":383},"j35cdc1qz8dzq7pn","JavaScript笔记（三）进阶知识",{"bookId":375,"id":385,"indexOrder":13,"name":386},"sdhodlihphnpcg37","JavaScript笔记（四）前端基础","包含JavaScript最新语法规范讲解",2026,"JavaScript教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1xq6gBgESU",{"cateId":120,"chapters":392,"desc":400,"id":394,"time":37,"title":401,"video":402},[393,397],{"bookId":394,"id":395,"indexOrder":13,"name":396},23,"bsisgazdftiz3o9c","HTML5笔记（一）基础内容",{"bookId":394,"id":398,"indexOrder":13,"name":399},"njol93fs34gfwuzf","HTML5笔记（二）高级内容","包含HTML基础内容和相关知识点","HTML5核心教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1BrBiYNEWg",{"cateId":120,"chapters":404,"desc":415,"id":406,"time":37,"title":416,"video":417},[405,409,412],{"bookId":406,"id":407,"indexOrder":13,"name":408},25,"jo74ciirtg8wh90y","CSS笔记（一）基础入门",{"bookId":406,"id":410,"indexOrder":13,"name":411},"ap5ixyomoejuw4ue","CSS笔记（二）盒模型和布局",{"bookId":406,"id":413,"indexOrder":13,"name":414},"4djgk5xy1lzpiuf2","CSS笔记（三）变换和过渡","包含CSS3基础内容和相关知识点","CSS3核心教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1sQeEzFEKi","包含Web前端学习路径全部教程笔记，打下坚实的基础","Web前端 系列笔记",{"books":421,"desc":458,"id":423,"image":368,"title":459},[422,432,450],{"cateId":423,"chapters":424,"desc":429,"id":426,"time":163,"title":430,"video":431},100,[425],{"bookId":426,"id":427,"indexOrder":13,"name":428},20,"o0ab271mkdsas87","Markdown基础语法","编写简洁而又优美的文档","Markdown教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1eJ4m157kC",{"cateId":423,"chapters":433,"desc":447,"id":435,"time":60,"title":448,"video":449},[434,438,441,444],{"bookId":435,"id":436,"indexOrder":13,"name":437},14,"6386mh7anqt4tzyv","设计模式（一）面向对象设计原则",{"bookId":435,"id":439,"indexOrder":13,"name":440},"8ftkb38wfn6ox0ug","设计模式（二）创建型",{"bookId":435,"id":442,"indexOrder":13,"name":443},"i1msql1k8y70etey","设计模式（三）结构型",{"bookId":435,"id":445,"indexOrder":13,"name":446},"5434a3cyyjvwhs8s","设计模式（四）行为型","使你的编码水平得到质的飞跃","设计模式系列","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1u3411P7Na\u002F",{"cateId":423,"chapters":451,"desc":456,"id":453,"time":60,"title":457},[452],{"bookId":453,"id":454,"indexOrder":13,"name":455},15,"zj9uvg0sp3b0sok8","Docker 容器技术 笔记","这里包含其他中间件课程笔记","其他中间件笔记","我们对知识的探索从未停止，只有不断地学习，才能走向美好的未来！","其他笔记分类 🌽",200,true,{"data":463,"status":460,"success":461},{"bookId":153,"content":464,"id":157,"indexOrder":66,"introduction":465,"lastUpdate":466,"name":158},"![QQ_1723876184371](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F17\u002FdBvqmlkx2TU8fHW.png)\n\n# Mybatis快速上手\n\n**注意：** 开始本课程前，务必先完成MySQL视频课程、JavaWeb JDBC部分、Lombok视频课程学习。\n\n在前面JDBC的学习中，虽然我们能够通过JDBC来连接和操作数据库，但是这实在是太麻烦了，哪怕只是完成一个SQL语句的执行，都需要编写大量的代码，更不用说如果我还需要进行实体类映射，将数据转换为我们可以直接操作的实体类型，JDBC虽然进行了接口的定义，但是还不够方便，我们需要一种更加简洁高效的方式来和数据库进行交互。\n\n接下来隆重介绍我们本章节的主角：\n\n![image-20230306163528771](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F12\u002FJ7rWH6iIqUMEgjP.png)\n\nMyBatis 是一款优秀的持久层框架，它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息，将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。\n\n这一块内容很多很杂，各位小伙伴一定要多实践，多尝试，否则会忘得很快。\n\n## 走进Mybatis\n\n在正式使用Mybatis之前，我们先来学习一些前置内容和准备工作。\n\n### XML语言概述\n\n在开始介绍Mybatis之前，我们先来给大家介绍一下XML语言，XML语言发明最初是用于数据的存储和传输，它是由一个一个的标签嵌套而成，一般长这样：\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\u003Couter>\n  \u003Cname>阿伟\u003C\u002Fname>\n  \u003Cdesc>怎么又在玩电动啊\u003C\u002Fdesc>\n\t\u003Cinner type=\"1\">\n    \u003Cage>10\u003C\u002Fage>\n    \u003Csex>男\u003C\u002Fsex>\n  \u003C\u002Finner>\n\u003C\u002Fouter>\n```\n\n如果你学习过前端知识，你会发现它和HTML几乎长得一模一样！但是请注意，虽然它们长得差不多，但是他们的意义却不同，HTML主要用于通过编排来展示数据，而XML主要是存放数据，它更像是一个配置文件！当然，浏览器也是可以直接打开XML文件的。\n\n一个XML文件存在以下的格式规范：\n\n- 必须存在一个根节点，将所有的子标签全部包含。\n- 可以但不必须包含一个头部声明（主要是可以设定编码格式）\n- 所有的标签必须成对出现，有些可以单独，可以嵌套但不能交叉嵌套\n- 区分大小写。\n- 标签中可以存在属性，比如上面的`type=\"1\"`就是`inner`标签的一个属性，属性的值由单引号或双引号包括。\n\nXML文件也可以使用注释：\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\u003C!-- 注释内容 -->\n```\n\n那如果我们的内容中出现了`\u003C`或是`>`字符，那该怎么办呢？我们就可以使用XML的转义字符来代替：\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F12\u002FbYBle7VIq6DNang.jpg)\n\n如果嫌一个一个改太麻烦，也可以使用CD来快速创建不解析区域：\n\n```xml\n\u003Ctest>\n    \u003Cname>\u003C![CDATA[我看你\u003C>\u003C>\u003C>是一点都不懂哦>>>]]>\u003C\u002Fname>\n\u003C\u002Ftest>\n```\n\n那么，我们现在了解了XML文件的定义，现在该如何去解析一个XML文件呢？比如我们希望将定义好的XML文件读取到Java程序中，这时该怎么做呢？\n\nJDK为我们内置了一个叫做`org.w3c`的XML解析库，我们来看看如何使用它来进行XML文件内容解析：\n\n```java\n\u002F\u002F 创建DocumentBuilderFactory对象\nDocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n\u002F\u002F 创建DocumentBuilder对象\ntry {\n    DocumentBuilder builder = factory.newDocumentBuilder();\n    Document d = builder.parse(\"file:mappers\u002Ftest.xml\");\n    \u002F\u002F 每一个标签都作为一个节点\n    NodeList nodeList = d.getElementsByTagName(\"test\");  \u002F\u002F 可能有很多个名字为test的标签\n    Node rootNode = nodeList.item(0); \u002F\u002F 获取首个\n\n    NodeList childNodes = rootNode.getChildNodes(); \u002F\u002F 一个节点下可能会有很多个节点，比如根节点下就囊括了所有的节点\n    \u002F\u002F节点可以是一个带有内容的标签（它内部就还有子节点），也可以是一段文本内容\n\n    for (int i = 0; i \u003C childNodes.getLength(); i++) {\n        Node child = childNodes.item(i);\n        if(child.getNodeType() == Node.ELEMENT_NODE)  \u002F\u002F过滤换行符之类的内容，因为它们都被认为是一个文本节点\n            System.out.println(child.getNodeName() + \"：\" +child.getFirstChild().getNodeValue());\n        \u002F\u002F 输出节点名称，也就是标签名称，以及标签内部的文本（内部的内容都是子节点，所以要获取内部的节点）\n    }\n} catch (Exception e) {\n    e.printStackTrace();\n}\n```\n\n当然，学习和使用XML只是为了更好地去认识Mybatis的工作原理，以及如何使用XML来作为Mybatis的配置文件，这是在开始之前必须要掌握的内容（使用Java读取XML内容不要求掌握，但是需要知道Mybatis就是通过这种方式来读取配置文件的）\n\n不仅仅是Mybatis，包括后面的Spring等众多框架都会用到XML来作为框架的配置文件。\n\n### 初次使用MyBatis\n\n那么我们首先来感受一下Mybatis给我们带来的便捷，就从搭建环境开始，中文文档网站：https:\u002F\u002Fmybatis.org\u002Fmybatis-3\u002Fzh_CN\u002Fgetting-started.html，还是老规矩，我们直接先下载好Mybatis的依赖Jar包：https:\u002F\u002Fgithub.com\u002Fmybatis\u002Fmybatis-3\u002Freleases\n\n如果使用Maven可以直接引入：\n\n```xml\n\u003Cdependency>\n  \u003CgroupId>org.mybatis\u003C\u002FgroupId>\n  \u003CartifactId>mybatis\u003C\u002FartifactId>\n  \u003Cversion>3.5.16\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n依赖变多之后，我们可以将其放到一个单独的文件夹，不然会很繁杂：\n\n![QQ_1723452238551](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F12\u002FY7ykK3idxSvUT1r.png)\n\n依赖导入完成后，我们就可以编写Mybatis的配置文件了（现在不是在Java代码中配置了，而是通过一个XML文件去配置，这样就使得硬编码的部分大大减少，项目后期打包成Jar运行不方便修复，但是通过配置文件，我们随时都可以去修改，就变得很方便了，同时代码量也大幅度减少，配置文件填写完成后，我们只需要关心项目的业务逻辑而不是如何去读取配置文件）我们按照官方文档给定的提示，在项目根目录下新建名为`mybatis-config.xml`的文件，并填写以下内容：\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\u003C!DOCTYPE configuration\n        PUBLIC \"-\u002F\u002Fmybatis.org\u002F\u002FDTD Config 3.0\u002F\u002FEN\"\n        \"http:\u002F\u002Fmybatis.org\u002Fdtd\u002Fmybatis-3-config.dtd\">\n```\n\n我们发现，在最上方还引入了一个叫做DTD（文档类型定义）的东西，它提前帮助我们规定了一些标签以及这些标签应该具有哪些属性，这些标签是Mybatis专属的配置标签，我们也必须要按照对应的方式来进行配置。\n\n接着我们就可以使用DTD中声明的各种标签进行配置的编写了，最外层需要使用`configuration`标签进行囊括，内部首先需要编写环境配置，我们的项目可能会分多个环境进行编写，比如我们在开发的时候可能连接的是自己本地的数据库，但是项目部署到服务器上之后，可能会连接其他的数据库，所以我们可以提供多个环境配置，只不过这里我们只是做开发，只需要配置一个`environment`即可：\n\n```xml\n\u003Cconfiguration>\n    \u003Cenvironments default=\"development\">\n        \u003Cenvironment id=\"development\">\n            \u003CtransactionManager type=\"JDBC\"\u002F>\n            \u003CdataSource type=\"POOLED\">\n                \u003Cproperty name=\"driver\" value=\"com.mysql.cj.jdbc.Driver\"\u002F>\n                \u003Cproperty name=\"url\" value=\"jdbc:mysql:\u002F\u002Flocalhost:3306\u002Fweb_study\"\u002F>\n                \u003Cproperty name=\"username\" value=\"test\"\u002F>\n                \u003Cproperty name=\"password\" value=\"123456\"\u002F>\n            \u003C\u002FdataSource>\n        \u003C\u002Fenvironment>\n    \u003C\u002Fenvironments>\n\u003C\u002Fconfiguration>\n```\n\n在`environment`中我们可以配置两个部分，一个是事务管理器，还有一个是数据源，事务管理器我们这里直接使用JDBC即可，有关其他事务管理器我们会在后续Spring课程中为大家介绍。数据源中配置就是驱动、连接地址、用户名和密码，按照之前JDBC的方式进行配置即可，最后还有一个数据源类型配置我们也会放在后续Spring阶段再给各位小伙伴进行介绍。\n\n配置文件完成后，接着我们就可以通过Java来使用Mybatis了，首先我们要介绍的是`SqlSessionFactoryBuilder`它用于构建`SqlSessionFactory`对象，每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的，使用也很简单，这里我们使用的是XML的形式进行配置的：\n\n```java\npublic static void main(String[] args) throws FileNotFoundException {\n  \t\u002F\u002F使用build方法来创建SqlSessionFactory，这里我们通过文件输入流传入配置文件\n    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream(\"mybatis-config.xml\"));\n}\n```\n\n虽然SqlSessionFactory也可以使用`Configuration`进行纯Java配置，但是相比XML文件来说，配置起来非常麻烦，要构建大量对象，这里我们就不仅讲解了，各位小伙伴也可以前往官方文档深入了解：https:\u002F\u002Fmybatis.org\u002Fmybatis-3\u002Fzh_CN\u002Fgetting-started.html\n\n```java\npublic SqlSessionFactory build(InputStream inputStream);\n\u002F\u002F手动指定配置文件中多个环境的其中一个\npublic SqlSessionFactory build(InputStream inputStream, String environment);\n\u002F\u002F手动配置一些属性，通过Properties对象来传递\npublic SqlSessionFactory build(InputStream inputStream, Properties properties);\npublic SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);\n```\n\n既然有了 SqlSessionFactory，顾名思义，我们可以从中获得 SqlSession 的实例，每一个SqlSession都代表一个会话，也就相当于我们之前通过命令行访问MySQL的一个窗口，不同会话之间相互隔离，不受影响：\n\n```java\ntry (SqlSession session = sqlSessionFactory.openSession(true)) {\n    \u002F\u002F由于SqlSession需要在使用结束后关闭，这里我们也可以使用try-with-resource来编写\n    \u002F\u002F这里的参数是设置自动提交，和之前JDBC一样\n}\n```\n\n![QQ_1723455145061](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F12\u002Feq3LwI1paXucYNl.png)\n\nSqlSession接口中为我们预设了非常多的增删改查操作：\n\n```java\npublic interface SqlSession extends Closeable {\n    \u003CT> T selectOne(String statement);\n    \u003CT> T selectOne(String statement, Object parameter);\n    \u003CE> List\u003CE> selectList(String statement);\n    ...\n    int insert(String statement);\n    int insert(String statement, Object parameter);\n    int update(String statement);\n    int update(String statement, Object parameter);\n    int delete(String statement);\n    int delete(String statement, Object parameter);\n```\n\n可以看到，针对于增删改查这类操作，Mybatis已经把每一种操作都预设好了，并且查询的返回结果直接就是我们需要的实体类型，相比JDBC来说方便太多了，那么我们该如何去使用这些方法呢？由于Mybatis并不知道我们具体需要执行的SQL语句，以及需要返回哪些数据作为结果，因此我们同样需要编写配置文件来告诉Mybatis我们要做什么。\n\n我们可以在项目目录下创建一个新的`mappers`目录，然后创建一个新的文件`TestMapper.xml`作为我们的SQL语句映射配置，并添加以下内容：\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\u003C!DOCTYPE mapper\n        PUBLIC \"-\u002F\u002Fmybatis.org\u002F\u002FDTD Mapper 3.0\u002F\u002FEN\"\n        \"http:\u002F\u002Fmybatis.org\u002Fdtd\u002Fmybatis-3-mapper.dtd\">\n\u003Cmapper namespace=\"testMapper\">\n  \n\u003C\u002Fmapper>\n```\n\n这里我们添加了映射器（Mapper）的DTD来方便我们后续的配置，其中mapper标签用于囊括后续编写的所有SQL映射，`namespace`属性用于区分不同的映射器（一个项目可以有多个Mapper配置文件，用于分类存放不同业务的SQL映射）以及后续我们会介绍的接口绑定等。接着我们就可以开始尝试编写一个测试用的SQL语句映射了，假设我们需要查询user表中所有数据，首先构建一个实体类：\n\n```java\n@Data\npublic class User {   \u002F\u002F属性名称必须和数据库中字段名称一一对应，不然会赋值失败\n    int id;\n    String name;\n    int age;\n}\n```\n\n接着我们在配置文件中添加一个新的映射，由于这里我们需要使用到`select`语句，所以使用名字为select的标签：\n\n```xml\n\u003Cselect id=\"selectAllUser\" resultType=\"com.test.User\">\n    select * from user\n\u003C\u002Fselect>\n```\n\n由于一个Mapper文件中可以存在多个SQL语句映射，这里的`id`是用于区分其他SQL语句映射的，后面的`resultType`代表SQL语句查询结果需要转换的实体类型，其他未使用的参数我们会在后续课程中逐步介绍。标签的中间就是我们具体要进行查询的SQL语句了，这和我们之前使用JDBC是差不多的。\n\n现在我们已经完成了Mapper配置文件的编写，接着我们需要将其添加到一开始的Mybatis配置文件中，使得Mybatis可以在一开始的时候正常加载。\n\n```xml\n\u003Cconfiguration>\n    ...\n    \u003Cmappers>\n        \u003Cmapper url=\"file:mappers\u002FTestMapper.xml\"\u002F>\n    \u003C\u002Fmappers>\n\u003C\u002Fconfiguration>\n```\n\n最后在程序中使用我们定义好的SQL语句映射也很简单，假设现在我们需要执行刚刚编写好的查询操作，只需要通过SqlSession提供的预设方法即可：\n\n```java\ntry (SqlSession session = sqlSessionFactory.openSession(true)) {\n    List\u003CUser> users = session.selectList(\"selectAllUser\");  \u002F\u002F直接填写我们刚刚编写的映射id\n    users.forEach(System.out::println);  \u002F\u002F直接查询并自动转换为对应类型\n}\n```\n\nMybatis非常智能，只需要配置一个映射关系，就能够直接将查询结果转化为一个实体类，属性会自动按照字段名称进行一一对应，免去了我们之前使用JDBC查询实体类的很多步骤。\n\n## Mybatis详解\n\n从这一部分开始我们就正式来学习一下Mybatis的使用方式。\n\n### 查询操作\n\n前面我们带各位小伙伴大概熟悉了一下Mybatis的配置流程，这一节我们来详细介绍一下如何配置查询操作。由于`SqlSessionFactory`一般只需要创建一次，因此我们可以创建一个工具类来集中创建`SqlSession`，这样会更加方便一些：\n\n```java\npublic class MybatisUtil {\n\n    \u002F\u002F在类加载时就进行创建\n    private static SqlSessionFactory sqlSessionFactory;\n    static {\n        try {\n            sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream(\"mybatis-config.xml\"));\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n        }\n    }\n\n    \u002F**\n     * 获取一个新的会话\n     * @param autoCommit 是否开启自动提交（跟JDBC是一样的，如果不自动提交，则会变成事务操作）\n     * @return SqlSession对象\n     *\u002F\n    public static SqlSession openSession(boolean autoCommit){\n        return sqlSessionFactory.openSession(autoCommit);\n    }\n}\n```\n\n现在我们只需要在main方法中直接使用工具类就能快速创建一个新的会话，然后查询结果了：\n\n```java\ntry(SqlSession sqlSession = MybatisUtil.openSession(true)) {\n    List\u003CUser> users = sqlSession.selectList(\"selectUser\");\n    users.forEach(System.out::println);\n}\n```\n\n查询操作在XML配置中使用一个select标签进行囊括，这里我们通过一个例子来介绍一下最基本的查询需要配置的参数，假设我们现在需要编写一个根据ID查询用户的操作，首先我们需要指定它的id，建议把id名称起的有代表性一点：\n\n```xml\n\u003Cselect id=\"selectUserById\">  \n\u003C\u002Fselect>\n```\n\n接着是我们需要进行查询的参数，这里我们需要根据用户ID查询，那么传入的参数就是一个int类型的参数，参数也可以是字符串类型的，类型名称：\n\n1. 如果是基本类型，需要使用`_int`这样前面添加下划线。\n2. 如果是JDK内置的包装类型或是其他类型，可以直接使用其名称，比如`String`、`int`（Integer的缩写）、`Long`\n3. 如果是自己编写的类型，需要完整的包名+类名才可以。\n\n当然，如果各位小伙伴觉得非常麻烦，我们也可以直接不填这个属性，Mybatis会自动判断：\n\n```xml\n\u003Cselect id=\"selectUserById\" parameterType=\"int\">\n\u003C\u002Fselect>\n```\n\n接下来就是编写我们的SQL语句了，由于这里我们需要通过一个参数来查询，所以需要填入一个占位符，通过使用`#{xxx}`或是`${xxx}`来填入我们给定的属性，名称我们先随便起一个：\n\n```xml\n\u003Cselect id=\"selectUserById\" parameterType=\"int\">\n    select * from user where id = #{id}\n\u003C\u002Fselect>\n```\n\n实际上Mybatis也是通过`PreparedStatement`首先进行一次预编译，来有效地防止SQL注入问题，但是如果使用`${xxx}`就不再是通过预编译，而是直接传值，因此对于常见的一些查询参数，我们一般都使用`#{xxx}`来进行操作保证安全性。\n\n最后我们查询到结果后，一般都是将其转换为对应的实体类对象，所以说这里我们之间填写之前建好的实体类名称，使用resultType属性来指定：\n\n```xml\n\u003Cselect id=\"selectUserById\" parameterType=\"int\" resultType=\"com.test.User\">\n    select * from user where id = #{id}\n\u003C\u002Fselect>\n```\n\n当然，如果你觉得像这样每次都要写一个完整的类名太累了，也可以为它起个别名，我们只需要在Mybatis的配置文件中进行编写即可：\n\n```xml\n\u003CtypeAliases>\n    \u003CtypeAlias type=\"com.test.User\" alias=\"User\"\u002F>\n\u003C\u002FtypeAliases>\n```\n\n也可以直接扫描整个包下的所有实体类，自动起别名，默认情况下别名就是类的名称：\n\n```xml\n\u003CtypeAliases>\n    \u003Cpackage name=\"com.test.entity\"\u002F>\n\u003C\u002FtypeAliases>\n```\n\n这样，SQL语句映射配置我们就编写好了，接着就是Java这边进行调用了：\n\n```java\n\u002F\u002F这里我们填写刚刚的id，然后将我们的参数填写到后面\nUser user = session.selectOne(\"selectUserById\", 1);\nSystem.out.println(user);\n```\n\n这样就可以成功查询到ID为1的用户信息了：\n\n![QQ_1723625949253](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F14\u002FiQcgRtofhPlDU7T.png)\n\n可以看到Mybatis直接省去了我们之前使用JDBC读取ResultSet的部分，直接转换为对应的实体类，当然，如果你不需要转换为实体类，Mybatis也为我们提供了多种转换方案，比如转换为一个Map对象：\n\n```java\n\u002F\u002F使用Map类型变量进行接受，Key为String类型，Value为Object类型\nMap\u003CString, Object> user = session.selectOne(\"selectUserById\", 1);\nSystem.out.println(user);\n```\n\n我们可以尝试接着来写一个同时查询ID和年龄的查询操作，为了方便这里我们就不写parameterType属性了：\n\n```xml\n\u003Cselect id=\"selectUserByIdAndAge\" resultType=\"com.test.User\">\n    select * from user where id = #{id} and age = #{age}\n\u003C\u002Fselect>\n```\n\n因为这里需要多个参数，我们可以使用一个Map或是具有同样参数的实体类来传递，显然Map用起来更便捷一些，注意key的名称需要与我们编写的SQL语句中占位符一致：\n\n```java\nUser user = session.selectOne(\"selectUserByIdAndAge\", Map.of(\"id\", 1, \"age\", 18));\nSystem.out.println(user);\n```\n\n是不是感觉还是挺简单的？我们接着来看下面这种情况，实体类中定义的属性名称和我们数据库中的名称似乎有点不太一样，这会导致Mybatis自动处理出现问题：\n\n```java\n@Data\npublic class User {\n    int uid;\n    String username;\n    int age;\n}\n```\n\n运行后发现，Mybatis虽然可以查询到对应的记录，但是转换的实体类数据并没有被添加上去，这是因为数据库字段名称与类中字段名称不匹配导致的，我们可以手动配一个resultMap来解决这种问题，直接在Mapper中添加：\n\n```xml\n\u003Cselect id=\"selectUserByIdAndAge\" resultMap=\"user\">\n    select * from user where id = #{id} and age = #{age}\n\u003C\u002Fselect>\n\u003CresultMap id=\"user\" type=\"com.test.User\">\n  \t\u003C!-- 因为id为主键，这里也可以使用\u003Cid>标签，有助于提高性能 -->\n    \u003Cresult column=\"id\" property=\"uid\"\u002F>\n    \u003Cresult column=\"name\" property=\"username\"\u002F>\n\u003C\u002FresultMap>\n```\n\n这里我们在resultMap标签中配置了一些result标签，每一个result标签都可以配置数据库字段和类属性的对应关系，这样Mybatis就可以按照我们的配置来正确找到对应的位置并赋值了，没有手动配置的字段会按照之前默认的方式进行赋值。配置完成后，最终只需要将resultType改为resultMap并指定对应id即可，然后就能够正确查询了。\n\n这里有一个RowBounds参数，用于实现分页效果，但是其分页功能是对查询到的数据进行划分，非常鸡肋，这里不进行介绍，了解即可。\n\n我们再来尝试编写一下查询一个列表，查询列表时，resultType无需设置为list这种类型，而是使用List内部所包含的类型，所以这里还是填写`com.test.User`类型或是Map类型：\n\n```xml\n\u003Cselect id=\"selectUsers\" resultType=\"com.test.User\">\n    select * from user;\n\u003C\u002Fselect>\n```\n\n由于返回的结果是一个列表，这里我们需要使用`selectList`方法来执行，如果使用之前的`selectOne`会导致异常：\n\n```java\nList\u003CUser> user = session.selectList(\"selectUsers\");\nSystem.out.println(user);\n```\n\n我们同样可以进行简单的条件查询，比如我们想要查询所有年龄大于等于18岁的用户：\n\n```xml\n\u003Cselect id=\"selectUsersByAge\" resultType=\"com.test.User\">\n    select * from user where age &gt; #{age};\n\u003C\u002Fselect>\n```\n\n注意由于这里是XML配置，其中一些字符被用作标签表示，无法代表其原本的意思，比如小于、大于符号，分别需要使用`&lt;`和`&gt;`来进行转义。\n\n```java\nList\u003CUser> user = session.selectList(\"selectUsersByAge\", 18);\n```\n\n我们接着来看一个比较特殊的选择方法`selectMap`，它可以将查询结果以一个Map的形式表示，只不过这和我们之前说的Map不太一样，它返回的Map是使用我们想要的属性作为Key，然后得到的结果作为Value的Map，它适用于单个数据查询或是多行数据查询：\n\n```java\n\u002F\u002F最后一个参数为我们希望作为key的属性\nMap\u003CString, User> user = session.selectMap(\"selectUserById\", 1, \"id\");\n```\n\n此时得到的结果就是：\n\n![QQ_1723711033070](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F15\u002FskVRAyvo8KqjxE7.png)\n\n可以看到这个Map中确实使用的是id作为Key，然后查询得到的实体对象作为Value。\n\n还有一个比较特殊的选择操作是`selectCursor`，它可以得到一个`Cursor`对象，同样是用于列表查询的，只不过使用起来和我们之前JDBC中的ResultSet比较类似，也是通过迭代器的形式去进行数据的读取，官方解释它主要用于惰性获取数据，提高性能：\n\n```java\npublic interface Cursor\u003CT> extends Closeable, Iterable\u003CT> { ... }\n```\n\n可以看到它本身是实现了Iterable接口的，表明它可以获取迭代器或是直接使用foreach来遍历：\n\n```java\nCursor\u003CUser> cursor = session.selectCursor(\"selectUsers\");\nfor (User user : cursor) {\n    System.out.println(user);\n}\n```\n\n只不过这种方式在大部分请情况下还是用的比较少，我们主要还是以`selectOne`和`selectList`为主。\n\n最后还有一个普通的`select`方法，它支持我们使用Lambda的形式进行查询结果的处理：\n\n```java\nsession.select(\"selectUsers\", context -> {  \u002F\u002F使用ResultHandler来处理结果\n    System.out.println(context.getResultObject());\n});\n```\n\n结果会自动进行遍历并依次执行我们传入的Lambda表达式。\n\n### 指定构造方法\n\n通过前面的学习，我们已经知道如何使用Mybatis进行各种查询操作。我们知道，Mybatis在执行完查询语句后，会自动将查询的结果转换为我们所需要的实体类，那么它具体是怎么做的呢？\n\n实际上Mybatis一开始会通过我们实体类默认的无参构造得到一个最初的对象，然后通过反射进行赋值，我们可以手动编写一个带调试信息的无参构造：\n\n```java\npublic User() {\n    System.out.println(\"????\");\n}\n```\n\n![QQ_1723726330745](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F15\u002F7vFVPi3xXSfGtye.png)\n\n可以看到Mybatis确实调用了我们的无参构造方法来构建对象，属性则是通过反射进行赋值，这里截取部分Mybatis源代码进行演示：\n\n```java\n\u002F\u002F这里的object就是刚刚构造好的实体类对象，prop是要设置的值的字段信息，value就是要设置的值\nprivate void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {\n  try {\n    \u002F\u002FInvoker是Mybatis内部编写一个用于反射设置对象属性值的工具\n    Invoker method = metaClass.getSetInvoker(prop.getName());\n    Object[] params = { value };\n    try {\n      method.invoke(object, params);  \u002F\u002F通过Invoker为传入的实体类对象赋值\n    } catch (Throwable t) {\n      throw ExceptionUtil.unwrapThrowable(t);\n    }\n  } catch (Throwable t) {\n    ...\n  }\n}\n```\n\n由于Mybatis默认情况下直接通过无参构造来创建实体类对象，如果我们的类中存在其他的构造方法覆盖掉默认的无参构造，那么Mybatis会选择可用的构造方法来进行构造。但是如果存在多个构造方法，Mybatis会出现问题：\n\n```java\n@ToString\npublic class User {\n    ...\n\n    public User(int id) {\n        this.id = id;\n    }\n    \n    public User(String name, int age) {\n        this.name = name;\n        this.age = age;\n    }\n}\n```\n\n运行时出现错误：\n\n```\nException in thread \"main\" org.apache.ibatis.exceptions.PersistenceException: \n### Error querying database.  Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.User matching [java.lang.Integer, java.lang.String, java.lang.Integer]\n### The error may exist in file:mappers\u002FTestMapper.xml\n```\n\n此时由于类中存在多个构造方法，而Mybatis不知道该如何选择，那么就会告诉我们找不到合适的构造方法，要解决这种问题也很简单，我们不需要删除这些多余的构造方法，只需添加一个无参构造或是全参构造即可，注意全参构造必须与查询结果字段参数一一对应。但是注意，Mybatis仅仅是使用这种方式进行对象的构建，而字段的赋值无论是什么构造方法，都会使用反射进行一次赋值：\n\n```java\npublic User(int id, String name, int age) {\n    this.id = id;\n    this.name = name;\n    this.age = age + 20;   \u002F\u002F这里我们让age在赋值时增加一次\n}\n```\n\n我们会发现，就算像这样进行了修改，最终的结果依然是被赋值为数据库中的结果，也就是说构造方法在默认情况下仅仅只是用于构造一个单纯的对象罢了。\n\n如果需要让Mybatis完全使用构造方法进行对象构建与赋值工作，那么我们需要在XML中手动编写配置，同样需要使用resultMap来完成：\n\n```xml\n\u003Cselect id=\"selectUserById\" resultMap=\"test\">\n    select * from user where id = #{id}\n\u003C\u002Fselect>\n\u003CresultMap id=\"test\" type=\"com.test.User\">\n    \u003Cconstructor>\n            \n    \u003C\u002Fconstructor>\n\u003C\u002FresultMap>\n```\n\n这一次我们在resultMap中添加constructor标签，表示我们的查询结果直接使用指定的构造方法来处理。接着我们需要配置一下constructor里面的内容，使其符合我们指定构造方法的定义，比如现在我们有一个这样的构造方法：\n\n```java\npublic User(int id, String name) {\n    this.id = id;\n    this.name = name + \"同学\";\n}\n```\n\n那么对应的XML配置编写为，使用arg标签来代表每一个参数，主键可以使用idArg来表示，有助于优化性能：\n\n```xml\n\u003Cconstructor>\n    \u003CidArg column=\"id\" javaType=\"_int\"\u002F>\n    \u003Carg column=\"name\" javaType=\"String\"\u002F>\n\u003C\u002Fconstructor>\n```\n\n注意参数的顺序，必须和构造方法的顺序一致，否则会导致Mybatis无法确认。指定构造方法后，若此字段被填入了构造方法作为参数，将不会通过反射给字段单独赋值，而构造方法中没有传入的字段，依然会被反射赋值。\n\n### 接口绑定\n\n之前我们演示了，如何创建一个映射器来将结果快速转换为实体类，但是这样可能还是不够方便，我们每次都需要去找映射器对应操作的名称，而且还要知道对应的返回类型，再通过`SqlSession`来执行对应的方法，能不能再方便一点呢？\n\n我们可以通过`namespace`来将各种操作绑定到一个接口上，然后使用方法的形式来表示，注意接口的参数和返回值必须正确对应，否则可能会出现问题：\n\n```java\npublic interface TestMapper {\n    User selectUserById(int id);\n}\n```\n\n接着将Mapper文件的命名空间修改为我们的接口完整名称：\n\n```xml\n\u003Cselect id=\"selectUserById\" resultType=\"com.test.User\">\n    select * from user where id = #{id}\n\u003C\u002Fselect>\n```\n\n这里建议将对应的xml配置也放到放到同包中，作为内部资源：\n\n![QQ_1723457472353](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F12\u002FN3bTsDLtlS1mckq.png)\n\n作为内部资源后，我们需要修改一下配置文件中的mapper文件目录，不使用url而是resource表示是Jar内部的文件：\n\n```xml\n\u003Cmappers>\n    \u003Cmapper resource=\"com\u002Ftest\u002Fmapper\u002FTestMapper.xml\"\u002F>\n\u003C\u002Fmappers>\n```\n\n现在我们可以直接通过`SqlSession`获取我们编写接口的实现类，这个实现类是由Mybatis根据我们的配置自动生成的，不需要我们做任何事情：\n\n```java\ntry(SqlSession sqlSession = MybatisUtil.openSession(true)) {\n    TestMapper mapper = sqlSession.getMapper(TestMapper.class);   \u002F\u002F直接获取实现类\n    \u002F\u002F这里调用我们编写的接口方法\n  \tmapper.selectUser().forEach(System.out::println);\n}\n```\n\n是不是感觉非常强大？\n\n那肯定有人好奇，TestMapper明明是一个我们自己定义接口啊，Mybatis也不可能提前帮我们写了实现类啊，那这接口怎么就出现了一个实现类呢？我们可以通过调用`getClass()`方法来看看实现类是个什么：\n\n```java\nTestMapper testMapper = sqlSession.getMapper(TestMapper.class);\nSystem.out.println(testMapper.getClass());\n```\n\n我们发现，得到的类名称很奇怪`class jdk.proxy2.$Proxy4`，它其实是通过动态代理生成的，相当于在程序运行过程中动态生成了一个实现类，而不是预先定义好的，有关Mybatis这一部分的原理，我们放在最后一节进行讲解。\n\n我们接着来看更方便的用法，有些时候，我们的查询操作可能需要不止一个参数：\n\n```xml\n\u003Cselect id=\"selectUserByIdAndAge\" resultType=\"com.test.entity.User\">\n    select * from user where id = #{id} and age = #{age}\n\u003C\u002Fselect>\n```\n\n一种最简单的方式就是和之前一样，我们使用一个Map作为参数，然后将这些参数添加到Map中进行传递：\n\n```java\nUser selectUserByIdAndAge(Map\u003CString, Object> map);\n```\n\n```java\nTestMapper mapper = session.getMapper(TestMapper.class);\nSystem.out.println(mapper.selectUserByIdAndAge(Map.of(\"id\", 1, \"age\", 18)));\n```\n\n只不过，这样编写实在是太复杂了，要是由一种更简单的方式就好了，我们也可以直接将这两个参数定义到形参列表中：\n\n```java\nUser selectUserByIdAndAge(int id, int age);\n```\n\n只不过这种方式查询的话，Mybatis会并不能正确获取对应的参数：\n\n```\n### Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]\n\tat org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)\n```\n\n这是因为Java代码编译后形参名称无法保留，导致Mybatis无法确定具体哪个参数交什么名字，所以默认情况下它们将会以 param 加上它们在参数列表中的位置来命名，比如：#{param1}、#{param2}等，这里id实际上就是param1：\n\n```sql\nselect * from user where id = #{param1} and age = #{param2}\n```\n\n当然，如果你实在需要使用对应的属性名称，我们也可以手动添加一个`@Param`注解来指定某个参数的名称：\n\n```java\nUser selectUserByIdAndAge(@Param(\"id\") int id, @Param(\"age\") int age);\n```\n\n这样Mybatis就可以正确识别了。\n\n### 复杂查询\n\n前面我们介绍了简单查询，只不过有些时候可能会遇到比较麻烦的查询，这一节我们来介绍一下一对一、一对多、多对一的查询实现方式。\n\n首先来看最简单的一对一查询，假设我们每个用户都有一个自己的详细信息表：\n\n![QQ_1723792352474](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F16\u002FM4SX2Woa9CybQUw.png)\n\n这里的id与用户id一致，作为用户id的逻辑外键，表示对应用户的详细信息。对应的实体类为：\n\n```java\n@Data\npublic class UserDetail {\n    int id;\n    String description;\n    Date register;\n    String avatar;\n}\n\n@Data\npublic class User {\n    int id;\n    String name;\n    int age;\n    UserDetail detail;\n}\n```\n\n现在我们希望查询User时，同时将用户的详细信息包含在内，像这种一对一查询该怎么实现呢？我们现在同样需要使用`resultMap`来自定义映射规则\n\n```xml\n\u003Cselect id=\"selectUserById\" resultMap=\"test\">\n\u003C\u002Fselect>\n\u003CresultMap id=\"test\" type=\"com.test.entity.User\">\n\u003C\u002FresultMap>\n```\n\nMyBatis 有两种不同的方式加载关联：\n\n- 嵌套结果映射：使用嵌套的结果映射来处理连接结果的重复子集。\n- 嵌套 Select 查询：通过执行另外一个 SQL 映射语句来加载期望的复杂类型。\n\n我们先来看第一种方式，这里我们需要使用关联查询将用户的详细信息一并获取，然后配置关联查询相关信息，最后由Mybatis来对查询的结果进行处理即可，首先是关联查询的SQL语句，这里我们直接使用左连接：\n\n```sql\nselect * from user left join user_detail on user.id = user_detail.id where user.id = #{id}\n```\n\n![QQ_1723794339355](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F16\u002FITDpQg3dA7aL9o1.png)\n\n接着我们需要在resultMap编写好对应关系，一对一查询我们可以使用association标签来进行指定，其中property就是需要进行一对一处理的对象，在此标签内部填写需要进行一对一映射的对象属性：\n\n```xml\n\u003CresultMap id=\"test\" type=\"com.test.entity.User\">\n    \u003Cid property=\"id\" column=\"id\"\u002F>\n    \u003Cresult property=\"name\" column=\"name\"\u002F>\n    \u003Cresult property=\"age\" column=\"age\"\u002F>\n    \u003Cassociation property=\"detail\" column=\"id\" javaType=\"com.test.entity.UserDetail\">\n        \u003Cid property=\"id\" column=\"id\"\u002F>\n        \u003Cresult property=\"description\" column=\"description\"\u002F>\n        \u003Cresult property=\"register\" column=\"register\"\u002F>\n        \u003Cresult property=\"avatar\" column=\"avatar\"\u002F>\n    \u003C\u002Fassociation>\n\u003C\u002FresultMap>\n```\n\n这里的column和javaType可以不填，Mybatis一般情况下可以自动完成推断，配置完成后，我们在查询时Mybatis就可以自动把额外信息也封装好了：\n\n![QQ_1723796178911](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F16\u002F3e6fAudX7G9qFsU.png)\n\n我们接着来看第二种方式，使用嵌套select语句进行查询，也就是说，我们可以在查询user表的时候，同时查询user_detail表的对应信息，分别执行两个选择语句，最后再由Mybatis将其结果合并，效果和第一种方法是一样的：\n\n```xml\n\u003Cselect id=\"selectUserById\" resultMap=\"test\">\n    select * from user where id = #{id}\n\u003C\u002Fselect>\n\u003CresultMap id=\"test\" type=\"com.test.entity.User\">\n    \u003Cid property=\"id\" column=\"id\"\u002F>\n    \u003Cresult property=\"name\" column=\"name\"\u002F>\n    \u003Cresult property=\"age\" column=\"age\"\u002F>\n    \u003Cassociation property=\"detail\" column=\"id\" select=\"selectUserDetailById\" javaType=\"com.test.entity.UserDetail\"\u002F>\n\u003C\u002FresultMap>\n\n\u003Cselect id=\"selectUserDetailById\" resultType=\"com.test.entity.UserDetail\">\n    select * from user_detail where id = #{id}\n\u003C\u002Fselect>\n```\n\n这里我们分别配置了两个select标签用于分别查询用户基本信息和详细信息，并使用association标签的select属性来指定关联查询操作，得到结果是一样的：\n\n![QQ_1723796178911](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F16\u002F3e6fAudX7G9qFsU.png)\n\n我们可以开启Mybatis的日志来观察具体执行的操作，这里我们需要在Mybatis配置文件中添加：\n\n```xml\n\u003Csettings>\n    \u003Csetting name=\"logImpl\" value=\"STDOUT_LOGGING\"\u002F>\n\u003C\u002Fsettings>\n```\n\n这样Mybatis运行时就会打印日志到控制台了：\n\n```\n...\nOpening JDBC Connection\nCreated connection 1962329560.\n==>  Preparing: select * from user where id = ?\n==> Parameters: 1(Integer)\n\u003C==    Columns: id, name, age\n\u003C==        Row: 1, 小明, 18\n====>  Preparing: select * from user_detail where id = ?\n====> Parameters: 1(Integer)\n\u003C====    Columns: id, description, register, avatar\n\u003C====        Row: 1, 我是一个阳光开朗大男孩, 2024-08-16 15:15:03, https:\u002F\u002Fwww.baidu.com\n\u003C====      Total: 1\n\u003C==      Total: 1\nUser(id=1, name=小明, age=18, detail=UserDetail(id=1, description=我是一个阳光开朗大男孩, register=Fri Aug 16 15:15:03 CST 2024, avatar=https:\u002F\u002Fwww.baidu.com))\nClosing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@74f6c5d8]\nReturned connection 1962329560 to pool.\n```\n\nMybatis日志中，`==>`向右的箭头就是发送给MySQL服务器的SQL语句以及其参数列表，`\u003C==`向左的箭头就是得到的结果，可以看到这里一共执行了两次SQL语句，分别是user表和user_detail表的查询操作。\n\n一对一查询非常简单，我们接着来看一对多查询，现在来一个新的表，用于存放用户借阅的图书，对应实体类如下：\n\n```java\n@Data\npublic class Book {\n    int bid;\n    String title;\n}\n\n@Data\npublic class User {\n    int id;\n    String name;\n    int age;\n    List\u003CBook> books;   \u002F\u002F直接得到用户所属的所有书籍信息\n}\n```\n\n其中`book`表设计如下，其中uid作为用户id的逻辑外键，表示这本书是谁借的：\n\n![QQ_1723733068017](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F15\u002F8RlcWkFe7yb2nXT.png)\n\n对于一对多查询，我们也可以进行关联查询来让Mybatis自动解析结果并封装为对象，首先还是关联查询的SQL语句，这里我们让user左连接到book表中：\n\n```sql\nselect * from user left join book on user.id = book.uid where user.id = #{id}\n```\n\n![QQ_1723802885239](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F16\u002F4Ijp6iU5P9X2N8u.png)\n\n此时由于出现了多行数据，我们需要配置一个`collection`标签来让其可以正确处理关联的集合结果，Mybatis会根据我们配置的属性自动将关联数据变为一个集合并存放在对象中：\n\n```xml\n\u003CresultMap id=\"test\" type=\"com.test.entity.User\">\n    \u003Cid column=\"id\" property=\"id\"\u002F>\n    \u003Cresult column=\"name\" property=\"name\"\u002F>\n    \u003Cresult column=\"age\" property=\"age\"\u002F>\n    \u003Ccollection property=\"books\" ofType=\"com.test.entity.Book\">\n        \u003Cid column=\"bid\" property=\"bid\"\u002F>\n        \u003Cresult column=\"title\" property=\"title\"\u002F>\n    \u003C\u002Fcollection>\n\u003C\u002FresultMap>\n```\n\n我们需要在resultMap中完整编写需要查询对象的属性对应关系以及在collection中编写关联查询的集合内类型相关属性对应关系，当然这个关系哪怕只写一个Mybatis也可以自动推断其他的，不过建议还是写完整一点：\n\n![QQ_1723734667720](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F15\u002FudbylhUV7GT9znm.png)\n\n了解了一对多，那么多对一又该如何查询呢，比如每个用户现在都有一个小组，但是他们目前都是在同一个小组中，此时我们查询所有用户信息的时候，需要自动携带他们的小组：\n\n![QQ_1723818427656](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F16\u002Fgl5qZpvAc6Lk4Fi.png)\n\n这里我们需要修改一下user表来记录每一个用户所属的小组id，这里使用gid作为分组id的逻辑外键：\n\n![QQ_1723818596301](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F16\u002FdsWXt4pEATocyRO.png)\n\n接着是实体类设计：\n\n```java\n@Data\npublic class Group {\n    int id;\n    String name;\n}\n\n@Data\npublic class User {\n    int id;\n    String name;\n    int age;\n    Group group;\n}\n```\n\n接着就是编写SQL映射，实际上这里跟我们之前的一对一非常类似，我们只需要让查询出来的每一个用户都左连接分组信息即可，这样Mybatis就可以通过association来自动处理了：\n\n```sql\nselect *, groups.name as gname from user left join `groups` on user.gid = groups.id\n```\n\n注意SQL语句中一些字段存在歧义，我们需要手动为其起个别名，接着就是XML编写了：\n\n```xml\n\u003Cselect id=\"selectAllUser\" resultMap=\"test2\">\n    select *, groups.name as gname from user left join `groups` on user.gid = groups.id\n\u003C\u002Fselect>\n\u003CresultMap id=\"test2\" type=\"com.test.entity.User\">\n    \u003Cid column=\"id\" property=\"id\"\u002F>\n    \u003Cresult column=\"name\" property=\"name\"\u002F>\n    \u003Cresult column=\"age\" property=\"age\"\u002F>\n    \u003Cassociation property=\"group\">\n        \u003Cid column=\"gid\" property=\"id\"\u002F>\n        \u003Cresult column=\"gname\" property=\"name\"\u002F>\n    \u003C\u002Fassociation>\n\u003C\u002FresultMap>\n```\n\n这样我们就可以成功实现多对一查询了，这与之前的一对一比较类似。\n\n### DML操作\n\n前面我们介绍了查询操作，我们接着来看修改相关操作。Mybatis为我们的DML操作提供了几个预设方法：\n\n```java\nint insert(String statement);\nint insert(String statement, Object parameter);\nint update(String statement);\nint update(String statement, Object parameter);\nint delete(String statement);\nint delete(String statement, Object parameter);\n```\n\n可以看到，这些方法默认情况下返回的结果都是`int`类型的，这与我们之前JDBC中是一样的，它代表执行SQL后受影响的行数。\n\n我们来尝试编写一个插入操作，Mybatis为我们提供的插入操作非常快捷，我们可以直接让一个User对象作为参数传入，即可在配置中直接解析其属性到insert语句中，这里需要用到insert标签：\n\n```xml\n\u003Cinsert id=\"addUser\" parameterType=\"com.test.entity.User\">\n    insert into user (name, age) values (#{name}, #{age})\n\u003C\u002Finsert>\n```\n\n这里我们将parameterType类型设置为我们的实体类型，这样下面在使用`#{name}`时Mybatis就会自动调用类中对应的Get方法来获取结果，不过，即使这里不指定具体类型，Mybatis也能完成自动推断，非常智能。\n\n有些时候，我们的数据插入后使用的是一个自增主键ID，那么这个自增的主键值我们该如何获取到呢？Mybatis为我们提供了一些参数用于处理这种问题：\n\n```xml\n\u003Cinsert id=\"addUser\" parameterType=\"com.test.entity.User\" useGeneratedKeys=\"true\" keyProperty=\"id\" keyColumn=\"id\">\n    insert into user (name, age) values (#{name}, #{age})\n\u003C\u002Finsert>\n```\n\n这里`useGeneratedKeys`设置为`true`表示我们希望获取数据库生成的键，`keyProperty`设置为User类中的需要获取自增结果的属性名，`keyColumn`为数据库中自增的字段名称，但是一般情况下不需要手动设置，但是某些数据库（像 PostgreSQL）中，当主键列不是表中的第一列的时候，必须设置。\n\n这样我们就可以获取到自增后的值了，接着我们什么都不需要做，Mybatis会在查询完后自动为我们的User对象赋值：\n\n![QQ_1723824700173](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F17\u002FOUWu97HmpPNyB1K.png)\n\n是不是感觉很方便？和之前一样，我们也可以直接将其绑定到一个接口上：\n\n```java\npublic interface TestMapper {\n    int addUser(User user);\n}\n```\n\n注意返回类型必须是int或是long这类数字类型，表示生效的行数，然后这里我们传入的参数直接写成对应的类型即可。\n\n我们接着来看修改操作，比如要根据ID修改用户的年龄：\n\n```xml\n\u003Cupdate id=\"setUserAgeById\">\n    update user set age = #{age} where id = #{id}\n\u003C\u002Fupdate>\n```\n\n```java\nint setUserAgeById(User user);\n```\n\n这里的参数我们依然选择使用User，和之前insert一样，Mybatis会从传入的对象中自动获取需要的参数，当然我们也可以将此方法设计为两个参数的形式：\n\n```java\nint setUserAgeById(@Param(\"age\") int age, @Param(\"id\") int id);\n```\n\n删除操作则更为简单，假设我们要根据用户的id进行数据的删除：\n\n```xml\n\u003Cdelete id=\"deleteUserById\">\n    delete from user where id = #{id}\n\u003C\u002Fdelete>\n```\n\n这些操作相比查询操作来说非常简单就可以实现，这里就不多做介绍了。\n\n### 事务操作\n\n我们可以在获取`SqlSession`关闭自动提交来开启事务模式，和JDBC其实都差不多，在创建SqlSession的时候不填写参数默认使用的就是事务模式：\n\n```java\ntry (SqlSession session = sqlSessionFactory.openSession(false)) { ... }\n```\n\n我们发现，在关闭自动提交后，我们的内容是没有进入到数据库的：\n\n```java\ntry(SqlSession session = MybatisUtils.openSession(false)) {\n    TestMapper mapper = session.getMapper(TestMapper.class);\n    mapper.deleteUserById(1);   \u002F\u002F虽然日志中已经提示生效1行，但是并没有提交\n}\n```\n\nSqlSession接口中为我们提供了事务操作相关的方法，这里我们可以直接尝试进行事务的提交：\n\n```java\nTestMapper mapper = session.getMapper(TestMapper.class);\nmapper.deleteUserById(7);\nsession.commit();   \u002F\u002F通过SqlSession进行事务提交\n```\n\n注意，如果我们在提交事务之前，没有进行任何的DML操作，也就是删除、更新、插入的其中任意一种操作，那么调用`commit`方法则不会进行提交，当然如果仍然需要提交的话也可以使用`commit(true)`来强制提交。\n\n我们接着来测试一下回滚操作：\n\n```java\nTestMapper mapper = session.getMapper(TestMapper.class);\nmapper.deleteUserById(1);\nSystem.out.println(mapper.selectUserById(1));   \u002F\u002F此时由于数据被删除，无法查到\nsession.rollback();   \u002F\u002F进行回滚操作\nSystem.out.println(mapper.selectUserById(1));   \u002F\u002F之前被删除的数据回来了\n```\n\n事务相关操作非常简单，这里就暂时先介绍这么多。\n\n### 动态SQL\n\n在之前JDBC讲解的时候，我们就提到过批量执行语句的问题，当我们要执行很多条语句时，大家可能会一个一个地提交：\n\n```java\n\u002F\u002F现在要求把下面所有用户都插入到数据库中\nList\u003CString> users = List.of(\"小刚\", \"小强\", \"小王\", \"小美\", \"小黑子\");\n\u002F\u002F使用for循环来一个一个执行insert语句\nfor (String user : users) {\n    statement.executeUpdate(\"insert into user (name, age) values ('\" + user + \"', 18)\");\n}\n```\n\n虽然这样看似非常完美，也符合逻辑，但是实际上我们每次执行SQL语句，都像是去厨房端菜到客人桌上一样，我们每次上菜的时候只从厨房端一个菜，效率非常低，但是如果我们每次上菜推一个小推车装满N个菜一起上，效率就会提升很多，而数据库也是这样，我们每一次执行SQL语句，都需要一定的时间开销，但是如果我把这些任务合在一起告诉数据库，效率会截然不同：\n\n![QQ_1722352388506](https:\u002F\u002Fs2.loli.net\u002F2024\u002F07\u002F30\u002F7lxfue9jU8nMYLQ.png)\n\n可见，使用循环操作执行数据库相关操作实际上非常耗费资源，不仅带来网络上的额外开销，还有数据库的额外开销，我们更推荐大家使用批处理来优化这种情况，一次性提交一个批量操作给数据库。\n\n需要在Mybatis中开启批处理，我们只需要在创建SqlSession时进行一些配置即可：\n\n```java\nfactory.openSession(ExecutorType.BATCH, autoCommit);\n```\n\n在使用`openSession`时直接配置`ExecutorType`为BATCH即可，这样SqlSession会开启批处理模式，在多次处理相同SQL时会尽可能转换为一次执行，开启批处理后，无论是否处于事务模式下，我们都需要`flushStatements()`来一次性提交之前是所有批处理操作：\n\n```java\nTestMapper mapper = session.getMapper(TestMapper.class);\nfor (int i = 1; i \u003C= 5; i++) {\n    mapper.deleteUserById(i);\n}\nsession.flushStatements();\n```\n\n此时日志中可以看到Mybatis在尽可能优化我们的SQL操作：\n\n![QQ_1724050907084](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F19\u002Fb2ZexXIYiHSsM7v.png)\n\n除了使用批处理之外，Mybatis还为我们提供了一种更好的方式来处理这种问题，我们可以使用动态SQL来一次性生成一个批量操作的SQL语句，这里先介绍一下什么是动态SQL。\n\n> 动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架，你应该能理解根据不同条件拼接 SQL 语句有多痛苦，例如拼接时要确保不能忘记添加必要的空格，还要注意去掉列表最后一个列名的逗号。利用动态 SQL，可以彻底摆脱这种痛苦。\n\n简单来说，动态SQL在执行时可以进行各种条件判断以及循环拼接等操作，极大地提升了SQL语句编写的的灵活性。\n\n我们先来看看条件判断，在编写SQL时，我们可以添加一些用于条件判断的标签到SQL语句中，比如我们希望在根据ID查询用户时，如果查询的ID大于3，那么必须同时要满足大于18岁这个条件，这看似是一个很奇怪的查询条件，但是在我们进入到公司后确实可能会遇到这些奇葩需求，此时动态SQL就能很轻松实现这个操作：\n\n```xml\n\u003Cselect id=\"selectUserById\" resultType=\"User\">\n    select * from user where id = #{id}\n    \u003Cif test=\"id > 3\">\n        and age > 18\n    \u003C\u002Fif>\n\u003C\u002Fselect>\n```\n\n这里我们使用`if`标签表示里面的内容会在判断条件满足时拼接到后面，如果不满足，那么就不拼接里面的内容到原本的SQL中，其中test属性就是我们需要填写的判断条件，它采用OGNL表达式进行编写，语法与Java比较相似，如果各位小伙伴比较感兴趣也可以前往：https:\u002F\u002Fcommons.apache.org\u002Fdormant\u002Fcommons-ognl\u002F 详细了解。\n\n可以看到，当我们查询条件不同时，Mybatis会选择性拼接我们的SQL语句：\n\n![QQ_1724052613799](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F19\u002FfSOXZdTjFR4oMgP.png)\n\n除了if操作之外，Mybatis还针对多分支情况提供了choose操作，它类似于Java中的switch语句，比如现在我们希望在查询用户时，ID等于1的必须同时要满足小于18岁，ID等于2的必须满足等于18岁，其他情况的必须满足大于18岁，我们可以像这样进行编写：\n\n```xml\n\u003Cselect id=\"selectUserById\" resultType=\"User\">\n    select * from user where id = #{id}\n    \u003Cchoose>\n        \u003Cwhen test=\"id == 1\">\n             and age &lt;= 18\n        \u003C\u002Fwhen>\n        \u003Cwhen test=\"id == 2\">\n            and age = 18\n        \u003C\u002Fwhen>\n        \u003Cotherwise>\n            and age > 18\n        \u003C\u002Fotherwise>\n    \u003C\u002Fchoose>\n\u003C\u002Fselect>\n```\n\n注意在`when`中不允许使用`\u003C`或是`>`这种模糊匹配的条件。\n\n最后我们再来介绍一下`foreach`操作，它与Java中的for类似，可以实现批量操作，这非常适合处理我们前面说的批量执行SQL的问题：\n\n```java\nfor (int i = 1; i \u003C= 5; i++) {\n    mapper.deleteUserById(i);\n}\n```\n\n但是实际上这种情况完全可以简写为一个SQL语句：\n\n```sql\nDELETE FROM users WHERE id IN (1, 2, 3, 4, 5);\n```\n\n所以，现在我们使用foreach来完成它就很简单了：\n\n```xml\n\u003Cdelete id=\"deleteUsers\">\n    delete from user where id in\n    \u003Cforeach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\",\" close=\")\">\n        #{item}\n    \u003C\u002Fforeach>\n\u003C\u002Fdelete>\n```\n\n其中collection就是我们需要遍历的集合或是数组等任意可迭代对象，item和index分别代表我们在foreach标签中使用每一个元素和下标的变量名称，最后open和close用于控制起始和结束位置添加的符号，separator用于控制分隔符，现在执行以下操作：\n\n```java\nsession.delete(\"deleteUsers\", List.of(1, 2, 3, 4, 5));\n```\n\n最后实际执行的SQL为：\n\n![QQ_1724059653501](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F19\u002FrFP1oJNM56ZebA4.png)\n\n是不是感觉有点那味了？我们再来看一个例子，比如现在我们想要批量插入一些用户到数据库里面，原本Java应该这样写，但是这是一种极其不推荐的做法：\n\n```java\nTestMapper mapper = session.getMapper(TestMapper.class);\nList\u003CUser> users = List.of(new User(\"小美\", 17),\n        new User(\"小张\", 18),\n        new User(\"小刘\", 19));\nfor (User user : users) {\n    mapper.insertUser(user);\n}\n```\n\n实际上这种操作完全可以浓缩为一个SQL语句：\n\n```sql\nINSERT INTO user (name, age) VALUES ('小美', 17), ('小张', 18), ('小刘', 19);\n```\n\n那这时又可以直接使用咱们的动态SQL来完成操作了：\n\n```xml\n\u003Cinsert id=\"insertAllUser\">\n    insert into user (name, age) values\n    \u003Cforeach collection=\"list\" item=\"user\" separator=\",\">\n        (#{user.name}, #{user.age})\n    \u003C\u002Fforeach>\n\u003C\u002Finsert>\n```\n\n![QQ_1724060416443](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F19\u002FDRAt7hbdZizqpSk.png)\n\n通过使用动态SQL语句，我们基本上可以解决大部分的SQL查询和批量处理场景了。\n\n### 缓存机制\n\n其实缓存机制我们在之前学习IO流的时候已经提及过了，我们可以提前将一部分内容放入缓存，下次需要获取数据时，就可以直接从缓存中读取，这样的话相当于直接从内存中获取而不是再去向数据库索要数据，效率会更高，缓存的概念在我们后续的学习中还会经常遇见，它也是现在提高数据获取效率的良好解决方案。\n\nMybatis为了查询效率，同样内置了一个缓存机制，我们在查询时，如果Mybatis缓存中存在数据，那么我们就可以直接从缓存中获取，而不是再去向数据库进行请求，节省性能开销。\n\n![image-20230306163638882](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F20\u002FVn4fQlW1roPMaDH.png)\n\nMybatis的缓存机制有些复杂，存在一级缓存和二级缓存，我们首先来看一下一级缓存，默认情况下，只启用了本地的会话缓存，也就是一级缓存，它仅仅对一个会话中的数据进行缓存（一级缓存强制启用，无法关闭，只能做调整）也就是每一个SqlSession都有有一个对应的缓存，我们来看看下面这段代码：\n\n```java\nTestMapper mapper = session.getMapper(TestMapper.class);\nSystem.out.println(mapper.selectUserById(1));\nSystem.out.println(mapper.selectUserById(1));  \u002F\u002F再次获取\n```\n\n这里我们连续获取了两次ID为1的用户，我们会在日志中惊奇地发现，这里的查询操作实际上只进行了一次：\n\n![QQ_1724167595196](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F20\u002FOlFMk4gEYTZf2NC.png)\n\n我们去掉类上的`@Data`注解，会发现得到的两个对象实际上就是同一个：\n\n![QQ_1724167645759](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F20\u002F4iTIlZgFa3JS5RM.png)\n\n也就是说我们第二次查询不仅压根就没执行SQL语句，甚至直接没有重新构造对象，而是直接获取之前创建好的。可见，Mybatis确实存在着缓存机制来进行性能优化。\n\n那么如果我修改了数据库中的内容，缓存还会生效吗：\n\n```java\nSystem.out.println(mapper.selectUserById(1));\nmapper.updateAgeById(1, 19);\nSystem.out.println(mapper.selectUserById(1));\n```\n\n此时由于我们更新了数据库中的数据，那么之前缓存的内容也会跟着失效，第二次获取的时候会进行重新查询。也就是说Mybatis知道我们对数据库里面的数据进行了修改，所以之前缓存的内容可能就不是当前数据库里面最新的内容了。但是一定注意，一级缓存只针对于单个会话，多个会话之间不相通。\n\n因此， 一个会话DML操作只会重置当前会话的缓存，不会重置其他会话的缓存，我们可以来试验一下：\n\n```java\ntry(SqlSession s1 = MybatisUtils.openSession(true);\n    SqlSession s2 = MybatisUtils.openSession(true)) {\n    TestMapper m1 = s1.getMapper(TestMapper.class);\n    TestMapper m2 = s2.getMapper(TestMapper.class);\n    System.out.println(m1.selectUserById(1));\n    m2.updateAgeById(1, 19);\n    System.out.println(m1.selectUserById(1));\n}\n```\n\n可以看到，会话1在重复查询数据时，即使会话2已经修改了数据，但是依然没有影响会话1之中的缓存。\n\n一级缓存给我们提供了很高速的访问效率，但是它的作用范围实在是有限，如果一个会话结束，那么之前的缓存就全部失效了，但是我们希望缓存能够扩展到所有会话都能使用，无论哪个会话对于数据的查询缓存都可以直接被所有会话使用。\n\n我们可以通过二级缓存来实现，二级缓存默认是关闭状态，要开启二级缓存，我们需要在映射器XML文件中添加：\n\n```xml\n\u003Ccache\u002F>\n```\n\n二级缓存是Mapper级别的，只要是使用这个Mapper的会话，都会关联到这个二级缓存，无论哪个会话失效，它之前查询的缓存依然会存在于二级缓存中，依然可以被其他会话直接使用。\n\n我们可以对cache标签进行一些配置：\n\n```xml\n\u003Ccache\n  eviction=\"FIFO\"\n  flushInterval=\"60000\"\n  size=\"512\"\n  readOnly=\"true\"\u002F>\n```\n\n其中，`size`表示最大的缓存对象数量，当缓存达到上限时，会根据`eviction`配置的策略进行清理：\n\n* `LRU` – 最近最少使用：移除最长时间不被使用的对象。\n* `FIFO` – 先进先出：按对象进入缓存的顺序来移除它们。\n* `SOFT` – 软引用：基于垃圾回收器状态和软引用规则移除对象。\n* `WEAK` – 弱引用：更积极地基于垃圾收集器状态和弱引用规则移除对象。\n\n`flushInterval`用于控制缓存刷新时间，当到达指定时间时会自动清理所有缓存，默认情况下如果不配置此项则不会进行定时清理。`readOnly`（只读）属性可以被设置为 true 或 false，只读的缓存会给所有调用者返回相同的缓存对象，且对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会（通过序列化）返回缓存对象的拷贝。 速度上会慢一些，但是更安全，因此默认值是 false。\n\n**注意：** 二级缓存是事务性的，这意味着，当 SqlSession 结束并提交时，或是结束并回滚，而且没有执行 flushCache=true 的 insert\u002Fdelete\u002Fupdate 语句时，缓存才会被更新。\n\n开启二级缓存后，再次执行我们之前的操作，就可以直接在二级缓存中命中了：\n\n![QQ_1724169383598](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F20\u002FiIZPElyrYN2RdKf.png)\n\n实际上，添加了二级缓存之后，Mybatis会先从二级缓存中查找数据，当二级缓存中没有时，才会从一级缓存中获取，当一级缓存中都还没有数据时，才会请求数据库。\n\n当我们开启二级缓存后，默认情况下一个Mapper中所有的操作都会使用二级缓存，我们也可以单独配置其不使用二级缓存，只需要修改useCache属性即可：\n\n```xml\n\u003Cselect id=\"selectUserById\" useCache=\"false\" resultType=\"com.test.User\">\n    select * from user where id = #{id}\n\u003C\u002Fselect>\n```\n\n有些操作可能比较特殊，比如我们希望某个操作执行完成后，直接清除所有缓存，无论是一级缓存还是二级缓存，那么此时就可以开启`flushCache`属性：\n\n```xml\n\u003Cselect id=\"selectUserById\" flushCache=\"true\" resultType=\"com.test.User\">\n    select * from user where id = #{id}\n\u003C\u002Fselect>\n```\n\n开启此选项后，调用此操作将直接导致一级和二级缓存被清除。\n\n虽然缓存机制给我们提供了很大的性能提升，但是缓存存在一个问题，我们之前在`计算机组成原理`中可能学习过缓存一致性问题，也就是说当多个CPU在操作自己的缓存时，可能会出现各自的缓存内容不同步的问题。\n\n![image-20230306163717033](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F20\u002Ff4hm7o8jcrXbul6.png)\n\n而Mybatis也会这样，我们来看看这个例子：\n\n```java\nTestMapper mapper = session.getMapper(TestMapper.class);\nwhile (true){\n    Thread.sleep(3000);\n    System.out.println(mapper.selectUserById(1));\n}\n```\n\n我们现在循环地每三秒读取一次，而在这个过程中，我们使用其他软件手动修改数据库中的数据，将1号用户的ID改成100，那么理想情况下，下一次读取将直接无法获取到这行数据，因为ID已经发生变化了。\n\n但是结果却是依然能够读取，并且sid并没有发生改变，这也证明了Mybatis的缓存在生效，因为我们是从外部进行修改，Mybatis不知道我们修改了数据，所以依然在使用缓存中的数据，但是这样很明显是不正确的，因此，如果存在多台服务器或者是多个程序都在使用Mybatis操作同一个数据库，并且都开启了缓存，需要解决这个问题，我们只能关闭所有二级缓存，并且在Mybatis每个操作都配置`flushCache`为true来保证刷新。\n\n只不过这种操作实际上是治标不治本的，实现多服务器缓存共用才是最终解决方案，也就是让所有的Mybatis都使用同一个缓存进行数据存取，在后面，我们会继续学习Redis、Ehcache、Memcache等缓存框架，通过使用这些工具，就能够很好地解决缓存一致性问题。\n\n### 使用注解开发\n\n在之前的学习中，我们已经体验到Mybatis为我们带来的便捷了，我们只需要编写对应的映射器，并将其绑定到一个接口上，即可直接通过该接口执行我们的SQL语句，极大的简化了我们之前JDBC那样的代码编写模式。那么，能否实现无需XML映射器配置，而是直接使用注解在接口上进行配置呢？\n\n从这节课开始，我们可以直接删除掉所有的`Mapper.xml`文件了，只保留Mapper相关的接口。\n\n现在，我们来尝试以全注解的形式重现编写咱们的SQL语句映射，还是以查询所有的用户为例，既然现在不需要配置`Mapper.xml`文件了，那么我们现在需要重写在Mybatis的配置文件中进行mapper的配置，因为现在只需要使用接口来进行配置，所以使用package标签来指定一个包，包下所有的接口都将直接作为Mapper配置接口：\n\n```xml\n\u003Cmappers>\n    \u003Cpackage name=\"com.test.mapper\"\u002F>\n\u003C\u002Fmappers>\n```\n\n现在我们还是在Mapper中添加一个对应的方法用于执行：\n\n```java\npublic interface TestMapper {\n    List\u003CUser> selectAllUser();\n}\n```\n\n之前我们需要像这样编写：\n\n```xml\n\u003Cselect id=\"selectAllUser\" resultType=\"com.test.User\">\n    select * from user\n\u003C\u002Fselect>\n```\n\n现在只需要一个注解即可，Mybatis为我们提供了丰富的注解用于表示不同SQL语句类型，这里的`@Select`代表的就是select标签，我们只需要直接在其中编写SQL语句即可，而返回类型Mybatis会自动根据方法的返回值进行判断：\n\n```java\npublic interface TestMapper {\n    @Select(\"select * from user\")\n    List\u003CUser> selectAllUser();\n}\n```\n\n现在我们来试试看吧：\n\n![QQ_1724143033753](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F20\u002FALdMlG5evncp4kH.png)\n\n包括一些SQL语句参数的使用也是和之前完全一样，比如插入一个用户：\n\n```java\n@Insert(\"insert into user (name, age) values (#{name}, #{age})\")\nint insertUser(User user);\n```\n\n虽然Mybatis为我们提供了之前XML配置中各种操作的对应注解，但是我们发现，这些注解并不能像之前XML那样直接修改一些属性，比如这里我们希望配置`useGeneratedKeys`来得到自动生成的主键，需要配置这些额外的参数，我们可以使用`@Options`注解：\n\n```java\n@Options(useGeneratedKeys = true, keyColumn = \"id\", keyProperty = \"id\")\n@Insert(\"insert into user (name, age) values (#{name}, #{age})\")\nint insertUser(User user);\n```\n\n```xml\n\u003Cinsert id=\"insertUser\" parameterType=\"com.test.entity.User\" useGeneratedKeys=\"true\" keyProperty=\"id\" keyColumn=\"id\">\n    insert into user (name, age) values (#{name}, #{age})\n\u003C\u002Finsert>\n```\n\n这两种写法效果是完全一样的，所以我们通过Mybatis为我们设计的这一系列注解就可以很轻松地取代掉之前的配置。\n\n假如现在我们的实体类字段名称与数据库不同，此时该如何像之前一样配置`resultMap`呢？\n\n```java\npublic class User {\n    int uid;\n    String username;\n    int age;\n}\n```\n\n```xml\n\u003CresultMap id=\"test\" type=\"User\">\n    \u003Cid property=\"id\" column=\"uid\"\u002F>\n    \u003Cresult column=\"name\" property=\"username\"\u002F>\n\u003C\u002FresultMap>\n```\n\n我们可以使用`@Results`注解来实现这种操作，它的使用方式与resultMap几乎没什么区别：\n\n```java\n@Results({\n        @Result(id = true, column = \"id\", property = \"uid\"), \n        @Result(column = \"name\", property = \"username\")\n})\n@Select(\"select * from user\")\nList\u003CUser> selectAllUser();\n```\n\n是不是感觉很简单？当然，如果你还是觉得这种方式配置起来不如之前方便，那么你也可以单独在XML中配置一个resultMap然后直接通过注解的形式引用：\n\n```java\n@ResultMap(\"test\")\n@Select(\"select * from user\")\nList\u003CUser> selectAllUser();\n```\n\n那么现在如果我们需要指定使用的构造方法怎么办呢？就像我们之前在使用constrator标签一样，Mybatis为我们提供了`@ConstructorArgs`注解，配置方式和之前几乎一致：\n\n```java\npublic class User {\n    int id;\n    String name;\n    int age;\n\n    public User(int id, String name, int age) {\n        this.id = id;\n        this.name = name;\n        this.age = age + 20;\n    }\n}\n```\n\n```java\n@ConstructorArgs({\n        @Arg(id = true, column = \"id\", javaType = int.class),\n        @Arg(column = \"name\", javaType = String.class),\n        @Arg(column = \"age\", javaType = int.class)\n})\n@Select(\"select * from user\")\nList\u003CUser> selectAllUser();\n```\n\n这与我们之前的XML配置完全一致：\n\n```xml\n\u003Cconstructor>\n    \u003CidArg column=\"id\" javaType=\"_int\"\u002F>\n    \u003Carg column=\"name\" javaType=\"String\"\u002F>\n    \u003Carg column=\"age\" javaType=\"_int\"\u002F>\n\u003C\u002Fconstructor>\n```\n\n我们再来看看之前在resultMap中配置的关联查询该如何编写，Mybatis也为我们提供了丰富的注解用于处理这类问题，我们首先来看看一对一查询：\n\n```java\n@Results({\n        @Result(id = true, column = \"id\", property = \"id\"),\n        @Result(column = \"id\", property = \"detail\", one = @One(select = \"selectDetailById\"))\n})\n@Select(\"select * from user where id = #{id};\")\nUser selectUserById(int id);\n\n@Select(\"select * from user_detail where id = #{id}\")\nUserDetail selectDetailById(int id);\n```\n\n我们在配置`@Result`注解时，只需要将one或是many参数进行填写即可，它们分别代表一对一关联和一对多关联，使用`@One`和`@Many`注解来指定其他查询语句进行嵌套查询，就像是我们之前使用`association`和`collection`那样。\n\n不过很遗憾的是，我们无法完全通过注解来实现之前的联合查询解析（这是因为 Java 注解不允许循环引用）只能使用这种嵌套查询来完成复杂查询操作，因此，如果对这种复杂查询有着一定需求的话，建议使用之前的XML方式进行配置。\n\n我们还可以使用注解进行动态SQL的配置，比如现在我们想要实现之前的这个奇葩需求：\n\n```xml\n\u003Cselect id=\"selectUserById\" resultType=\"User\">\n    select * from user where id = #{id}\n    \u003Cif test=\"id > 3\">\n        and age > 18\n    \u003C\u002Fif>\n\u003C\u002Fselect>\n```\n\nMybatis针对于所有的SQL操作都提供了对应的Provider注解，用于配置动态SQL，我们需要先创建一个类编写我们的动态SQL操作：\n\n```java\npublic class TestSqlBuilder {\n    public static String buildGetUserById(int id) {\n        return new SQL(){{   \u002F\u002FSQL类中提供了常见的SELECT、FORM、WHERE等操作\n            SELECT(\"*\");\n            FROM(\"user\");\n            WHERE(\"id = #{id}\");\n            if (id > 3) {\n                WHERE(\"age > 18\");\n            }\n        }}.toString();\n    }\n}\n```\n\n详细的SQL语句构建器语法文档：https:\u002F\u002Fmybatis.org\u002Fmybatis-3\u002Fzh_CN\u002Fstatement-builders.html\n\n构建完成后，接着我们就可以使用`@SelectProvider`来引用这边编写好的动态SQL操作：\n\n```java\n@SelectProvider(type = TestSqlBuilder.class, method = \"buildGetUserById\")\nUser selectUserById(int id);\n```\n\n效果和之前我们编写XML形式的动态SQL一致，当然，如果遇到了多个参数的情况，我们同样需要使用`@Param`来指定参数名称，包括TestSqlBuilder中编写的方法也需要添加，否则必须保证形参列表与这边接口一致。\n\n虽然这样可以实现和之前差不多的效果，但是这实在是太过复杂了，我们还需要单独编写一个类来做这种事情，实际上我们也可以直接在`@Select`中编写一个XML配置动态SQL，Mybatis同样可以正常解析：\n\n```java\n@Select(\"\"\"\n         \u003Cscript>\n            select * from user where id = #{id}\n            \u003Cif test=\"id > 3\">\n                 and age > 18\n            \u003C\u002Fif>\n         \u003C\u002Fscript>\n         \"\"\")\nUser selectUserById(int id);\n```\n\n这里只需要包括一个script标签我们就能像之前XML那样编写动态SQL了，只不过由于IDEA不支持这种语法的识别，可能会出现一些莫名其妙的红标，但是是可以正常运行的。\n\n最后我们来看一下二级缓存相关的配置，使用`@CacheNamespace`注解直接定义在接口上即可，然后我们可以通过使用`@Options`来控制单个操作的缓存启用：\n\n```java\n@CacheNamespace(size = 512, readWrite = false)\npublic interface TestMapper {\n```\n\n我们如果需要控制单个方法的缓存，同样可以使用`@Option`来进行配置：\n\n```java\n@Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = false)\n@Select(\"select * from user where id = #{id}\")\nUser selectUserById(int id);\n```\n\n这里我们不如再做的更加极致一点，咱们把配置文件也给变成代码配置，彻底抛弃XML配置，实际上我们的XML配置中所有配置项都可以以Configuration对象的形式进行配置，最后在构造SqlSessionFactory时也可以通过此对象进行创建：\n\n```java\nprivate static Configuration initConfiguration() {\n    Configuration configuration = new Configuration();\n    PooledDataSource dataSource = new PooledDataSource(\n            \"com.mysql.cj.jdbc.Driver\",\n            \"jdbc:mysql:\u002F\u002Flocalhost:3306\u002Fweb_study\",\n            \"test\",\n            \"123456\");\n    Environment environment = new Environment(\"development\", new JdbcTransactionFactory(), dataSource);\n    configuration.setEnvironment(environment);\n    configuration.getTypeAliasRegistry().registerAliases(\"com.test.entity\");\n    configuration.setLogImpl(StdOutImpl.class);\n    configuration.addMappers(\"com.test.mapper\");\n    return configuration;\n}\n```\n\n有关Mybatis的基本使用，我们就暂时介绍到这里。\n\n## 高级用法与原理探究\n\n我们这一部分来研究Mybatis的高级用法和原理探究。\n\n### 类型处理器（选学）\n\n在我们前面的学习中，相信各位小伙伴应该有留意到一个叫做`typeHandler`的属性，在正常情况下，我们使用Mybatis得到结果都能够自动完成属性类型的的推断，比如：\n\n```java\npublic class User {\n    int id;\n    String name;\n    int age;\n}\n```\n\n这里类中的所有字段类型实际上在查询时都可以被正确进行解析，这是因为Mybatis默认情况下对这些JDK提供的类型做了支持，因此他们可以直接正确进行解析，详细列表：https:\u002F\u002Fmybatis.org\u002Fmybatis-3\u002Fzh_CN\u002Fconfiguration.html#typeHandlers\n\n但是有些时候可能我们的对象字段并不是JDK提供的类型，尤其是后面各位小伙伴在后续Spring阶段的学习中接触到JSON这种数据时，数据的转换就会变得很重要，这里我们通过一个简单例子来进行讲解：\n\n```java\n@Data\npublic class MyString {   \u002F\u002F现在我们自己写了一个对String包装类型\n    private final String text;\n}\n```\n\n接着我们将实体类的对应的字段进行修改：\n\n```java\npublic class User {\n    int id;\n    MyString name;   \u002F\u002F使用我们自己定义的String包装类\n    int age;\n}\n```\n\n此时，我们希望的是Mybatis可以正确将数据库中的name字段读取出来，并自动转化为一个MyString对象，里面的text值为查询name得到的结果，但这毕竟是理想情况，实际上Mybatis压根不知道这种类型该怎么进行数据的映射，就直接报错了：\n\n```\n### Error building SqlSession.\n### The error may exist in com\u002Ftest\u002Fmapper\u002FTestMapper.java (best guess)\n### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalStateException: Type handler was null on parameter mapping for property 'name'. It was either not specified and\u002For could not be found for the javaType (com.test.handler.MyString) : jdbcType (null) combination.\n\tat org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)\n```\n\n这里Mybatis提示我们找不到这个类型的Type handler，也就是Mybatis并不知道这种类型的数据该怎么进行映射，映射规则是什么。此时我们就可以手动提供一个TypeHandler实现，要编写自定义的类型处理器，我们需要：\n\n```java\npublic class MyTypeHandler extends BaseTypeHandler\u003CMyString> {\n    @Override\n    public void setNonNullParameter(PreparedStatement ps, int i, MyString parameter, JdbcType jdbcType) throws SQLException {\n        \n    }\n\n    @Override\n    public MyString getNullableResult(ResultSet rs, String columnName) throws SQLException {\n        return null;\n    }\n\n    @Override\n    public MyString getNullableResult(ResultSet rs, int columnIndex) throws SQLException {\n        return null;\n    }\n\n    @Override\n    public MyString getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {\n        return null;\n    }\n}\n```\n\n可以看到这里我们需要实现很多种不同的方法，我们一个一个来介绍。\n\n首先我们需要在类的最上方添加一个`@MappedJdbcTypes`注解，用于指定当前类型处理器是用于处理对应的数据库什么类型的数据的：\n\n```java\n@MappedJdbcTypes(JdbcType.VARCHAR)  \u002F\u002F这里我们映射的是数据库里面的varchar\npublic class MyTypeHandler extends BaseTypeHandler\u003CMyString> {\n  ...\n}\n```\n\n接着是四个方法的实现，它们涉及到两种操作，一个是如何将我们的Java类型转换为对应的JDBC类型，还有一个就是如何将JDBC类型转换为对应的Java类型。\n\n首先是如何将Java转JDBC类型，这里的PreparedStatement我们在JDBC阶段已经讲过，不再赘述：\n\n```java\n@Override\npublic void setNonNullParameter(PreparedStatement ps, int i, MyString parameter, JdbcType jdbcType) throws SQLException {\n  \t\u002F\u002F这里的i就是PreparedStatement的第几个参数，直接使用setString设置即可\n    ps.setString(i, parameter.getText());  \u002F\u002F按照我们自己的方式把需要插入的值设置进去\n}\n```\n\n剩下三个都是JDBC类型和Java类型的转换，只是参数不同而已，我们就简单编写一下：\n\n```java\n\u002F\u002F用于配置如何从ResultSet中读取并转换为我们需要的类型\n@Override\npublic MyString getNullableResult(ResultSet rs, String columnName) throws SQLException {\n    String value = rs.getString(columnName);  \u002F\u002F这里提供的是columnName进行查询\n    return new MyString(value);\n}\n\n@Override\npublic MyString getNullableResult(ResultSet rs, int columnIndex) throws SQLException {\n    String value = rs.getString(columnIndex);  \u002F\u002F同上，这里提供的是columnIndex进行查询\n    return new MyString(value);\n}\n\n@Override\npublic MyString getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {\n    String value = cs.getString(columnIndex);  \u002F\u002F一般用于存储过程之类的操作，同上\n    return new MyString(value);\n}\n```\n\n这样，我们自定义的类型处理器就编写好了，首先我们需要在Mybatis配置文件中注册这个类型处理器：\n\n```xml\n\u003CtypeHandlers>\n    \u003CtypeHandler handler=\"com.test.handler.MyTypeHandler\"\u002F>\n\u003C\u002FtypeHandlers>\n```\n\n接着我们需要在Mapper中配置这里编写好的类型处理器：\n\n```java\n@Results({\n        @Result(column = \"name\", property = \"name\", typeHandler = MyTypeHandler.class)\n})\n@Select(\"select * from user where id = #{id}\")\nUser selectUserById(int id);\n```\n\n```xml\n\u003Cselect id=\"selectUserByIdAndAge\" resultMap=\"user\">\n    select * from user where id = #{id} and age = #{age}\n\u003C\u002Fselect>\n\u003CresultMap id=\"user\" type=\"com.test.User\">\n    \u003Cresult column=\"name\" property=\"username\" typeHandler=\"com.test.handler.MyTypeHandler\"\u002F>\n\u003C\u002FresultMap>\n```\n\n这样，我们的数据就可以正确进行解析了：\n\n![QQ_1724172380846](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F21\u002FmhQ7GHsqXwSpD3x.png)\n\n并不是所有情况下类型处理器都需要我们自己来编写，一些框架可能会内置对应的Mybatis类型处理器，比如FastJson、Jackson之类的，我们后续再进行探讨。\n\n### 动态代理探究（选学）\n\n在探究动态代理机制之前，我们要先聊聊什么是代理，代理实际上在我们的设计模式中有介绍。\n\n![QQ_1724170166302](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F21\u002FVcSB9vxy2PQIjUA.png)\n\n顾名思义，假如我们种了一大片的西瓜，正值夏天西瓜全部成熟了，现在我们需要将这批西瓜出售掉，但是靠我们自己去出售非常困难，此时我们就需要寻找一些代理商来帮助我们进行出售，它们有自己的销售方式有自己的顾客群体，这样就会简单许多，这便是一种代理的思想。当然，对于顾客来说，无论是找我们的代理商还是我们自己都可以买到西瓜，我们都可以提供一样的服务，只不过代理商肯定会自己抽成，除了进行基本的卖瓜操作外，还有自己的一些操作。\n\n那么现在我们来尝试实现一下这样的类结构，首先定义一个接口用于规范行为：\n\n```java\npublic interface Shopper {\n    \u002F\u002F卖瓜行为\n    void saleWatermelon();\n}\n```\n\n然后需要实现一下卖瓜行为，也就是我们最基本的西瓜销售行为：\n\n```java\npublic class ShopperImpl implements Shopper{\n    \u002F\u002F卖瓜行为的实现\n    @Override\n    public void saleWatermelon() {\n        System.out.println(\"销售一个西瓜给客户，获得摩拉10￥\");\n    }\n}\n```\n\n最后老板代理后肯定要用自己的方式去出售这些西瓜，成交之后再按照我们告诉老板的价格进行出售：\n\n```java\npublic class ShopperProxy implements Shopper{\n    \u002F\u002F被代理商代理的供应商\n    private final Shopper impl;\n\n    public ShopperProxy(Shopper impl) {\n        this.impl = impl;\n    }\n\n    \u002F\u002F代理卖瓜行为\n    @Override\n    public void saleWatermelon() {\n        impl.saleWatermelon();\n        System.out.println(\"代理抽成摩拉8￥\");\n    }\n}\n```\n\n现在我们就来模拟一下顾客从代理商那买瓜：\n\n```java\npublic class Main {\n    public static void main(String[] args) {\n        Shopper shopper = new ShopperProxy(new ShopperImpl());\n        shopper.saleWatermelon();   \u002F\u002F此时不仅有我们原本的卖瓜行为，代理商还进行了额外的抽成\n    }\n}\n```\n\n这样的操作我们称为静态代理，也就是说我们提前知道接口的定义，然后在子类直接进行实现完成的代理，而Mybatis这样的框架在处理我们的这些Mapper接口时，是无法预知内部的具体定义的，这个时候我们就需要用到动态代理。\n\nJDK提供的反射框架就为我们很好地解决了动态代理的问题，它支持在程序运行时动态对某个类或是接口进行代理，提供了一个叫做`InvocationHandler`的接口用于实现，这里我们就对它进行一下介绍，相当于对JavaSE阶段反射的内容的补充：\n\n```java\nclass ShopperProxy implements InvocationHandler {\n    \u002F\u002F被代理商代理的对象\n    Object target;\n    public ShopperProxy(Object target){\n        this.target = target;\n    }\n\n    \u002F\u002F动态生成对应的代理对象时，当代理对象的方法被调用，就会执行这里的invoke方法\n    @Override\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n        Object invoke = method.invoke(target, args);  \u002F\u002F使用method.invoke()调用被代理对象的方法\n        System.out.println(\"销售一个西瓜给客户，代理抽成摩拉8￥\");   \u002F\u002F添加我们代理商的自定义操作\n        return invoke;  \u002F\u002F最后返回原方法得到的返回值\n    }\n}\n```\n\n通过实现InvocationHandler来成为一个动态代理，我们发现它提供了一个invoke方法，用于调用被代理对象的方法并完成我们的代理工作，其中提供了一些参数，第一个proxy就是当前的代理实例对象，method就是代理对象被调用的方法名称，args就是方法传入的实际参数。\n\n现在就可以通过` Proxy.newProxyInstance`方法来生成一个动态代理类：\n\n```java\nShopperImpl target = new ShopperImpl();\nShopper shopper = (Shopper) Proxy.newProxyInstance(\n        target.getClass().getClassLoader(),   \u002F\u002F直接使用被代理对象的类加载器\n        target.getClass().getInterfaces(),    \u002F\u002F这里还需要提供被代理类的接口列表，生成的代理类也会实现这些接口\n        new ShopperProxy(target)   \u002F\u002F最后传入我们写好的代理对象\n);\nshopper.saleWatermelon();\nSystem.out.println(shopper.getClass());\n```\n\n通过打印类型我们发现，这里通过JDK动态代理生成的类，就是我们之前看到的那种奇怪的类：`class com.sun.proxy.$Proxy0`，因此Mybatis其实也是这样的来实现的。\n\n只不过，Mybatis是直接对我们提供的接口进行动态代理，使用方式其实是差不多的，我们可以来试试看，这里我们直接来尝试生成一个我们TestMapper接口的代理类，首先创建一个代理类用于扩展代理操作：\n\n```java\npublic class MapperProxy implements InvocationHandler {\n    @Override\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n        System.out.println(\"调用了接口方法: \" + method);  \u002F\u002F一会我们观察一下是不是确实能够检测到\n        return null;\n    }\n}\n```\n\n接着我们就可以直接创建一个对Mapper的代理类：\n\n```java\nTestMapper mapper = (TestMapper) Proxy.newProxyInstance(\n        TestMapper.class.getClassLoader(),\n        new Class[]{ TestMapper.class },\n        new MapperProxy()\n);\nmapper.selectAllUser();\n```\n\n最后我们可以去看看Mybatis的源码，实际上它内部就是通过这种方式进行实现的：\n\n```java\npublic class MapperProxy\u003CT> implements InvocationHandler, Serializable {\n  ...\n  \n\t@Override\n  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n    try {\n      if (Object.class.equals(method.getDeclaringClass())) {\n        return method.invoke(this, args);\n      }\n      \u002F\u002F调用内部实现的代理操作实现SQL的执行和结果处理\n      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);\n    } catch (Throwable t) {\n      throw ExceptionUtil.unwrapThrowable(t);\n    }\n  }\n}\n```\n\n关于动态代理相关的实现，除了Mybatis用到之外，包括我们后面学习的Spring也会在AOP中使用到，有关更多的内容希望各位小伙伴可以在后续的学习中发现。\n\n### 自定义拦截器（选学）\n\nMybatis除了面向咱们使用者提供了丰富的API之外，也向一些插件开发者提供了丰富的API，便于他们快速开发针对于Mybatis的插件，比如Mybatis并不支持SQL级别的数据分页操作，但是我们可以通过开发插件的形式实现。\n\nMybatis为所有的插件提供了一个Interceptor接口，它表示拦截器类，拦截器可以对Mybatis中一些类的任意方法进行动态代理，然后添加我们的自定义操作，支持的类包括：\n\n* Executor及其子类，如CacheExecutor等 - 整个Mybatis的执行器，通过调用StatementHandler来操作数据库\n* StatementHandler - 操作数据库执行SQL\n* ParameterHandler - 用于设置SQL的参数\n* ResultSetHandler - 用于结果集映射为对象\n\n其他的类型虽然也使用Interceptor接口进行动态代理只不过官方没有支持，并不能正常使用。\n\n这里我们来进行一下简单的尝试，首先所有的拦截器必须实现自Interceptor接口：\n\n```java\npublic class MyInterceptor implements Interceptor {\n  \n  @Override\n    public Object intercept(Invocation invocation) throws Throwable {\n      System.out.println(\"测试\");\n      return invocation.proceed();\n    }\n}\n```\n\n接着我们需要在类上添加注解用于指示此拦截器可以对哪些类的哪些方法进行动态代理：\n\n```java\n@Intercepts({})\n```\n\n假如现在我们要对Executor的query方法进行动态代理，这个方法是Mybatis执行SQL的核心，一般用于查询操作，下一节中我们会进行详细介绍：\n\n```java\npublic interface Executor {\n\n  ...\n\n  \u003CE> List\u003CE> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,\n      CacheKey cacheKey, BoundSql boundSql) throws SQLException;\n\n  \u003CE> List\u003CE> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)\n      throws SQLException;\n```\n\n我们只需要添加对应的`@Signature`注解即可，在注解中指明需要代理的类型、方法名称、参数列表即可：\n\n```java\n@Intercepts({\n        @Signature(type = Executor.class, method = \"query\", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),\n        @Signature(type = Executor.class, method = \"query\", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),\n})\n```\n\n最后我们只需要在配置文件中将这个拦截器以插件的形式注册即可：\n\n```xml\n\u003Cplugins>\n    \u003Cplugin interceptor=\"com.test.interceptor.MyInterceptor\"\u002F>\n\u003C\u002Fplugins>\n```\n\n接着就可以看到确实在进行查询的时候执行了我们编写的测试代码：\n\n![QQ_1724393522296](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F23\u002FdKxCEe39FBYw7Lt.png)\n\n利用这种机制，我们就可以很方便地修改这次查询操作，比如我希望随意篡改我们要查询的用户ID，而这个用户ID实际上是这里被代理方法的第二个参数`parameter`，我们直接修改它的值即可：\n\n```java\n@Override\npublic Object intercept(Invocation invocation) throws Throwable {\n    Object[] args = invocation.getArgs();\n    args[1] = 2;  \u002F\u002F把参数列表的第二个实际参数修改为2\n    return invocation.proceed();   \u002F\u002F再执行原本的操作\n}\n```\n\n此时可以看到，虽然我们这里给的是1，但是由于被我们的拦截器进行了修改，导致其变成了2：\n\n![QQ_1724393741215](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F23\u002F4cTb2klJPQLYwxs.png)\n\n当然，这只是拦截器使用的冰山一角，利用动态代理实际上可以做很多事情，比如对SQL进行分页处理、对查询的数据进行脱敏等，这里我们就不去一一实现了，在后面Spring阶段我们会学习更高级的框架，它们一般都自带这些拦截器。\n\n我们再来研究下这玩意怎么实现的吧，首先是从Configuration类开始对所有的拦截器进行注册，内部维护了一个`interceptorChain`对象，用于以链式存放所有的拦截器，因此拦截器的执行也是按照链式有序执行的，然后为支持拦截的类型进行动态代理，这里以`Executor`为例：\n\n```java\npublic Executor newExecutor(Transaction transaction, ExecutorType executorType) {\n  ...\n  return (Executor) interceptorChain.pluginAll(executor);   \u002F\u002F使用集成所有拦截操作到动态代理中\n}\n```\n\n这里`pluginAll`就是将所有拦截器的操作全部添加到动态代理需要执行的操作中：\n\n```java\npublic Object pluginAll(Object target) {\n  for (Interceptor interceptor : interceptors) {\n    target = interceptor.plugin(target);   \u002F\u002F按顺序依次调用拦截器的plugin方法，并且这里是一个一直将代理嵌套下去的操作，形成链式\n  }\n  return target;\n}\n```\n\n```java\ndefault Object plugin(Object target) {\n  return Plugin.wrap(target, this);   \u002F\u002F这里的Plugin就是专门用于拦截器的动态代理实现\n}\n```\n\n最后以Plugin为动态代理实现，生成`target`的动态代理对象：\n\n```java\npublic static Object wrap(Object target, Interceptor interceptor) {\n  Map\u003CClass\u003C?>, Set\u003CMethod>> signatureMap = getSignatureMap(interceptor);\n  Class\u003C?> type = target.getClass();\n  Class\u003C?>[] interfaces = getAllInterfaces(type, signatureMap);\n  if (interfaces.length > 0) {\n    return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));\n  }\n  return target;\n}\n```\n\n最后可以看到在Plugin类中，动态代理的方法会执行拦截器里面的操作：\n\n```java\n@Override\npublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n  try {\n    Set\u003CMethod> methods = signatureMap.get(method.getDeclaringClass());\n    if (methods != null && methods.contains(method)) {\n      \u002F\u002F如果拦截操作匹配当前的方法，则直接调用拦截器进行处理\n      return interceptor.intercept(new Invocation(target, method, args));\n    }\n    return method.invoke(target, args);   \u002F\u002F如果不匹配，调用原方法\n  } catch (Exception e) {\n    throw ExceptionUtil.unwrapThrowable(e);\n  }\n}\n```\n\n支持，有关拦截器相关的使用和原理讲解就到这里。\n\n### 自定义缓存（选学）\n\n前面我们讲解了Mybatis的缓存机制，每一个会话都有一个本地缓存（一级缓存），我们也可以开启针对于整个Mapper的二级缓存，但是我们提到它可能会存在多个程序同时进行修改的问题，这时要解决问题只能由我们自己来处理二级缓存的操作了。这一节我们就来看看如何自定义缓存。\n\n在开始之前，我们需要先了解Mybatis内部的缓存机制是如何实现的，在Mybatis中存在一个Cache接口，它定义了缓存相关的操作：\n\n```java\npublic interface Cache {\n  String getId();\n  \u002F\u002F给一个key和目标对象，将对象缓存\n  void putObject(Object key, Object value);\n  \u002F\u002F从缓存中根据key取出对象\n  Object getObject(Object key);\n  \u002F\u002F根据key移除缓存中的对象\n  Object removeObject(Object key);\n\t\u002F\u002F清空缓存\n  void clear();\n\t\u002F\u002F获取大小\n  int getSize();\n\n  ...\n}\n```\n\n缓存接口的实现类非常多，但是简单的实现类只有一个，叫做`PerpetualCache`，它是纯粹的Cache实现类，内部通过一个Map来实现各种缓存功能，其他所有的Cache实现类全部是基于装饰模式编写的扩展操作，均存放在`decorators`中：\n\n![QQ_1724321629027](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F22\u002FvxU65KXoqtFROgz.png)\n\n在我们不启用二级缓存的情况下，默认一级缓存使用一个`PerpetualCache`来进行缓存操作。我们可以为PerpetualCache打上断点，接着就可以看到在Mybatis内部执行`query`的时候对一级缓存进行了判断：\n\n```java\n\u002F\u002F这里的MappedStatement参数包含了我们Mapper中定义的信息，包括方法、SQL、还有各种配置等，CacheKey是由Mybatis生成的一个用于作为缓存Key的对象，由SQL操作的基本信息组成\npublic \u003CE> List\u003CE> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {\n  ErrorContext.instance().resource(ms.getResource()).activity(\"executing a query\").object(ms.getId());\n  if (closed) {  \u002F\u002F首先判断当前执行器是否被关闭，都关闭了那还说个毛，执行器每个SqlSession都有一个\n    throw new ExecutorException(\"Executor was closed.\");\n  }\n  if (queryStack == 0 && ms.isFlushCacheRequired()) {\n    clearLocalCache();   \u002F\u002F判断如果配置了flushCache则会直接清除一级缓存\n  }\n  List\u003CE> list;   \u002F\u002F查询的结果\n  try {\n    queryStack++;   \u002F\u002FqueryStack是为了防止使用Interceptor的情况下，防止递归操作，因此进行的计数，实际上这个方法里面很多地方都进行了判断，来确保递归状态下只有最外层正常\n    \u002F\u002F这里首先从缓存中进行了一次查询，localCache默认是PerpetualCache\n    list = resultHandler == null ? (List\u003CE>) localCache.getObject(key) : null;\n    if (list != null) {  \u002F\u002F如果缓存中存在，则直接结束\n      \u002F\u002F这里是针对存储过程进行的参数缓存，可以无视掉\n      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);\n    } else {\n      \u002F\u002F缓存没有的话就从数据库里面进行查询了，注意这里就有可能出现我们上面说的Interceptor拦截，因为要从数据库查询\n      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);\n    }\n  } finally {\n    queryStack--;  \u002F\u002F执行完成后queryStack自减\n  }\n  if (queryStack == 0) {\n    for (DeferredLoad deferredLoad : deferredLoads) {\n      deferredLoad.load();\n    }\n    deferredLoads.clear();\n    \u002F\u002F这里很关键，如果localCacheScope配置项的值为STATEMENT，那么这里会直接清除一级缓存\n    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {\n      clearLocalCache();\n    }\n  }\n  return list;\n}\n```\n\n这里的流程非常清晰，首先进行最基本的判断，保证可以正常使用，然后进行`flushCache`选项判断，如果是则直接清除缓存，接着就是从缓存中查询是否存在对应的结果，如果没有就去查数据库，最后直接返回得到的结果。\n\n只不过在最后有一个非常有意思的操作，如果如果localCacheScope配置项的值为STATEMENT，那么会直接清除一级缓存，我们可以直接在配置文件中试试看：\n\n```xml\n\u003Csetting name=\"localCacheScope\" value=\"STATEMENT\"\u002F>\n```\n\n接着执行两次相同的查询：\n\n![QQ_1724346245081](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F23\u002FFl1ERuDUgoKr8dP.png)\n\n可以看到，此时缓存竟然真的失效了，也就是说虽然Mybatis不允许我们关闭一级缓存，但是只要我们将`localCacheScope`属性的值设置为`STATEMENT`，那么一级缓存的作用域将被严格限制在一次Statement内，也就是一次query内，在正常情况下，其最终效果约等于关闭一级缓存，其已经不具备在多个操作间共享的能力了。\n\n但是这里并不能完全认为是一级缓存关闭了，如果出现我们前面说的Interceptor嵌套查询的情况或是执行某个存储过程的情况下，由于还没来得及清理，实际上这里的一级缓存还是有机会使用的，因此，一级缓存依然是无法彻底关闭的。\n\n我们可以进一步研究一下`queryFromDatabase`数据库查询操作：\n\n```java\nprivate \u003CE> List\u003CE> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {\n  List\u003CE> list;\n  localCache.putObject(key, EXECUTION_PLACEHOLDER);   \u002F\u002F添加占位符，用于处理延迟加载相关问题\n  try {\n    \u002F\u002F执行真正的查询\n    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);\n  } finally {\n    localCache.removeObject(key);\n  }\n  localCache.putObject(key, list);   \u002F\u002F最后会将查询结果放到缓存中\n  if (ms.getStatementType() == StatementType.CALLABLE) {  \n    \u002F\u002F单独处理存储过程，跟上面一样的\n    localOutputParameterCache.putObject(key, parameter);\n  }\n  return list;  \u002F\u002F最后返回\n}\n```\n\n这样，整个一级缓存就形成了一个完整的闭环。但是注意，就一级缓存的设计上来说，这里使用的是普通的Map类型进行存储，那必然是存在一定的并发问题的，因此，同一个SqlSession并不适用于多线程环境下使用，最好是一个线程配一个单独的SqlSession对象。\n\n我们接着来看二级缓存，二级缓存相比一级缓存就复杂很多了，它在我们上面的一级缓存代码的外层，我们可以在Mapper上开启二级缓存之后，直接断点看源码：\n\n```java\npublic \u003CE> List\u003CE> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {\n  Cache cache = ms.getCache();  \u002F\u002F如果开启了二级缓存，MappedStatement会存在一个Cache对象\n  if (cache != null) {\n    flushCacheIfRequired(ms);   \u002F\u002F首先和一级缓存一样，依然是先判断一次flushCache配置\n    if (ms.isUseCache() && resultHandler == null) {  \u002F\u002F单独判断一次当前的Statement是否配置了useCache选择，如果为false则依然不走二级缓存\n      ensureNoOutParams(ms, boundSql);  \u002F\u002F处理存储过程部分操作，无视掉\n      @SuppressWarnings(\"unchecked\")\n      \u002F\u002F从tcm中读取缓存数据，这里的tcm就是TransactionalCacheManager，由于二级缓存是事务性质的，有一个专门的缓存管理器来进行操作，对二级缓存相关操作进行封装\n      List\u003CE> list = (List\u003CE>) tcm.getObject(cache, key);\n      if (list == null) {\n        \u002F\u002F当二级缓存没有查询到数据时，直接走一级缓存，和最后一行代码一样\n        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);\n        tcm.putObject(cache, key, list); \u002F\u002F从一级缓存或是数据库查询到数据之后，放入二级缓存\n      }\n      return list;  \u002F\u002F返回最终结果\n    }\n  }\n  \u002F\u002F这里的delegate实际上是装饰模式的被装饰对象，同样为Executor，也就是我们之前的一级缓存SimpleExecutor\n  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);\n}\n```\n\n可以看到，二级缓存相比一级缓存来说，最大的区别就是使用了TransactionalCacheManager来进行缓存管理，那么我们又来研究一下它与我们直接使用PerpetualCache有什么区别，直接上源码：\n\n```java\n\u002F\u002F事务缓存管理器用于管理所有Mapper的二级缓存\npublic class TransactionalCacheManager {\n\t\u002F\u002F存储所有Mapper的二级缓存对象\n  private final Map\u003CCache, TransactionalCache> transactionalCaches = new HashMap\u003C>();\n\n  ...\n\n  public Object getObject(Cache cache, CacheKey key) {\n    \u002F\u002F直接获取map里面的缓存对象，然后进行操作\n    return getTransactionalCache(cache).getObject(key);\n  }\n\n  public void putObject(Cache cache, CacheKey key, Object value) {\n    getTransactionalCache(cache).putObject(key, value);   \u002F\u002F同上\n  }\n\n  ...\n\n  private TransactionalCache getTransactionalCache(Cache cache) {\n    \u002F\u002F这里用于判断是否存在对应的缓存，如果是第一次则直接创建一个新的TransactionalCache放入map的并返回\n    \u002F\u002F注意这里构造TransactionalCache时候使用的是一个Lamdba，实际调用的是TransactionalCache(Cache delegate)构造方法，把外面传入的cache对象存入到内部\n    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);\n  }\n}\n```\n\n这里用于存储缓存对象的map比较奇怪，它的键是Cache类型，也就是我们在XML里面配置的二级缓存类型，接着是一个TransactionalCache对象，实际上这个对象是对我们传入Cache的一个修饰，我们来看看它的内部构造：\n\n```java\npublic class TransactionalCache implements Cache {\n  private final Cache delegate;   \u002F\u002F被修饰的内部Cache对象\n  private final Map\u003CObject, Object> entriesToAddOnCommit;  \u002F\u002F暂存列表，因为二级缓存需要提交才能真正进入缓存，所以说没提交之前需要找个地方暂存\n  private final Set\u003CObject> entriesMissedInCache;   \u002F\u002F未命中列表，当从被修饰的Cache中获取缓存时发现此数据并不存在，那么将其Key放入未命中列表中，没错，没查到的数据也会缓存，防止“缓存穿透”\n  ...\n\n  @Override\n  public void putObject(Object key, Object object) {\n    \u002F\u002F在插入对象到缓存时，并没有直接调用被修饰的Cache对象进行缓存，而是将其添加到暂存列表中\n    entriesToAddOnCommit.put(key, object);\n  }\n  \n  ...\n  \n  public void commit() {\n    \u002F\u002F调用clear()方法后会标记为true，下次commit的时候就会清除，直接让被修饰的Cache对象清除数据，算是一种延时清除？\n    if (clearOnCommit) {\n      delegate.clear();\n    }\n    \u002F\u002F接着直接将所有暂存列表和未命中列表中的键值对全部放入被修饰的Cache对象中\n    flushPendingEntries();\n    \u002F\u002F清理暂存列表和未命中列表并重置提交状态\n    reset();\n  }\n  \n  private void flushPendingEntries() {\n    \u002F\u002F将所有暂存列表的数据全部放入被修饰的Cache中真正进入缓存\n    for (Map.Entry\u003CObject, Object> entry : entriesToAddOnCommit.entrySet()) {\n      delegate.putObject(entry.getKey(), entry.getValue());\n    }\n    \u002F\u002F将所有暂存列表中不存在的未命中列表的数据全部放入被修饰的Cache中真正进入缓存，值直接为null\n    for (Object entry : entriesMissedInCache) {\n      if (!entriesToAddOnCommit.containsKey(entry)) {\n        delegate.putObject(entry, null);\n      }\n    }\n  }\n  \n  @Override\n  public Object getObject(Object key) {\n    \u002F\u002F通过被修饰的Cache获取结果\n    Object object = delegate.getObject(key);\n    if (object == null) {   \u002F\u002F如果没拿到，说明没被缓存，记录到未命中列表中\n      entriesMissedInCache.add(key);\n    }\n    \u002F\u002F如果之前调用过clear()标记过清除，这里按理说应该是拿不到数据的，直接返回null\n    if (clearOnCommit) {\n      return null;\n    }\n    return object;   \u002F\u002F返回查询到的结果\n  }\n  \n  ...\n}\n```\n\n可以看到，在TransactionalCache中，采用修饰模式，为我们原本的Cache添加了事务相关操作的支持，使得整个二级缓存变成事务化的。\n\n我们接着来看外部传入的Cache对象又是什么鬼，这里TransactionalCache实际上就是直接对我们外部传入的Cache对象进行的修饰，我们可以在断点中查看：\n\n![QQ_1724349767437](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F23\u002F23ekzDoV4rsFpux.png)\n\n可见，二级缓存提供的Cache并不像一级缓存那样直接就是一个PerpetualCache对象，而是由多级进行修饰的强化版PerpetualCache对象，实际上这些都是对PerpetualCache的装饰类，它们分别用于：\n\n* SynchronizedCache - 用于控制并发加锁相关操作，保证二级缓存在多线程环境下正常运行\n* LoggingCache - 用于打印相关日志操作，主要是打印二级缓存的命中率\n* LruCache  -  二级缓存类型的默认配置，采用LRU算法对Key进行管理，并对缓存进行清理\n* PerpetualCache  -  最内层缓存具体实现类型，之前已经介绍过\n\n因此，现在整个Mybatis缓存机制就已经非常清晰了，我们可以来测试一下修改二级缓存的`eviction`属性：\n\n```java\n@CacheNamespace(readWrite = false, eviction = FifoCache.class)\npublic interface TestMapper {\n```\n\n实际上这里修改的正是第三层的修饰，而SynchronizedCache和LoggingCache是固定不变的：\n\n![QQ_1724350229621](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F23\u002FXqi2tnTCJdkcsvG.png)\n\n而最内层的PerpetualCache也是可以进行替换的，只不过具体的缓存实现需要我们自己实现Cache接口进行编写，Mybatis提供的几乎都是修饰类。现在附上一张完整的Mybatis内部缓存执行流程图：\n\n![QQ_1724351661372](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F23\u002FCm7FPeuz89abgI6.png)\n\n最后我们可以来尝试进行一些简单的编写和实现，这里仿照PerpetualCache写一个差不多的：\n\n```java\npublic class MyCache implements Cache {\n    private final Map\u003CObject, Object> map = new HashMap\u003C>();\n    private final String id;\n    \n  \t\u002F\u002F自定义Cache必须存在一个可以传递id的构造方法，这是规矩\n    public MyCache(String id) {\n        this.id = id;\n    }\n\n    @Override\n    public String getId() {\n        return id;\n    }\n\n    @Override\n    public void putObject(Object key, Object value) {\n        map.put(key, value);\n    }\n\n    @Override\n    public Object getObject(Object key) {\n        return map.get(key);\n    }\n\n    @Override\n    public Object removeObject(Object key) {\n        return map.remove(key);\n    }\n\n    @Override\n    public void clear() {\n        map.clear();\n    }\n\n    @Override\n    public int getSize() {\n        return map.size();\n    }\n}\n```\n\n要启用我们自定义的Cache只需要配置一下`implementation`属性即可：\n\n```java\n@CacheNamespace(readWrite = false, eviction = FifoCache.class, implementation = MyCache.class)\npublic interface TestMapper {\n```\n\n只不过，使用自定义的Cache实现之后，默认情况下Mybatis只会修饰一层日志处理，而并发控制和缓存数据清理则不会自动附加，因此我们在编写时一定要考虑并发问题和数据清理问题：\n\n![QQ_1724350700194](https:\u002F\u002Fs2.loli.net\u002F2024\u002F08\u002F23\u002FLn6X3GJwhsg7RFq.png)\n\n后续我们在学习完Redis相关课程后，各位小伙伴可以尝试使用Redis来做缓存，真正解决多个服务间不同步问题。\n\n","MyBatis 是一款优秀的持久层框架，它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息，将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。","2025-07-04 23:41:15",{"data":468,"status":460,"success":461},[469,474],{"id":8,"image":470,"link":471,"name":472,"type":473},"\u002Fimage\u002Fadv\u002Frainyun-2025-06.webp","https:\u002F\u002Fwww.rainyun.com\u002Fitbaima_","雨云优惠购","cloud",{"id":66,"image":475,"link":476,"name":477,"type":478},"\u002Fimage\u002Fadv\u002Fsimcard-2025-11.webp","https:\u002F\u002Fmall.itbaima.cn","号卡优惠","simcard"]