老杜MyBatis
一、MyBatis概述
1.1 框架
- 在文献中看到的framework被翻译为框架
- Java常用框架:
- SSM三大框架:Spring + SpringMVC + MyBatis
- SpringBoot
- SpringCloud
- 等。。
- 框架其实就是对通用代码的封装,提前写好了一堆接口和类,我们可以在做项目的时候直接引入这些接口和类(引入框架),基于这些现有的接口和类进行开发,可以大大提高开发效率。
- 框架一般都以jar包的形式存在。(jar包中有class文件以及各种配置文件等。)
- SSM三大框架的学习顺序:MyBatis、Spring、SpringMVC(仅仅是建议)
1.2 三层架构
- 表现层(UI):直接跟前端打交互(一是接收前端ajax请求,二是返回json数据给前端)
- 业务逻辑层(BLL):一是处理表现层转发过来的前端请求(也就是具体业务),二是将从持久层获取的数据返回到表现层。
- 数据访问层(DAL):直接操作数据库完成CRUD,并将获得的数据返回到上一层(也就是业务逻辑层)。
- Java持久层框架:
- MyBatis
- Hibernate(实现了JPA规范)
- jOOQ
- Guzz
- Spring Data(实现了JPA规范)
- ActiveJDBC
- ……
1.3 JDBC不足
- 示例代码1:
1 | // ...... |
- 示例代码2:
1 | // ...... |
- JDBC不足:
- SQL语句写死在Java程序中,不灵活。改SQL的话就要改Java代码。违背开闭原则OCP。
- 给?传值是繁琐的。能不能自动化???
- 将结果集封装成Java对象是繁琐的。能不能自动化???
1.4 了解MyBatis
- MyBatis本质上就是对JDBC的封装,通过MyBatis完成CRUD。
- MyBatis在三层架构中负责持久层的,属于持久层框架。
- MyBatis的发展历程:【引用百度百科】
- MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis。2013年11月迁移到Github。
- iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAOs)。
- 打开mybatis代码可以看到它的包结构中包含:ibatis
- ORM:对象关系映射
- O(Object):Java虚拟机中的Java对象
- R(Relational):关系型数据库
- M(Mapping):将Java虚拟机中的Java对象映射到数据库表中一行记录,或是将数据库表中一行记录映射成Java虚拟机中的一个Java对象。
- ORM图示
- MyBatis属于半自动化ORM框架。
- Hibernate属于全自动化的ORM框架。
- MyBatis框架特点:
- 支持定制化 SQL、存储过程、基本映射以及高级映射
- 避免了几乎所有的 JDBC 代码中手动设置参数以及获取结果集
- 支持XML开发,也支持注解式开发。【为了保证sql语句的灵活,所以mybatis大部分是采用XML方式开发。】
- 将接口和 Java 的 POJOs(Plain Ordinary Java Object,简单普通的Java对象)映射成数据库中的记录
- 体积小好学:两个jar包,两个XML配置文件。
- 完全做到sql解耦合。
- 提供了基本映射标签。
- 提供了高级映射标签。
- 提供了XML标签,支持动态SQL的编写。
- ……
二、MyBatis入门程序
只要你会JDBC,MyBatis就可以学。
2.1 版本
软件版本:
- IntelliJ IDEA:2022.1.4
- Navicat for MySQL:16.0.14
- MySQL数据库:8.0.30
组件版本:
- MySQL驱动:8.0.30
- MyBatis:3.5.10
- JDK:Java17
- JUnit:4.13.2
- Logback:1.2.11
2.2 MyBatis下载
- 从github上下载,地址:https://github.com/mybatis/mybatis-3
- 将框架以及框架的源码都下载下来,下载框架后解压,打开mybatis目录
- 通过以上解压可以看到,框架一般都是以jar包的形式存在。我们的mybatis课程使用maven,所以这个jar我们不需要。
- 官方手册需要。
2.3 MyBatis入门程序开发步骤
- 写代码前准备:
- 准备数据库表:汽车表t_car,字段包括:
- id:主键(自增)【bigint】
- car_num:汽车编号【varchar】
- brand:品牌【varchar】
- guide_price:厂家指导价【decimal类型,专门为财务数据准备的类型】
- produce_time:生产时间【char,年月日即可,10个长度,’2022-10-11’】
- car_type:汽车类型(燃油车、电车、氢能源)【varchar】
- 使用navicat for mysql工具建表
- 使用navicat for mysql工具向t_car表中插入两条数据,如下:
- 创建Project:建议创建Empty Project,设置Java版本以及编译版本等。
- 设置IDEA的maven
- 创建Module:普通的Maven Java模块
- 准备数据库表:汽车表t_car,字段包括:
- 步骤1:打包方式:jar(不需要war,因为mybatis封装的是jdbc。)
1 | <groupId>com.powernode</groupId> |
- 步骤2:引入依赖(mybatis依赖 + mysql驱动依赖)
1 | <!--mybatis核心依赖--> |
步骤3:编写mybatis核心配置文件, 在resources根目录下新建mybatis-config.xml配置文件(可以参考mybatis手册拷贝),这个文件名称和位置不是固定的,一般情况是放在类的根路径下(resources里的东西都属于类根路径)
另一个核心配置文件是xxxMapper.xml,在这个配置文件当中编写SQL语句(一表一个)。
- 比如t_user表会对应一个UserMapper.xml
以下是mybatis-config.xml
1 |
|
注意1:mybatis核心配置文件的文件名不一定是mybatis-config.xml,可以是其它名字。
注意2:mybatis核心配置文件存放的位置也可以随意。这里选择放在resources根下,相当于放到了类的根路径下。
- 步骤4:在resources根目录下新建CarMapper.xml配置文件(可以参考mybatis手册拷贝)
1 |
|
注意1:sql语句最后结尾可以不写“;”
注意2:CarMapper.xml文件的名字不是固定的。可以使用其它名字。
注意3:CarMapper.xml文件的位置也是随意的。这里选择放在resources根下,相当于放到了类的根路径下。
注意4:将CarMapper.xml文件路径配置到mybatis-config.xml:
1 | <mapper resource="CarMapper.xml"/> |
步骤5:编写MyBatisIntroductionTest代码
在MyBatis当中,负责执行SQL语句的那个对象叫做SqlSession,
SqlSession是专门用来执行SQL语句的,是一个Java程序和数据库之间的一次会话。
要想获取SqlSession对象,需要先获取SqlSessionFactory对象,通过SqlSessionFactory工厂来生产SqlSession对象。
怎么获取SqlSessionFactory对象呢?
需要首先获取SqlSessionFactoryBuilder对象。
通过SqlSessionFactoryBuilder对象的build方法,来获取一个SqlSessionFactory对象。
mybatis的核心对象包括:- SqlSessionFactoryBuilder
- SqlSessionFactory
- SqlSession
顺序是:SqlSessionFactoryBuilder –> SqlSessionFactory –> SqlSession
1 | /** |
关于第一个程序的小细节:
- mybatis中sql语句的结尾”;”可以省略。
- Resources.getResourceAsStream
- 小技巧:以后凡是遇到resource这个单词,大部分情况下,这种加载资源的方式就是从类的根路径下开始加载。(开始查找)
- 优点:采用这种方式,从类路径当中加载资源,项目的移植性很强。项目从windows移植到linux,代码不需要修改,因为这个资源文件一直都在类路径当中。
- InputStream is = new FileInputStream(“d:\mybatis-config.xml”);采用这种方式也可以。
缺点:可移植性太差,程序不够健壮。可能会移植到其他的操作系统当中。导致以上路径无效,还需要修改java代码中的路径。这样违背了OCP原则。 - CarMapper.xml文件的路径都不是固定的。
resource属性:这种方式是从类路径当中加载资源。 url属性:这种方式是从绝对路径当中加载资源。
注意1:默认采用的事务管理器是:JDBC。JDBC事务默认是不提交的,需要手动提交。
关于mybatis的事务管理机制。(深度剖析):
在mybatis-config.xml文件中,可以通过以下的配置进行mybatis的事务管理
type属性的值包括两个:
- JDBC(jdbc):JDBC事务管理器
- MANAGED(managed):MANAGED事务管理器
type后面的值,只有以上两个值可选,不区分大小写。
JDBC事务管理器:
mybatis框架自己管理事务,自己采用原生的JDBC代码去管理事务:1
2
3conn.setAutoCommit(false); 开启事务。
....业务处理...
conn.commit(); 手动提交事务使用JDBC事务管理器的话,底层创建的事务管理器对象:JdbcTransaction对象。
(看老杜源码剖析)如果你编写的代码是下面的代码就表示没有开启事务。因为这种方式压根不会执行conn.setAutoCommit(false);
1 | SqlSession sqlSession = sqlSessionFactory.openSession(true); |
在JDBC事务中,如果你没有在JDBC代码中执行:conn.setAutoCommit(false);的话,默认的autoCommit是true。
**重点:**如果autoCommit是true,就表示没有开启事务。只要执行任意一条DML语句就提交一次。只有你的autoCommit是false的时候,就表示开启了事务。
MANAGED事务管理器:
mybatis不再负责事务的管理了。事务管理交给其它容器来负责。例如:spring。也就是让spring负责事务
对于我们当前的单纯的只有mybatis的情况下,如果配置为:MANAGED
1
<transactionManager type="MANAGED"/>
那么事务这块是没人管的。没有人管理事务表示事务压根没有开启。没有人管理事务就是没有事务。
步骤6:运行程序,查看运行结果,以及数据库表中的数据
2.5 MyBatis第一个比较完整的代码写法
1 | /** |
运行后数据库表的变化:
2.6 引入JUnit
- JUnit是专门做单元测试的组件,在test文件夹执行。
在实际开发中,单元测试一般是由我们Java程序员来完成的。
我们要对我们自己写的每一个业务方法负责任,要保证每个业务方法在进行测试的时候都能通过。
测试的过程中涉及到两个概念:
- 期望值
- 实际值
@Test public void testSum(){ MathService mathService =new MathService(); // 获取实际值 int actual = mathService.sum(1, 2); // 期望值 int expected = 3; // 加断言进行测试 Assert.assertEquals(expected, actual); }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
- 期望值和实际值相同表示测试通过,期望值和实际值不同则单元测试执行时会报错。
- 这里引入JUnit是为了代替main方法。
- 使用JUnit步骤:
- 第一步:引入依赖
```xml
<!-- junit依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
```
- 第二步:编写单元测试类【测试用例】,测试用例中每一个测试方法上使用@Test注解进行标注。
- 测试用例的名字以及每个测试方法的定义都是有规范的:
- 测试用例的名字:XxxTest
- 测试方法声明格式:public void test业务方法名(){}
```java
// 测试用例
public class CarMapperTest{
// 测试方法
@Test
public void testInsertCar(){}//注意起名规范
@Test
public void testUpdate(){}
}
```
- 第三步:可以在类上执行,也可以在方法上执行
- 在类上执行时,该类中所有的测试方法都会执行。
- 在方法上执行时,只执行当前的测试方法。
- 编写一个测试用例,来测试insertCar业务
```java
public class CarMapperTest {
@Test
public void testInsertCar(){
SqlSession sqlSession = null;
try {
// 1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 3.创建SqlSession对象
sqlSession = sqlSessionFactory.openSession();
// 4.执行SQL
int count = sqlSession.insert("insertCar");
System.out.println("更新了几条记录:" + count);
// 5.提交
sqlSession.commit();
} catch (Exception e) {
// 回滚
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
// 6.关闭
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
```
执行单元测试,查看数据库表的变化:

## 2.7 引入日志框架logback
- 引入日志框架的目的是为了看清楚mybatis执行的具体sql。
- 启用标准日志组件,只需要在mybatis-config.xml文件中添加以下配置:【可参考mybatis手册】
```xml
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
```
标准日志也可以用,但是配置不够灵活,可以集成其他的日志组件,例如:SLF4J(沙拉风):沙拉风是一个日志标准,其中有一个框架叫做logback,它实现了沙拉风规范。,log4j,log4j2,logback等。
- logback是目前日志框架中性能较好的,较流行的,所以我们选它。log4j log4j2 logback都是同一个作者开发的。
- STDOUT_LOGGING是标准日志,mybatis已经实现了这种标准日志。mybatis框架本身已经实现了这种标准。开启方法就是上面的配置,这个标签在编写的时候要注意,它应该出现在environments标签之前。不需要记忆这个顺序,因为有dtd文件进行约束。
- 引入logback的步骤:
- 第一步:引入logback相关依赖
```xml
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
<scope>test</scope>
</dependency>
```
- 第二步:引入logback相关配置文件(文件名叫做logback.xml或logback-test.xml,必须放到类路径当中),这个主要配置日志输出相关的级别以及日志具体的格式。里面的输出格式都是可以更改的
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!--mybatis log configure-->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
```
- 再次执行单元测试方法testInsertCar,查看控制台是否有sql语句输出
- 
## 2.8 MyBatis工具类SqlSessionUtil的封装
- 每一次获取SqlSession对象代码太繁琐,封装一个工具类
```java
/**
* MyBatis工具类
*
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionUtil {
private static SqlSessionFactory sqlSessionFactory;
/**
* 类加载时初始化sqlSessionFactory对象
* SqlsessionUtil工具类在进行第一次加载的时候,解析mybatis-config.xml文件
*/
static {
try {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 每调用一次openSession()可获取一个新的会话,该会话支持自动提交。
*
* @return 新的会话对象
*/
public static SqlSession openSession() {
return sqlSessionFactory.openSession(true);
}
}
```
- 测试工具类,将testInsertCar()改造
```java
@Test
public void testInsertCar(){
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL
int count = sqlSession.insert("insertCar");
System.out.println("插入了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
```

# 三、使用MyBatis完成CRUD
- 准备工作
- 创建module(Maven的普通Java模块):mybatis-002-crud
- pom.xml
- 打包方式jar
- 依赖:
- mybatis依赖
- mysql驱动依赖
- junit依赖
- logback依赖
- mybatis-config.xml放在类的根路径下
- CarMapper.xml放在类的根路径下
- logback.xml放在类的根路径下
- 提供com.powernode.mybatis.utils.SqlSessionUtil工具类
- 创建测试用例:com.powernode.mybatis.CarMapperTest
## 3.1 insert(Create)
分析以下SQL映射文件中SQL语句存在的问题
```xml
<!--namespace先随便写-->
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values('103', '奔驰E300L', 50.3, '2022-01-01', '燃油车')
</insert>
</mapper>
```
存在的问题是:SQL语句中的值不应该写死,值应该是用户提供的。之前的JDBC代码是这样写的:
```java
// JDBC中使用 ? 作为占位符。那么MyBatis中会使用什么作为占位符呢?
String sql = "insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(?,?,?,?,?)";
// 给 ? 传值。那么MyBatis中应该怎么传值呢?
ps.setString(1,"103");
ps.setString(2,"奔驰E300L");
```
在MyBatis中可以这样做:
**在Java程序中,将数据放到Map集合中**
**在sql语句中使用 #{map集合的key} 来完成传值,#{} 等同于JDBC中的 ? ,#{}就是占位符**
Java程序这样写:
```java
/**
* 测试MyBatis的CRUD
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class CarMapperTest {
@Test
public void testInsertCar(){
// 前端传过来数据了。
//这个对象我们先使用Map集合进行数据的封装
Map<String, Object> map = new HashMap<>();
map.put("k1", "103");
map.put("k2", "奔驰E300L");
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句(使用map集合给sql语句传递数据)
//第一个参数:sqlId,从CarMapper.xml文件中复制:
//第二个参数:封装数据的对象
int count = sqlSession.insert("insertCar", map);
System.out.println("插入了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
}
```
SQL语句这样写:
```xml
<!--namespace先随便写-->
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{k1},#{k2},#{k3},#{k4},#{k5})
</insert>
</mapper>
```
**#{} 的里面必须填写map集合的key,不能随便写。**运行测试程序,查看数据库:

如果#{}里写的是map集合中不存在的key会有什么问题?
```xml
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{kk},#{k2},#{k3},#{k4},#{k5})
</insert>
</mapper>
```
运行程序:


通过测试,看到程序并没有报错。正常执行。不过 #{kk} 的写法导致无法获取到map集合中的数据,最终导致数据库表car_num插入了NULL。
在以上sql语句中,可以看到#{k1} #{k2} #{k3} #{k4} #{k5}的可读性太差,为了增强可读性,我们可以将Java程序做如下修改:
```java
Map<String, Object> map = new HashMap<>();
// 让key的可读性增强
map.put("carNum", "103");
map.put("brand", "奔驰E300L");
map.put("guidePrice", 50.3);
map.put("produceTime", "2020-10-01");
map.put("carType", "燃油车");
```
SQL语句做如下修改,这样可以增强程序的可读性:
```xml
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
</mapper>
```
运行程序,查看数据库表:

使用Map集合可以传参,那使用**pojo**(简单普通的java对象)可以完成传参吗?测试一下:
- 第一步:定义一个pojo类Car,提供相关属性。
```java
package com.powernode.mybatis.pojo;
/**
* POJOs,简单普通的Java对象。封装数据用的。
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Car {
//数据库表当中的字段应该和pojo类的属性一一对应:
//建议使用包装类,这样可以防止null的问题
private Long id;
private String carNum;
private String brand;
private Double guidePrice;
private String produceTime;
private String carType;
@Override
public String toString() {
return "Car{" +
"id=" + id +
", carNum='" + carNum + '\'' +
", brand='" + brand + '\'' +
", guidePrice=" + guidePrice +
", produceTime='" + produceTime + '\'' +
", carType='" + carType + '\'' +
'}';
}
public Car() {
}
public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) {
this.id = id;
this.carNum = carNum;
this.brand = brand;
this.guidePrice = guidePrice;
this.produceTime = produceTime;
this.carType = carType;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCarNum() {
return carNum;
}
public void setCarNum(String carNum) {
this.carNum = carNum;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Double getGuidePrice() {
return guidePrice;
}
public void setGuidePrice(Double guidePrice) {
this.guidePrice = guidePrice;
}
public String getProduceTime() {
return produceTime;
}
public void setProduceTime(String produceTime) {
this.produceTime = produceTime;
}
public String getCarType() {
return carType;
}
public void setCarType(String carType) {
this.carType = carType;
}
}
```
- 第二步:Java程序
```java
@Test
public void testInsertCarByPOJO(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 创建POJO,封装数据
Car car = new Car(null,"3333","奔驰C200",30.0,"2020-10-11","燃油车");
// 执行SQL,传数据
int count = sqlSession.insert("insertCarByPOJO", car);
System.out.println("插入了几条记录" + count);
sqlSession.commit();
sqlSession.close();
}
```
- 第三步:SQL语句
```xml
<insert id="insertCarByPOJO">
<!--#{} 里写的是POJO的属性名-->
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
```
- 运行程序,查看数据库表:

#{} 里写的是POJO的属性名,如果写成其他的会有问题吗?
```xml
<insert id="insertCarByPOJO">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{a},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
```
运行程序,出现了以下异常:

错误信息中描述:在Car类中没有找到a属性的getter方法。
修改POJO类Car的代码,**只将getCarNum()方法名修改为getA(),其他代码不变**,如下:

再运行程序,查看数据库表中数据:

**经过测试得出结论:**
**如果采用map集合传参,#{} 里写的是map集合的key,如果key不存在不会报错,数据库表中会插入NULL。**
**如果采用POJO传参,#{} 里写的是get方法的方法名去掉get之后将剩下的单词首字母变小写(例如:getAge()对应的是#{age},getUserName()对应的是#{userName}),如果这样的get方法不存在会报错。**
也就是说mybatis在底层给?传值的时候,先要获取值,怎么获取的? 调用了pojo对象的get方法。例如:car.getCarNum(),car.getCarType(),car.getBrand()
注意:其实传参数的时候有一个属性parameterType,这个属性用来指定传参的数据类型,不过这个属性是可以省略的
```xml
<insert id="insertCar" parameterType="java.util.Map">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
<insert id="insertCarByPOJO" parameterType="com.powernode.mybatis.pojo.Car">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
```
## 3.2 delete(Delete)
需求:根据car_num进行删除。
SQL语句这样写:
```xml
<delete id="deleteByCarNum">
delete from t_car where car_num = #{SuiBianXie}
</delete>
```
Java程序这样写:
```java
@Test
public void testDeleteByCarNum(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
int count = sqlSession.delete("deleteByCarNum", "102");
System.out.println("删除了几条记录:" + count);
}
```
运行结果:

**注意:当占位符只有一个的时候,${} 里面的内容可以随便写。但最好见名知意**
## 3.3 update(Update)
需求:修改id=34的Car信息,car_num为102,brand为比亚迪汉,guide_price为30.23,produce_time为2018-09-10,car_type为电车
修改前:

SQL语句如下:
```xml
<update id="updateCarByPOJO">
update t_car set
car_num = #{carNum}, brand = #{brand},
guide_price = #{guidePrice}, produce_time = #{produceTime},
car_type = #{carType}
where id = #{id}
</update>
```
Java代码如下:
```java
@Test
public void testUpdateCarByPOJO(){
// 准备数据
Car car = new Car(34L, "102", "比亚迪汉", 30.23, "2018-09-10", "电车");
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
int count = sqlSession.update("updateCarByPOJO", car);
System.out.println("更新了几条记录:" + count);
}
```
运行结果:


当然了,如果使用**map**传数据也是可以的。
## 3.4 select(Retrieve)
select语句和其它语句不同的是:查询会有一个结果集。来看mybatis是怎么处理结果集的!!!
### 查询一条数据
需求:查询id为1的Car信息
SQL语句如下:
```xml
<select id="selectCarById">
select * from t_car where id = #{id}
</select>
```
Java程序如下:
```java
@Test
public void testSelectCarById(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
Object car = sqlSession.selectOne("selectCarById", 1);
System.out.println(car);
}
```
运行结果如下:
```java
### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException:
A query was run and no Result Maps were found for the Mapped Statement 'car.selectCarById'. 【翻译】:对于一个查询语句来说,没有找到查询的结果映射。
It's likely that neither a Result Type nor a Result Map was specified. 【翻译】:很可能既没有指定结果类型,也没有指定结果映射。
```
以上的异常大致的意思是:对于一个查询语句来说,你需要指定它的“结果类型”或者“结果映射”。
所以说,你想让mybatis查询之后返回一个Java对象的话,至少你要告诉mybatis返回一个什么类型的Java对象,可以在<select>标签中添加resultType属性,用来指定查询要转换的类型:
```xml
<select id="selectCarById" resultType="com.powernode.mybatis.pojo.Car">
select * from t_car where id = #{id}
</select>
```
运行结果:

运行后之前的异常不再出现了,resultType是不能省略的。
但返回的Car对象,只有id和brand两个属性有值,其它属性的值都是null,这是为什么呢?我们来观察一下查询结果列名和Car类的属性名是否能一一对应:
查询结果集的列名:id, car_num, brand, guide_price, produce_time, car_type
Car类的属性名:id, carNum, brand, guidePrice, produceTime, carType
通过观察发现:只有id和brand是一致的,其他字段名和属性名对应不上,这是不是导致null的原因呢?我们尝试在sql语句中使用as关键字来给查询结果列名起别名试试:
```xml
<select id="selectCarById" resultType="com.powernode.mybatis.pojo.Car">
select
id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from
t_car
where
id = #{id}
</select>
```
运行结果如下:

通过测试得知,如果当查询结果的字段名和java类的属性名对应不上的话,可以采用as关键字起别名,**当然还有其它解决方案,我们后面再看**。
### 查询多条数据
需求:查询所有的Car信息。
SQL语句如下:
```xml
<!--虽然结果是List集合,但是resultType属性需要指定的是List集合中元素的类型。-->
<select id="selectCarAll" resultType="com.powernode.mybatis.pojo.Car">
<!--记得使用as起别名,让查询结果的字段名和java类的属性名对应上。-->
select
id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from
t_car
</select>
```
Java代码如下:
```java
@Test
public void testSelectCarAll(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
List<Object> cars = sqlSession.selectList("selectCarAll");
// 输出结果
cars.forEach(car -> System.out.println(car));
}
```
运行结果如下:

## 3.5 关于SQL Mapper的namespace
在SQL Mapper配置文件中<mapper>标签的namespace属性可以翻译为命名空间,这个命名空间主要是为了防止sqlId冲突的。
创建CarMapper2.xml文件,代码如下:
```xml
<mapper namespace="car2">
<select id="selectCarAll" resultType="com.powernode.mybatis.pojo.Car">
select
id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from
t_car
</select>
</mapper>
```
不难看出,CarMapper.xml和CarMapper2.xml文件中都有 id="selectCarAll"
将CarMapper2.xml配置到mybatis-config.xml文件中。
```xml
<mappers>
<mapper resource="CarMapper.xml"/>
<mapper resource="CarMapper2.xml"/>
</mappers>
```
编写Java代码如下:
```java
@Test
public void testNamespace(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
List<Object> cars = sqlSession.selectList("selectCarAll");
// 输出结果
cars.forEach(car -> System.out.println(car));
}
```
运行结果如下:
```
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.IllegalArgumentException:
selectCarAll is ambiguous in Mapped Statements collection (try using the full name including the namespace, or rename one of the entries)
【翻译】selectCarAll在Mapped Statements集合中不明确(请尝试使用包含名称空间的全名,或重命名其中一个条目)
【大致意思是】selectCarAll重名了,你要么在selectCarAll前添加一个名称空间,要有你改个其它名字。
```
Java代码修改如下:
正确完整的写法:namespace+id,以后都要这么写
```java
@Test
public void testNamespace(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
// 正确完整的写法:namespace+id,以后都要这么写
//List<Object> cars = sqlSession.selectList("car.selectCarAll");
List<Object> cars = sqlSession.selectList("car2.selectCarAll");
// 输出结果
cars.forEach(car -> System.out.println(car));
}
```
运行结果如下:


# 四、MyBatis核心配置文件详解
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration这个单词一般就是xml文件中根标签的名字
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd"这个是dtd约束,规范标签>
<configuration>
<!--创建SqlSessionFactory时默认使用的环境-->
<environments default="development">
<!--其中的一个环境。连接的数据库是powernode-->
<!--一般一个环境/数据库会对应一个SqlSessionFactory对象。-->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
<mapper resource="CarMapper2.xml"/>
</mappers>
</configuration>
```
- configuration:根标签,表示配置信息。
- environments:环境(多个),以“s”结尾表示复数,也就是说mybatis的环境可以配置多个数据源。
- default属性:表示默认使用的是哪个环境,default后面填写的是environment的id。**default的值只需要和environment的id值一致即可**。
- environment:具体的环境配置(**主要包括:事务管理器的配置 + 数据源的配置**)
- id:给当前环境一个唯一标识,该标识用在environments的default后面,用来指定默认环境的选择。
- transactionManager:配置事务管理器
- type属性:指定事务管理器具体使用什么方式,可选值包括两个
- **JDBC**:使用JDBC原生的事务管理机制。**底层原理:事务开启conn.setAutoCommit(false); ...处理业务...事务提交conn.commit();**
- **MANAGED**:交给其它容器来管理事务,比如spring、WebLogic、JBOSS等。如果没有管理事务的容器,则没有事务。**没有事务的含义:只要执行一条DML语句,则提交一次**。
- 在mybatis中提供了一个事务管理器接口:Transaction
该接口下有两个实现类:
JdbcTransaction
ManagedTransaction
如果type="JDBC",那么底层会实例化JdbcTransaction对象。
如果type="MANAGED",那么底层会实例化ManagedTransaction
- dataSource:指定数据源
- dataSource的作用是为程序提供Connection对象,但凡是给程序提供Connection对象的都可以叫数据源
- 数据源实际上是一套JDK提供的规范:javax.sql.DataSource,自己写的数据源只要实现javax.sql.DataSource接口就可以
- type属性:用来指定具体使用的数据库连接池的策略,可选值包括三个
- **UNPOOLED**:不使用数据库连接池,采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口
- property可以是:
- driver 这是 JDBC 驱动的 Java 类全限定名。
- url 这是数据库的 JDBC URL 地址。
- username 登录数据库的用户名。
- password 登录数据库的密码。
- defaultTransactionIsolationLevel 默认的连接事务隔离级别。
- defaultNetworkTimeout 等待数据库操作完成的默认网络超时时间(单位:毫秒)
- **POOLED**:使用mybatis自己实现的数据库连接池,采用传统的javax.sql.DataSource规范,除了下面这些,UNPOOLED的也都可以使用
- property可以是(除了包含**UNPOOLED**中之外):
- poolMaximumActiveConnections 连接池当中最多的正在使用的连接对象的数量上限。最多有多少个连接可以活动。默认值:10
- poolMaximumIdleCheckoutTime 强行让某个连接空闲,超时时间的设置
- poolMaximumIdleConnections 任意时间可能存在的空闲连接数。
- poolTimeToWait每隔多少秒打印日志,并且尝试获取连接对象
- 其它....
- **JNDI**:java命名目录接口,集成其他第三方数据库连接池,大部分web容器都实现了JNDI规范比如tomcat,采用服务器提供的JNDI技术实现来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。如果不是web或者maven的war工程,JNDI是不能使用的。
- property可以是(最多只包含以下两个属性):
- initial_context 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
- data_source 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
- mappers:在mappers标签中可以配置多个sql映射文件的路径。
- mapper:配置某个sql映射文件的路径
- resource属性:使用相对于类路径的资源引用方式
- url属性:使用完全限定资源定位符(URL)方式
## 4.1 environment
mybatis-003-configuration
```xml
<configuration>
<!--默认使用开发环境-->
<!--<environments default="dev">-->
<!--默认使用生产环境-->
<environments default="production">
<!--开发环境-->
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<!--生产环境-->
<environment id="production">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
```
```xml
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
</mapper>
```
```java
public class ConfigurationTest {
@Test
public void testEnvironment() throws Exception{
// 准备数据
Car car = new Car();
car.setCarNum("133");
car.setBrand("丰田霸道");
car.setGuidePrice(50.3);
car.setProduceTime("2020-01-10");
car.setCarType("燃油车");
// 一个数据库对应一个SqlSessionFactory对象
// 两个数据库对应两个SqlSessionFactory对象,以此类推
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 使用默认数据库(默认production)
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession(true);
int count = sqlSession.insert("car.insertCar", car);
System.out.println("插入了几条记录:" + count);
// 使用指定数据库,后面的参数是String environment类型,用于指定环境
SqlSessionFactory sqlSessionFactory1 = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "dev");
SqlSession sqlSession1 = sqlSessionFactory1.openSession(true);
int count1 = sqlSession1.insert("car.insertCar", car);
System.out.println("插入了几条记录:" + count1);
}
}
```
执行结果:


## 4.2 transactionManager
```xml
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="MANAGED"/>
```
```java
@Test
public void testTransactionManager() throws Exception{
// 准备数据
Car car = new Car();
car.setCarNum("133");
car.setBrand("丰田霸道");
car.setGuidePrice(50.3);
car.setProduceTime("2020-01-10");
car.setCarType("燃油车");
// 获取SqlSessionFactory对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config2.xml"));
// 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行SQL
int count = sqlSession.insert("insertCar", car);
System.out.println("插入了几条记录:" + count);
}
```
当事务管理器是:JDBC
- 采用JDBC的原生事务机制:
- 开启事务:conn.setAutoCommit(false);
- 处理业务......
- 提交事务:conn.commit();
当事务管理器是:MANAGED
- 交给容器去管理事务,但目前使用的是本地程序,没有容器的支持,**当mybatis找不到容器的支持时:没有事务**。也就是说只要执行一条DML语句,则提交一次。
## 4.3 dataSource
```xml
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
```
```java
@Test
public void testDataSource() throws Exception{
// 准备数据
Car car = new Car();
car.setCarNum("133");
car.setBrand("丰田霸道");
car.setGuidePrice(50.3);
car.setProduceTime("2020-01-10");
car.setCarType("燃油车");
// 获取SqlSessionFactory对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config3.xml"));
// 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);
// 执行SQL
int count = sqlSession.insert("insertCar", car);
System.out.println("插入了几条记录:" + count);
// 关闭会话
sqlSession.close();
}
```
当type是UNPOOLED,控制台输出:

修改配置文件mybatis-config3.xml中的配置:
```xml
<dataSource type="POOLED">
```
Java测试程序不需要修改,直接执行,看控制台输出:

通过测试得出:UNPOOLED不会使用连接池,每一次都会新建JDBC连接对象。POOLED会使用数据库连接池。【这个连接池是mybatis自己实现的。】
```xml
<dataSource type="JNDI">
```
JNDI的方式:表示对接JNDI服务器中的连接池。这种方式给了我们可以使用第三方连接池的接口。如果想使用dbcp、c3p0、druid(德鲁伊)等,需要使用这种方式。
这种再重点说一下type="POOLED"的时候,它的属性有哪些?
```xml
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<!--最大连接数-->
<property name="poolMaximumActiveConnections" value="3"/>
<!--这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。-->
<property name="poolTimeToWait" value="20000"/>
<!--强行回归池的时间-->
<property name="poolMaximumCheckoutTime" value="20000"/>
<!--最多空闲数量-->
<property name="poolMaximumIdleConnections" value="1"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
```
poolMaximumActiveConnections:最大的活动的连接数量。默认值10
poolMaximumIdleConnections:最大的空闲连接数量。默认值5
poolMaximumCheckoutTime:强行回归池的时间。默认值20秒。
poolTimeToWait:当无法获取到空闲连接时,每隔20秒打印一次日志,避免因代码配置有误,导致傻等。(时长是可以配置的)
当然,还有其他属性。对于连接池来说,以上几个属性比较重要。
最大的活动的连接数量就是连接池连接数量的上限。默认值10,如果有10个请求正在使用这10个连接,第11个请求只能等待空闲连接。
最大的空闲连接数量。默认值5,如何已经有了5个空闲连接,当第6个连接要空闲下来的时候,连接池会选择关闭该连接对象。来减少数据库的开销。
需要根据系统的并发情况,来合理调整连接池最大连接数以及最多空闲数量。充分发挥数据库连接池的性能。【可以根据实际情况进行测试,然后调整一个合理的数量。】
下图是默认配置:

在以上配置的基础之上,可以编写java程序测试:
```java
@Test
public void testPool() throws Exception{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config3.xml"));
for (int i = 0; i < 4; i++) {
SqlSession sqlSession = sqlSessionFactory.openSession();
Object selectCarByCarNum = sqlSession.selectOne("selectCarByCarNum");
}
}
```
```xml
<select id="selectCarByCarNum" resultType="com.powernode.mybatis.pojo.Car">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where car_num = '100'
</select>
```

## 4.4 properties
mybatis提供了更加灵活的配置,连接数据库的信息可以单独写到一个属性资源文件中,假设在类的根路径下创建jdbc.properties文件,配置如下:
```properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/powernode
```
在mybatis核心配置文件中引入并使用:
```xml
<configuration>
<!--引入外部属性资源文件-->
<properties resource="jdbc.properties">
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="root"/>
</properties>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--${key}使用-->
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
```
编写Java程序进行测试:
```java
@Test
public void testProperties() throws Exception{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config4.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
Object car = sqlSession.selectOne("selectCarByCarNum");
System.out.println(car);
}
```
**properties两个属性:**
**resource:这个属性从类的根路径下开始加载。【常用的。】**
**url:从指定的url加载,假设文件放在d:/jdbc.properties,这个url可以写成:file:///d:/jdbc.properties。注意是三个斜杠哦。**
注意:如果不知道mybatis-config.xml文件中标签的编写顺序的话,可以有两种方式知道它的顺序:
- 第一种方式:查看dtd约束文件。
- 第二种方式:通过idea的报错提示信息。【一般采用这种方式】
## 4.5 mapper
mapper标签用来指定SQL映射文件的路径,包含多种指定方式,这里先主要看其中两种:
第一种:resource,从类的根路径下开始加载【比url常用】
```xml
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
```
如果是这样写的话,必须保证类的根下有CarMapper.xml文件。
如果类的根路径下有一个包叫做test,CarMapper.xml如果放在test包下的话,这个配置应该是这样写:
```xml
<mappers>
<mapper resource="test/CarMapper.xml"/>
</mappers>
```
第二种:url,从指定的url位置加载
假设CarMapper.xml文件放在d盘的根下,这个配置就需要这样写:
```xml
<mappers>
<mapper url="file:///d:/CarMapper.xml"/>
</mappers>
```
**mapper还有其他的指定方式,后面再看!!!**

# 五、手写MyBatis框架(掌握原理)
警示:该部分内容有难度,基础较弱的程序员可能有些部分是听不懂的,如果无法跟下来,可直接跳过,不影响后续知识点的学习。当然,如果你要能够跟下来,必然会让你加深对MyBatis框架的理解。
**我们给自己的框架起个名:GodBatis(起名灵感来源于:my god!!! 我的天呢!)**
## 5.1 dom4j解析XML文件
该部分内容不再赘述,不会解析XML的,请观看老杜前面讲解的dom4j解析XML文件的视频。
模块名:parse-xml-by-dom4j(普通的Java Maven模块)
第一步:引入dom4j的依赖
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.group</groupId>
<artifactId>parse-xml-by-dom4j</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!--dom4j依赖-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!--jaxen依赖-->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
```
第二步:编写配置文件godbatis-config.xml
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<mappers>
<mapper resource="sqlmapper.xml"/>
</mappers>
</environments>
</configuration>
```
第三步:解析godbatis-config.xml
```java
package com.powernode.dom4j;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 使用dom4j解析XML文件
*/
public class ParseXMLByDom4jTest {
@Test
public void testGodBatisConfigXML() throws Exception{
// 读取xml,返回document对象
SAXReader reader = new SAXReader();
// 括号内的是获取输入流,然后读取xml文件,返回document对象。document对象是文档对象,代表了整个XML文件。
Document document = reader.read(ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml"));
// 获取文档当中的根标签
Element rootElt = document.getRootElement();
String rootEltName = rootElt.getName();
System.out.println("根节点的名字:"+ rootEltName);
// 获取<environments>标签的default属性的值
//xpath是做标签路径匹配的。能够让我们快速定位XML文件中的元素,后面的内容代表从根下开始找configuration标签,然后找configuration标签下的子标
String xpath = "/configuration/environments";
Element environments = (Element)document.selectSingleNode(xpath);签environments// Element是Node类的子类,方法更多,使用更便捷,要强转
// 获取属性的值
String defaultId = environmentsElt.attributeValue("default");
System.out.println("默认环境的id" + defaultId);
//根据默认id获取具体的环境environment
xpath = "/configuration/environments/environment[@id='"+defaultEnvironmentId+"']";
System.out.println(xpath);
// 获取事务管理器类型(transactionManager节点),Element的element()方法用来获取孩子节点
Element transactionManager = environment.element("transactionManager");
String transactionManagerType = transactionManager.attributeValue("type");
System.out.println(transactionManagerType);
// 获取数据源(dataSource)类型
Element dataSource = environmentElt.element("dataSource");
String dataSourceType = dataSource.attributeValue("type");
System.out.println(dataSourceType);
// 将dataSource信息封装到List集合,然后获取dataSource节点下的所有子节点
List<Element> propertyElts = dataSource.elements();
// 遍历
propertyElts.forEach(propertyElt ->{
String name = propertyElt.attributeValue("name");
String value = propertyElt.attributeValue("value");
System.out.println(name + "=" + value);
};
// 获取所有的mapper标签
//不想从根下开始获取,你想从任意位置开始,获取所有的某个标签,xpath该这样写
xpath ="//mapper";//两个斜杠
List<Node> mappers = document.selectNodes(xpath);
// 遍历
mappers.forEach(mapper ->{
Element mapperElt =(Element)mapper;
String resource = mapperElt.attributeValue( s: "resource");
System.out.println(resource);
});
}
}
```
执行结果:

第四步:编写配置文件CarMapper.xml
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
<select id="selectCarByCarNum" resultType="com.powernode.mybatis.pojo.Car">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where car_num = #{carNum}
</select>
</mapper>
```
第五步:解析CarMapper.xml
```java
@Test
public void testParseSqlMapperXML() throws Exception{
// 读取xml,获取document对象
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Thread.currentThread().getContextClassLoader().getResourceAsStream("CarMapper.xml"));
// 获取namespace
String xpath = "/mapper";
Element mapperElt = (Element) document.selectSingleNode(xpath);
String namespace = mapperElt.attributeValue("namespace");
System.out.println(namespace);
// 获取mapper节点下所有的子节点
List<Element> elements = mapper.elements();
// 遍历
elements().forEach(elements -> {
// 获取sqlId
String id = elements.attributeValue("id");
System.out.println(id);
// 如果是select标签,还要获取它的resultType
String resultType = elements.attributeValue("resultType");//没有这个属性的话会自动返回"null“
System.out.println("resultType:" + resultType);
// sql语句(getTextTrim表示获取标签中的文本内容,而且去除前后空白)
String sql = statementElt.getTextTrim();
System.out.println("sql:" + sql);
// mybaits封装了jdbc。早晚要执行带有?的sql语句。
// 转换
String newSql =sql.replaceAll("#\\{[0-9A-Za-z_$]*}","?");
System.out.println(newSql);
});
}
```
执行结果:

## 5.2 GodBatis
手写框架之前,如果没有思路,可以先参考一下mybatis的客户端程序,通过客户端程序来逆推需要的类,参考代码:
```java
@Test
public void testInsert(){
SqlSession sqlSession = null;
try {
// 1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 3.创建SqlSession对象
sqlSession = sqlSessionFactory.openSession();
// 4.执行SQL
Car car = new Car(null, "111", "宝马X7", "70.3", "2010-10-11", "燃油车");
int count = sqlSession.insert("insertCar",car);
System.out.println("更新了几条记录:" + count);
// 5.提交
sqlSession.commit();
} catch (Exception e) {
// 回滚
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
// 6.关闭
if (sqlSession != null) {
sqlSession.close();
}
}
}
@Test
public void testSelectOne(){
SqlSession sqlSession = null;
try {
// 1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 3.创建SqlSession对象
sqlSession = sqlSessionFactory.openSession();
// 4.执行SQL
Car car = (Car)sqlSession.selectOne("selectCarByCarNum", "111");
System.out.println(car);
// 5.提交
sqlSession.commit();
} catch (Exception e) {
// 回滚
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
// 6.关闭
if (sqlSession != null) {
sqlSession.close();
}
}
}
```
### 第一步:IDEA中创建模块
模块:godbatis(创建普通的Java Maven模块,打包方式jar),引入相关依赖
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.god</groupId>
<artifactId>godbatis</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!--dom4j依赖-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!--jaxen依赖-->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
```
### 第二步:资源工具类,方便获取指向配置文件的输入流
```java
package org.god.core;
import java.io.InputStream;
/**
* 资源工具类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Resources {
/**
* 从类路径中获取配置文件的输入流
* @param config
* @return 输入流,该输入流指向类路径中的配置文件
*/
public static InputStream getResourcesAsStream(String config){
return Thread.currentThread().getContextClassLoader().getResourceAsStream(config);
}
}
```
### 第三步:定义SqlSessionFactoryBuilder类
提供一个无参数构造方法,再提供一个build方法,该build方法要返回SqlSessionFactory对象
```java
package org.god.core;
import java.io.InputStream;
/**
* SqlSessionFactory对象构建器
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactoryBuilder {
/**
* 创建构建器对象
*/
public SqlSessionFactoryBuilder() {
}
/**
* 获取SqlSessionFactory对象
* 该方法主要功能是:读取godbatis核心配置文件,并构建SqlSessionFactory对象
* @param inputStream 指向核心配置文件的输入流
* @return SqlSessionFactory对象
*/
public SqlSessionFactory build(InputStream inputStream){
// 解析配置文件,创建数据源对象
// 解析配置文件,创建事务管理器对象
// 解析配置文件,获取所有的SQL映射对象
// 将以上信息封装到SqlSessionFactory对象中
// 返回
return null;
}
}
```
### 第四步:分析SqlSessionFactory类中有哪些属性
- 事务管理器
- GodJDBCTransaction
- SQL映射对象集合
- Map<String, GodMappedStatement>
### 第五步:定义GodJDBCTransaction
事务管理器最好是定义一个接口,然后每一个具体的事务管理器都实现这个接口。
```java
package org.god.core;
import java.sql.Connection;
/**
* 事务管理器接口
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface TransactionManager {
/**
* 提交事务
*/
void commit();
/**
* 回滚事务
*/
void rollback();
/**
* 关闭事务
*/
void close();
/**
* 开启连接
*/
void openConnection();
/**
* 获取连接对象
* @return 连接对象
*/
Connection getConnection();
}
```
```java
package org.god.core;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 事务管理器
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class GodJDBCTransaction implements TransactionManager {
/**
* 连接对象,控制事务时需要
*/
private Connection conn;
/**
* 数据源对象
*/
private DataSource dataSource;
/**
* 自动提交标志:
* true表示自动提交
* false表示不自动提交
*/
private boolean autoCommit;
/**
* 构造事务管理器对象
* @param autoCommit
*/
public GodJDBCTransaction(DataSource dataSource, boolean autoCommit) {
this.dataSource = dataSource;
this.autoCommit = autoCommit;
}
/**
* 提交事务
*/
public void commit(){
try {
conn.commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 回滚事务
*/
public void rollback(){
try {
conn.rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void close() {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void openConnection() {
try {
this.conn = dataSource.getConnection();
this.conn.setAutoCommit(this.autoCommit);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Connection getConnection() {
return conn;
}
}
```
### 第六步:事务管理器中需要数据源,定义GodUNPOOLEDDataSource
```java
package org.god.core;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 数据源实现类,不使用连接池
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class GodUNPOOLEDDataSource implements javax.sql.DataSource{
private String url;
private String username;
private String password;
public GodUNPOOLEDDataSource(String driver, String url, String username, String password) {
try {
// 注册驱动
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
this.url = url;
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
```
### 第七步:定义GodMappedStatement
```java
package org.god.core;
/**
* SQL映射实体类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class GodMappedStatement {
private String sqlId;
private String resultType;
private String sql;
private String parameterType;
private String sqlType;
@Override
public String toString() {
return "GodMappedStatement{" +
"sqlId='" + sqlId + '\'' +
", resultType='" + resultType + '\'' +
", sql='" + sql + '\'' +
", parameterType='" + parameterType + '\'' +
", sqlType='" + sqlType + '\'' +
'}';
}
public String getSqlId() {
return sqlId;
}
public void setSqlId(String sqlId) {
this.sqlId = sqlId;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getSqlType() {
return sqlType;
}
public void setSqlType(String sqlType) {
this.sqlType = sqlType;
}
public GodMappedStatement(String sqlId, String resultType, String sql, String parameterType, String sqlType) {
this.sqlId = sqlId;
this.resultType = resultType;
this.sql = sql;
this.parameterType = parameterType;
this.sqlType = sqlType;
}
}
```
### 第八步:完善SqlSessionFactory类
```java
package org.god.core;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
/**
* SqlSession工厂对象,使用SqlSessionFactory可以获取会话对象
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactory {
private TransactionManager transactionManager;
private Map<String, GodMappedStatement> mappedStatements;
public SqlSessionFactory(TransactionManager transactionManager, Map<String, GodMappedStatement> mappedStatements) {
this.transactionManager = transactionManager;
this.mappedStatements = mappedStatements;
}
public TransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public Map<String, GodMappedStatement> getMappedStatements() {
return mappedStatements;
}
public void setMappedStatements(Map<String, GodMappedStatement> mappedStatements) {
this.mappedStatements = mappedStatements;
}
}
```
### 第九步:完善SqlSessionFactoryBuilder中的build方法
```java
package org.god.core;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.sql.DataSource;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* SqlSessionFactory对象构建器
*
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactoryBuilder {
/**
* 创建构建器对象
*/
public SqlSessionFactoryBuilder() {
}
/**
* 获取SqlSessionFactory对象
* 该方法主要功能是:读取godbatis核心配置文件,并构建SqlSessionFactory对象
*
* @param inputStream 指向核心配置文件的输入流
* @return SqlSessionFactory对象
*/
public SqlSessionFactory build(InputStream inputStream) throws DocumentException {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
Element environmentsElt = (Element) document.selectSingleNode("/configuration/environments");
String defaultEnv = environmentsElt.attributeValue("default");
Element environmentElt = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultEnv + "']");
// 解析配置文件,创建数据源对象
Element dataSourceElt = environmentElt.element("dataSource");
DataSource dataSource = getDataSource(dataSourceElt);
// 解析配置文件,创建事务管理器对象
Element transactionManagerElt = environmentElt.element("transactionManager");
TransactionManager transactionManager = getTransactionManager(transactionManagerElt, dataSource);
// 解析配置文件,获取所有的SQL映射对象
Element mappers = environmentsElt.element("mappers");
Map<String, GodMappedStatement> mappedStatements = getMappedStatements(mappers);
// 将以上信息封装到SqlSessionFactory对象中
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory(transactionManager, mappedStatements);
// 返回
return sqlSessionFactory;
}
private Map<String, GodMappedStatement> getMappedStatements(Element mappers) {
Map<String, GodMappedStatement> mappedStatements = new HashMap<>();
mappers.elements().forEach(mapperElt -> {
try {
String resource = mapperElt.attributeValue("resource");
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Resources.getResourcesAsStream(resource));
Element mapper = (Element) document.selectSingleNode("/mapper");
String namespace = mapper.attributeValue("namespace");
mapper.elements().forEach(sqlMapper -> {
String sqlId = sqlMapper.attributeValue("id");
String sql = sqlMapper.getTextTrim();
String parameterType = sqlMapper.attributeValue("parameterType");
String resultType = sqlMapper.attributeValue("resultType");
String sqlType = sqlMapper.getName().toLowerCase();
// 封装GodMappedStatement对象
GodMappedStatement godMappedStatement = new GodMappedStatement(sqlId, resultType, sql, parameterType, sqlType);
mappedStatements.put(namespace + "." + sqlId, godMappedStatement);
});
} catch (DocumentException e) {
throw new RuntimeException(e);
}
});
return mappedStatements;
}
private TransactionManager getTransactionManager(Element transactionManagerElt, DataSource dataSource) {
String type = transactionManagerElt.attributeValue("type").toUpperCase();
TransactionManager transactionManager = null;
if ("JDBC".equals(type)) {
// 使用JDBC事务
transactionManager = new GodJDBCTransaction(dataSource, false);
} else if ("MANAGED".equals(type)) {
// 事务管理器是交给JEE容器的
}
return transactionManager;
}
private DataSource getDataSource(Element dataSourceElt) {
// 获取所有数据源的属性配置
Map<String, String> dataSourceMap = new HashMap<>();
dataSourceElt.elements().forEach(propertyElt -> {
dataSourceMap.put(propertyElt.attributeValue("name"), propertyElt.attributeValue("value"));
});
String dataSourceType = dataSourceElt.attributeValue("type").toUpperCase();
DataSource dataSource = null;
if ("POOLED".equals(dataSourceType)) {
} else if ("UNPOOLED".equals(dataSourceType)) {
dataSource = new GodUNPOOLEDDataSource(dataSourceMap.get("driver"), dataSourceMap.get("url"), dataSourceMap.get("username"), dataSourceMap.get("password"));
} else if ("JNDI".equals(dataSourceType)) {
}
return dataSource;
}
}
```
### 第十步:在SqlSessionFactory中添加openSession方法
```java
public SqlSession openSession(){
transactionManager.openConnection();
SqlSession sqlSession = new SqlSession(transactionManager, mappedStatements);
return sqlSession;
}
```
### 第十一步:编写SqlSession类中commit rollback close方法
```java
package org.god.core;
import java.sql.SQLException;
import java.util.Map;
/**
* 数据库会话对象
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSession {
private TransactionManager transactionManager;
private Map<String, GodMappedStatement> mappedStatements;
public SqlSession(TransactionManager transactionManager, Map<String, GodMappedStatement> mappedStatements) {
this.transactionManager = transactionManager;
this.mappedStatements = mappedStatements;
}
public void commit(){
try {
transactionManager.getConnection().commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void rollback(){
try {
transactionManager.getConnection().rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void close(){
try {
transactionManager.getConnection().close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
```
### 第十二步:编写SqlSession类中的insert方法
```java
/**
* 插入数据
*
* @param sqlId 要执行的sqlId
* @param obj 插入的数据
* @return
*/
public int insert(String sqlId, Object obj) {
GodMappedStatement godMappedStatement = mappedStatements.get(sqlId);
Connection connection = transactionManager.getConnection();
// 获取sql语句
// insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
String godbatisSql = godMappedStatement.getSql();
// insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?)
String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_\\$]*}", "?");
// 重点一步
Map<Integer, String> map = new HashMap<>();
int index = 1;
while (godbatisSql.indexOf("#") >= 0) {
int beginIndex = godbatisSql.indexOf("#") + 2;
int endIndex = godbatisSql.indexOf("}");
map.put(index++, godbatisSql.substring(beginIndex, endIndex).trim());
godbatisSql = godbatisSql.substring(endIndex + 1);
}
final PreparedStatement ps;
try {
ps = connection.prepareStatement(sql);
// 给?赋值
map.forEach((k, v) -> {
try {
// 获取java实体类的get方法名
String getMethodName = "get" + v.toUpperCase().charAt(0) + v.substring(1);
Method getMethod = obj.getClass().getDeclaredMethod(getMethodName);
ps.setString(k, getMethod.invoke(obj).toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
int count = ps.executeUpdate();
ps.close();
return count;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
```
### 第十三步:编写SqlSession类中的selectOne方法
```java
/**
* 查询一个对象
* @param sqlId
* @param parameterObj
* @return
*/
public Object selectOne(String sqlId, Object parameterObj){
GodMappedStatement godMappedStatement = mappedStatements.get(sqlId);
Connection connection = transactionManager.getConnection();
// 获取sql语句
String godbatisSql = godMappedStatement.getSql();
String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_\\$]*}", "?");
// 执行sql
PreparedStatement ps = null;
ResultSet rs = null;
Object obj = null;
try {
ps = connection.prepareStatement(sql);
ps.setString(1, parameterObj.toString());
rs = ps.executeQuery();
if (rs.next()) {
// 将结果集封装对象,通过反射
String resultType = godMappedStatement.getResultType();
Class<?> aClass = Class.forName(resultType);
Constructor<?> con = aClass.getDeclaredConstructor();
obj = con.newInstance();
// 给对象obj属性赋值
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = rsmd.getColumnName(i);
String setMethodName = "set" + columnName.toUpperCase().charAt(0) + columnName.substring(1);
Method setMethod = aClass.getDeclaredMethod(setMethodName, aClass.getDeclaredField(columnName).getType());
setMethod.invoke(obj, rs.getString(columnName));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
try {
ps.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return obj;
}
```
## 5.3 GodBatis使用Maven打包

查看本地仓库中是否已经有jar包:

## 5.4 使用GodBatis
使用GodBatis就和使用MyBatis是一样的。
第一步:准备数据库表t_user

第二步:创建模块,普通的Java Maven模块:godbatis-test
第三步:引入依赖
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>godbatis-test</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!--godbatis依赖-->
<dependency>
<groupId>org.god</groupId>
<artifactId>godbatis</artifactId>
<version>1.0.0</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
```
第四步:编写pojo类
```java
package com.powernode.godbatis.pojo;
public class User {
private String id;
private String name;
private String email;
private String address;
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", email='" + email + '\'' +
", address='" + address + '\'' +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public User() {
}
public User(String id, String name, String email, String address) {
this.id = id;
this.name = name;
this.email = email;
this.address = address;
}
}
```
第五步:编写核心配置文件:godbatis-config.xml
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</environments>
</configuration>
```
第六步:编写sql映射文件:UserMapper.xml
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="user">
<insert id="insertUser">
insert into t_user(id,name,email,address) values(#{id},#{name},#{email},#{address})
</insert>
<select id="selectUserById" resultType="com.powernode.godbatis.pojo.User">
select * from t_user where id = #{id}
</select>
</mapper>
```
第七步:编写测试类
```java
package com.powernode.godbatis.test;
import com.powernode.godbatis.pojo.User;
import org.god.core.Resources;
import org.god.core.SqlSession;
import org.god.core.SqlSessionFactory;
import org.god.core.SqlSessionFactoryBuilder;
import org.junit.Test;
public class GodBatisTest {
@Test
public void testInsertUser() throws Exception{
User user = new User("1", "zhangsan", "zhangsan@1234.com", "北京大兴区");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourcesAsStream("godbatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
int count = sqlSession.insert("user.insertUser", user);
System.out.println("插入了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
@Test
public void testSelectUserById() throws Exception{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourcesAsStream("godbatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
Object user = sqlSession.selectOne("user.selectUserById", "1");
System.out.println(user);
sqlSession.close();
}
}
```
第八步:运行结果



## 5.5 总结MyBatis框架的重要实现原理
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="user">
<insert id="insertUser">
insert into t_user(id,name,email,address) values(#{id},#{name},#{email},#{address})
</insert>
<select id="selectUserById" resultType="com.powernode.godbatis.pojo.User">
select id,name,email,address from t_user where id = #{id}
</select>
</mapper>
```
思考两个问题:
- 为什么insert语句中 #{} 里填写的必须是属性名?
- 为什么select语句查询结果列名要属性名一致?

# 六、在WEB中应用MyBatis(使用MVC架构模式)
**目标:**
- 掌握mybatis在web应用中怎么用
- mybatis三大对象的作用域和生命周期
- ThreadLocal原理及使用
- 巩固MVC架构模式
- 为学习MyBatis的接口代理机制做准备
**实现功能:**
- 银行账户转账
**使用技术:**
- HTML + Servlet + MyBatis
**WEB应用的名称:**
- bank
## 6.1 需求描述

## 6.2 数据库表的设计和准备数据


## 6.3 实现步骤
### 第一步:环境搭建
- IDEA中创建Maven WEB应用(**mybatis-004-web**)

- IDEA配置Tomcat,这里Tomcat使用10+版本。并部署应用到tomcat。


- 默认创建的maven web应用没有java和resources目录,包括两种解决方案
- 第一种:自己手动加上。

- 第二种:修改maven-archetype-webapp-1.4.jar中的配置文件



- web.xml文件的版本较低,可以从tomcat10的样例文件中复制,然后修改
```xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
metadata-complete="true">
</web-app>
```
- 删除index.jsp文件,因为我们这个项目不使用JSP。只使用html。
- 确定pom.xml文件中的打包方式是war包,因为是web项目。
- 引入相关依赖
- 编译器版本修改为17
- 引入的依赖包括:mybatis,mysql驱动,junit,logback,servlet。
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>mybatis-004-web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<!--在maven中显示的名字-->
<name>mybatis-004-web</name>
<url>http://localhost:8080/bank</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--logback依赖-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!--servlet依赖-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>mybatis-004-web</finalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
```
- 引入相关配置文件,放到resources目录下(全部放到类的根路径下)
- mybatis-config.xml
- AccountMapper.xml
- logback.xml
- jdbc.properties
```properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/powernode
jdbc.username=root
jdbc.password=root
```
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"/>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--一定要注意这里的路径哦!!!-->
<mapper resource="AccountMapper.xml"/>
</mappers>
</configuration>
```
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="account">
</mapper>
```
### 第二步:前端页面index.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行账户转账</title>
</head>
<body>
<!--/bank是应用的根,部署web应用到tomcat的时候一定要注意这个名字-->
<form action="/bank/transfer" method="post">
转出账户:<input type="text" name="fromActno"/><br>
转入账户:<input type="text" name="toActno"/><br>
转账金额:<input type="text" name="money"/><br>
<input type="submit" value="转账"/>
</form>
</body>
</html>
```
### 第三步:创建pojo包、service包、dao包、web包、utils包
- com.powernode.bank.pojo
- com.powernode.bank.service
- com.powernode.bank.service.impl
- com.powernode.bank.dao
- com.powernode.bank.dao.impl
- com.powernode.bank.web.controller
- com.powernode.bank.exception
- com.powernode.bank.utils:**将之前编写的SqlSessionUtil工具类拷贝到该包下。**
### 第四步:定义pojo类:Account
```java
package com.powernode.bank.pojo;
/**
* 银行账户类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Account {
private Long id;
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
```
### 第五步:编写AccountDao接口,以及AccountDaoImpl实现类
分析dao中至少要提供几个方法,才能完成转账:
- 转账前需要查询余额是否充足:selectByActno
- 转账时要更新账户:update
```java
/**
* 账户数据访问对象
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface AccountDao {
/**
* 根据账号获取账户信息
* @param actno 账号
* @return 账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act 账户信息
* @return 1表示更新成功,其他值表示失败
*/
int updateByActno(Account act);
}
```
```java
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account acconut = (Account)sqlSession.selectOne("account.selectByActno", actno);
sqlSession.close();
return acconut;
}
@Override
public int update(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.updateByActno", act);
sqlSession.commit();
sqlSession.close();
return count;
}
}
```
### 第六步:AccountDaoImpl中编写了mybatis代码,需要编写SQL映射文件了
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="account">
<select id="selectByActno" resultType="com.powernode.bank.pojo.Account">
select * from t_act where actno = #{actno}
</select>
<update id="updateByActno">
update t_act set balance = #{balance} where actno = #{actno}
</update>
</mapper>
```
AccountServlet
~~~java
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
//为了让这个对象在其他方法中也可以用。声明为实例变量。
private AccountService accountService = new AccountServiceImpl();
@0verride
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException,I0Exception {
//获取表单数据
String fromActno =request.getParameter("fromActno");
String toActno =request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
try {
// 调用service的转账方法完成转账。(调业务层)
accountService.transfer(fromActno,toActno,money);
//程序能够走到这里,表示转账一定成功了。
//调用view完成展示结果。
response.sendRedirect(request.getContextPath() + "/success.html");
} catch(MoneyNotEnoughException e) {
response.sendRedirect(request.getContextPath() + "/error1.html");
}catch(TransferException e){
response.sendRedirect(request.getContextPath() + "/error2.html");
}catch(Exception e){
response.sendRedirect(request.getContextPath() + "/error2.html");
}
}
在写完代码测试的时候会出现使用注解但无法访问404的情况,这是因为在web.xml里有个metadata-complete,如果等于true就只支持配置文件,改成false就是注解和配置文件都支持
1 |
|
第七步:编写AccountService接口以及AccountServiceImpl
1 | /** |
1 | /** |
1 | /** |
1 | public class AccountServiceImpl implements AccountService { |
第八步:编写AccountController
1 | /** |
启动服务器,打开浏览器,输入地址:http://localhost:8080/bank,测试:
6.4 MyBatis对象作用域以及事务问题
MyBatis核心对象的作用域
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession(保证一个线程对应一个SqlSession)
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
1 | try (SqlSession session = sqlSessionFactory.openSession()) { |
事务问题
在之前的转账业务中,更新了两个账户,我们需要保证它们的同时成功或同时失败,这个时候就需要使用事务机制,在transfer方法开始执行时开启事务,直到两个更新都成功之后,再提交事务,我们尝试将transfer方法进行如下修改:
1 | public class AccountServiceImpl implements AccountService { |
运行前注意看数据库表中当前的数据:
执行程序:
再次查看数据库表中的数据:
傻眼了吧!!!事务出问题了,转账失败了,钱仍然是少了1万。这是什么原因呢?主要是因为service和dao中使用的SqlSession对象不是同一个。
怎么办?为了保证service和dao中使用的SqlSession对象是同一个,可以将SqlSession对象存放到ThreadLocal当中。修改SqlSessionUtil工具类:
1 | /** |
修改dao中的方法:AccountDaoImpl中所有方法中的提交commit和关闭close代码全部删除。
1 | public class AccountDaoImpl implements AccountDao { |
修改service中的方法:
1 | public class AccountServiceImpl implements AccountService { |
当前数据库表中的数据:
再次运行程序:
查看数据库表:没有问题。
再测试转账成功:
如果余额不足呢:
账户的余额依然正常:
6.5 分析当前程序存在的问题
我们来看一下DaoImpl的代码
1 | package com.powernode.bank.dao.impl; |
我们不难发现,这个dao实现类中的方法代码很固定,基本上就是一行代码,通过SqlSession对象调用insert、delete、update、select等方法,这个类中的方法没有任何业务逻辑,既然是这样,这个类我们能不能动态的生成,以后可以不写这个类吗?答案:可以。
七、使用javassist生成类
来自百度百科:
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。
7.1 Javassist的使用
我们要使用javassist,首先要引入它的依赖
1 | <dependency> |
样例代码:
1 | public class JavassistTest { |
运行要注意:加两个参数,要不然会有异常。
- –add-opens java.base/java.lang=ALL-UNNAMED
- –add-opens java.base/sun.net.util=ALL-UNNAMED
运行结果:
7.2 使用Javassist生成DaoImpl类
使用Javassist动态生成DaoImpl类
1 | /** |
凡是使用GenerateDaoProxy机制的。namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名,修改AccountMapper.xml文件:
1 |
|
修改service类中获取dao对象的代码:
1 | public class AccountServiceImpl implements AccountService{ |
启动服务器:启动过程中显示,tomcat服务器自动添加了以下的两个运行参数。所以不需要再单独配置。
测试前数据:
打开浏览器测试:
八、MyBatis中接口代理机制及使用
好消息!!!其实以上所讲内容mybatis内部已经实现了。直接调用以下代码即可获取dao接口的代理类:
1 | AccountDao accountDao = (AccountDao)sqlSession.getMapper(AccountDao.class); |
使用以上代码的前提是:AccountMapper.xml文件中的namespace必须和dao接口的全限定名称一致,id必须和dao接口中方法名一致。
将service中获取dao对象的代码再次修改,如下:
测试前数据:
测试后数据:
九、MyBatis小技巧
9.1 #{}和${}
#{}和${}的区别:
#{}:先编译sql语句,再给占位符传值,底层是PreparedStatement实现。可以防止sql注入,比较常用。
${}:先进行sql语句拼接,然后再编译sql语句,底层是Statement实现。存在sql注入现象。只有在需要进行sql语句关键字拼接的情况下才会用到。
需求:根据car_type查询汽车
模块名:mybatis-005-antic
使用#{}
CarMapper.xml,放在类的根路径下:注意namespace必须和接口名一致。id必须和接口中方法名一致。
1 |
|
测试程序
1 | package com.powernode.mybatis.test; |
执行结果:
通过执行可以清楚的看到,sql语句中是带有 ? 的,这个 ? 就是大家在JDBC中所学的占位符,专门用来接收值的。
把“燃油车”以String类型的值,传递给 ?
这就是 #{},它会先进行sql语句的预编译,然后再给占位符传值
使用${}
同样的需求,我们使用${}来完成
CarMapper.xml文件修改如下:
1 |
|
再次运行测试程序:
出现异常了,这是为什么呢?看看生成的sql语句:
很显然,${} 是先进行sql语句的拼接,然后再编译,出现语法错误是正常的,因为 燃油车 是一个字符串,在sql语句中应该添加单引号
这是#{}的sql语句:
1 | Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = ? |
这是${}的sql语句
1 | Preparing:select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as car_Type from t_car where car_type = 新能源 |
什么情况下必须使用${}
当需要进行sql语句关键字拼接的时候。必须使用${}
需求:通过向sql语句中注入asc或desc关键字,来完成数据的升序或降序排列。
- 先使用#{}尝试:
CarMapper接口:
1 | /** |
CarMapper.xml文件:
1 | <select id="selectAll" resultType="com.powernode.mybatis.pojo.Car"> |
测试程序
1 |
|
运行:
报错的原因是sql语句不合法,因为采用这种方式传值,最终sql语句会是这样:
1 | select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType from t_car order by carNum 'desc' |
desc是一个关键字,不能带单引号的,所以在进行sql语句关键字拼接的时候,必须使用${}
- 使用${} 改造
1 | <select id="selectAll" resultType="com.powernode.mybatis.pojo.Car"> |
再次执行测试程序:
拼接表名
业务背景:实际开发中,有的表数据量非常庞大,可能会采用分表方式进行存储,比如每天生成一张表,表的名字与日期挂钩,例如:2022年8月1日生成的表:t_user20220108。2000年1月1日生成的表:t_user20000101。此时前端在进行查询的时候会提交一个具体的日期,比如前端提交的日期为:2000年1月1日,那么后端就会根据这个日期动态拼接表名为:t_user20000101。有了这个表名之后,将表名拼接到sql语句当中,返回查询结果。那么大家思考一下,拼接表名到sql语句当中应该使用#{} 还是 ${} 呢?
使用#{}会是这样:select * from ‘t_car’
使用${}会是这样:select * from t_car
1 | <select id="selectAllByTableName" resultType="car"> |
1 | /** |
1 |
|
执行结果:
批量删除
业务背景:一次删除多条记录。
对应的sql语句:
- delete from t_user where id = 1 or id = 2 or id = 3;这个后面动态sql会讲
- delete from t_user where id in(1, 2, 3);
假设现在使用in的方式处理,前端传过来的字符串:1, 2, 3
如果使用mybatis处理,应该使用#{} 还是 ${}
使用#{} :delete from t_user where id in(‘1,2,3’) 执行错误:1292 - Truncated incorrect DOUBLE value: ‘1,2,3’
使用${} :delete from t_user where id in(1, 2, 3)
1 | /** |
1 | <delete id="deleteBatch"> |
1 |
|
执行结果:
模糊查询
需求:查询奔驰系列的汽车。【只要品牌brand中含有奔驰两个字的都查询出来。实现下面的这种语句
1 | select * from t_car where brand like '%奔驰%'; |
实现
1 | /** |
1 |
|
1 | <select id="selectLikeByBrand" resultType="Car"> |
使用这个会报错,因为传到jdbc里会变成brand like ‘%?%’,只有单个?才是占位符,如果在单引号里面就不是占位符了
第一种方案:
1 | '%${brand}%' |
第二种方案:concat函数,这个是mysql数据库当中的一个函数,专门进行字符串拼接
1 | concat('%',#{brand},'%') |
第三种方案:
1 | concat('%','${brand}','%') |
第四种方案:
1 | "%"#{brand}"%" |
9.2 typeAliases(起别名)
namespace不可以起别名,必须全限定接口名
别名不区分大小写
我们来观察一下CarMapper.xml中的配置信息:
1 | <mapper namespace="com.powernode.mybatis.mapper.CarMapper"> |
resultType属性用来指定查询结果集的封装类型,这个名字可以起别名
在mybatis-config.xml文件中使用typeAliases标签来起别名,包括两种方式:
第一种方式:typeAlias
1 | <typeAliases> |
- 首先要注意typeAliases标签的放置位置,如果不清楚的话,可以看看错误提示信息。
- typeAliases标签中的typeAlias可以写多个。
- typeAlias:
- type属性:指定给哪个类起别名
- alias属性:指定别名。
- alias属性不是必须的,如果缺省的话,type属性指定的类型名的简类名作为别名。
- alias是大小写不敏感的。也就是说假设alias=”Car”,再用的时候,可以CAR,也可以car,也可以Car,都行。
第二种方式:package
如果一个包下的类太多,每个类都要起别名,会导致typeAlias标签配置较多,所以mybatis用提供package的配置方式,只需要指定包名,该包下的所有类都自动起别名,别名就是简类名。并且别名不区分大小写。
1 | <typeAliases> |
package也可以配置多个的。
9.3 mappers
SQL映射文件的配置方式包括以下四种:
resource
这种方式是从类的根路径下开始査找资源。采用这种方式的话,你的配置文件需要放到类路径当中才行。
1 | <mapper resource="CarMapper.xml"/> |
url
这种方式显然使用了绝对路径的方式,这种配置对SQL映射文件存放的位置没有要求,随意,但是可移植性差。
1 | <mapper url="file:///d:/carMapper.xml"/> |
class
这个位置提供的是mapper接口的全限定接口名,必须带有包名的。
思考:mapper标签的作用是指定SqlMapper.xm文件的路径,指定接口名有什么用呢?
1 | <mapper class="com.powernode.mybatis.mapper.CarMapper"/> |
如果你class指定是:com.powernode.mybatis.mapper.CarMapper
那么mybatis框架会自动去com/powernode/mybatis/mapper目录下査找carMapper.xml文件。
注意:也就是说:如果你采用这种方式,那么你必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下。并且名字一致。
CarMapper接口-> CarMapper.xml
LogMapper接口->LogMapper.xml
如果使用这种方式必须满足以下条件:
- SQL映射文件和mapper接口放在同一个目录下。
- SQL映射文件的名字也必须和mapper接口名一致。
1 | <mapper class="全限定接口名,带有包名"/> |
将CarMapper.xml文件移动到和mapper接口同一个目录下:
在resources目录下新建多重目录的话,必须是这样创建:
com/powernode/mybatis/mapper,不能com.powernode.mybatis.mapper
将CarMapper.xml文件移动到mapper目录下
修改mybatis-config.xml文件
1 | <mappers> |
package
如果class较多,可以使用这种package的方式,但前提条件和上一种方式一样。
1 | <!-- 这种方式在实际开发中是使用的。--> |
9.4 idea配置文件模板
mybatis-config.xml和SqlMapper.xml文件可以在IDEA中提前创建好模板,以后通过模板创建配置文件。(右击创建时会显示)
9.5 插入数据时获取自动生成的主键
前提是:主键是自动生成的。
业务背景:一个用户有多个角色。
插入一条新的记录之后,自动生成了主键,而这个主键需要在其他表中使用时。
插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的id插入到角色表的user_id字段上。
第一种方式:可以先插入用户数据,再写一条查询语句获取id,然后再插入user_id字段。【比较麻烦】
第二种方式:mybatis提供了一种方式更加便捷。
1 | /** |
1 | <!-- |
十、MyBatis参数处理
模块名:mybatis-006-param
表:t_student
表中现有数据:
pojo类:
1 | package com.powernode.mybatis.pojo; |
10.1 单个简单类型参数
简单类型包括:
- byte short int long float double char
- Byte Short Integer Long Float Double Character
- String
- java.util.Date
- java.sql.Date
1 | /** |
1 | public class StudentMapperTest { |
-
<mapper namespace="com.powernode.mybatis.mapper.StudentMapper"> <!--parameterType属性的作用: 告诉mybatis框架,我这个方法的参数类型是什么类型: mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。 SQL语句最终是这样的: select * from t_student where id = ? JDBC底层会通过ps.setXxx(第几个问号,传什么值)来传值 --> <!--这里直接是student是因为起了别名--> <select id="selectById" resultType="student" parameterType="java.lang.Long"> select * from t_student where id = #{id} </select> </mapper>1
2
3
4
5
6
7
8
9
10
11
12
13
- 也就是说对于mybatis来说,它是可以自动推断出ps.setXxxx()方法的。ps.setString()还是ps.setInt()。它可以自动推断。
通过mybatis手册可以看到parameterType也可以简写

其实SQL映射文件中的配置比较完整的写法是:
~~~xml
<select id="selectByName" resultType="student" parameterType="java.lang.String">
select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}
</select>
其中sql语句中的javaType是告诉mybatis传过来的值java类型是string,jdbcType是数据库表里的类型是,以及select标签中的parameterType属性,都是用来帮助mybatis进行类型确定的。不过这些配置多数是可以省略的。因为mybatis它有强大的自动类型推断机制。
- javaType:可以省略
- jdbcType:可以省略
- parameterType:可以省略
如果参数只有一个的话,#{} 里面的内容就随便写了。对于 ${} 来说,注意加单引号。
10.2 Map参数
需求:根据name和age查询
1 | /** |
1 |
|
1 | <insert id="insertStudentByMap" resultType="map"> |
测试运行正常。
**这种方式是手动封装Map集合,将每个条件以key和value的形式存放到集合中。然后在使用的时候通过#{map集合的key}来取值。**里面一定要写key
10.3 实体类参数
需求:插入一条Student数据
1 | /** |
1 | parameterType="student"省略也可以 |
1 |
|
运行正常,数据库中成功添加一条数据。
这里需要注意的是:#{} 里面写的是属性名字。这个属性名其本质上是:set/get方法名去掉set/get之后的名字。
10.4 多参数
需求:通过name和sex查询
1 | /** |
1 |
|
1 | <select id="selectByNameAndSex" resultType="student"> |
执行结果:
异常信息描述了:name参数找不到,可用的参数包括[arg1, arg0, param1, param2]
修改StudentMapper.xml配置文件:尝试使用[arg1, arg0, param1, param2]去参数
1 | <select id="selectByNameAndSex" resultType="student"> |
运行结果:
再次尝试修改StudentMapper.xml文件
1 | <select id="selectByNameAndSex" resultType="student"> |
通过测试可以看到:
- arg0 是第一个参数
- param1是第一个参数
- arg1 是第二个参数
- param2是第二个参数
实现原理:实际上在mybatis底层会创建一个map集合,以arg0/param1为key,以方法上的参数为value,例如以下代码:
1 | Map<String,Object> map = new HashMap<>(); |
注意:使用mybatis3.4.2之前的版本时:要用#{0}和#{1}这种形式。
10.5 @Param注解(命名参数)
可以不用arg0 arg1 param1 param2吗?这个map集合的key我们自定义可以吗?当然可以。使用@Param注解即可。这样可以增强可读性。
需求:根据name和age查询
1 | /** |
1 |
|
1 | <select id="selectByNameAndAge" resultType="student"> |
通过测试,一切正常。
核心:@Param(“这里填写的其实就是map集合的key“)
注意:使用了@Param注解之后,arg0和arg1失效,但param1和param2还可以
10.6 @Param源码分析
十一、MyBatis查询语句专题
模块名:mybatis-007-select
打包方式:jar
引入依赖:mysql驱动依赖、mybatis依赖、logback依赖、junit依赖。
引入配置文件:jdbc.properties、mybatis-config.xml、logback.xml
创建pojo类:Car
创建Mapper接口:CarMapper
创建Mapper接口对应的映射文件:com/powernode/mybatis/mapper/CarMapper.xml
创建单元测试:CarMapperTest
拷贝工具类:SqlSessionUtil
11.1 返回Car
当查询的结果,有对应的实体类,并且查询结果只有一条时:
1 | package com.powernode.mybatis.mapper; |
1 |
|
1 | package com.powernode.mybatis.test; |
执行结果:
查询结果是一条的话可以使用List集合接收吗?当然可以。
1 | /** |
1 | <select id="selectByIdToList" resultType="Car"> |
1 |
|
执行结果:
11.2 返回List
当查询的记录条数是多条的时候,必须使用集合接收。如果使用单个实体类接收会出现异常。
1 | /** |
1 | <select id="selectAll" resultType="Car"> |
1 |
|
如果返回多条记录,采用单个实体类接收会怎样?
1 | /** |
1 | <select id="selectAll2" resultType="Car"> |
1 |
|
执行结果:
11.3 返回Map
当返回的数据,没有合适的实体类对应的话,可以采用Map集合接收。字段名做key,字段值做value。
查询如果可以保证只有一条数据,则返回一个Map集合即可。
1 | /** |
1 | <select id="selectByIdRetMap" resultType="map"> |
resultMap=”map”,这是因为mybatis内置了很多别名。【参见mybatis开发手册】
1 |
|
执行结果:
当然,如果返回一个Map集合,可以将Map集合放到List集合中吗?当然可以,这里就不再测试了。
反过来,如果返回的不是一条记录,是多条记录的话,只采用单个Map集合接收,这样同样会出现之前的异常:TooManyResultsException
11.4 返回List
查询结果条数大于等于1条数据,则可以返回一个存储Map集合的List集合。List
1 | /** |
1 | <select id="selectAllRetListMap" resultType="map"> |
1 |
|
执行结果:
1 | [ |
11.5 返回Map<String,Map>
拿Car的id做key,以后取出对应的Map集合时更方便。
1 | /** |
1 | <select id="selectAllRetMap" resultType="map"> |
1 |
|
执行结果:
1 | { |
11.6 resultMap结果映射
查询结果的列名和java对象的属性名对应不上怎么办?
- 第一种方式:as 给列起别名
- 第二种方式:使用resultMap进行结果映射
- 第三种方式:是否开启驼峰命名自动映射(配置settings)
使用resultMap进行结果映射
1 | /** |
1 | <!-- |
1 |
|
执行结果正常。
是否开启驼峰命名自动映射
使用这种方式的前提是:属性名遵循Java的命名规范,数据库表的列名遵循SQL的命名规范。
Java命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式。
SQL命名规范:全部小写,单词之间采用下划线分割。
比如以下的对应关系:
| 实体类中的属性名 | 数据库表的列名 |
|---|---|
| carNum | car_num |
| carType | car_type |
| produceTime | produce_time |
如何启用该功能,在mybatis-config.xml文件中进行配置:
1 | <!--放在properties标签后面--> |
1 | /** |
1 | <select id="selectAllByMapUnderscoreToCamelCase" resultType="Car"> |
1 |
|
执行结果正常。
11.7 返回总记录条数
需求:查询总记录条数
1 | /** |
1 | <!--long是别名,可参考mybatis开发手册。--> |
1 |
|
十二、动态SQL
有的业务场景,也需要SQL语句进行动态拼接,例如:
- 批量删除
1 | delete from t_car where id in(1,2,3,4,5,6,......这里的值是动态的,根据用户选择的id不同,值是不同的); |
- 多条件查询
1 | select * from t_car where brand like '丰田%' and guide_price > 30 and .....; |
创建模块:mybatis-008-dynamic-sql
打包方式:jar
引入依赖:mysql驱动依赖、mybatis依赖、junit依赖、logback依赖
pojo:com.powernode.mybatis.pojo.Car
mapper接口:com.powernode.mybatis.mapper.CarMapper
引入配置文件:mybatis-config.xml、jdbc.properties、logback.xml
mapper配置文件:com/powernode/mybatis/mapper/CarMapper.xml
编写测试类:com.powernode.mybatis.test.CarMapperTest
拷贝工具类:SqlSessionUtil
12.1 if标签
需求:多条件查询。
可能的条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)
1 | public interface CarMapper { |
1 | <mapper namespace="com.powernode.mybatis.mapper.CarMapper"> |
1 | public class CarMapperTest { |
执行结果:
如果第一个条件为空,剩下两个条件不为空,会是怎样呢?
1 | List<Car> cars = mapper.selectByMultiCondition("", 20.0, "燃油车"); |
执行结果:
报错了,SQL语法有问题,where后面出现了and。这该怎么解决呢?
- 可以where后面添加一个恒成立的条件。
执行结果:
如果三个条件都是空,有影响吗?
1 | List<Car> cars = mapper.selectByMultiCondition("", null, ""); |
执行结果:
三个条件都不为空呢?
1 | List<Car> cars = mapper.selectByMultiCondition("丰田", 20.0, "燃油车"); |
执行结果:
12.2 where标签
where标签的作用:让where子句更加动态智能。
- 所有条件都为空时,where标签保证不会生成where子句。
- 自动去除某些条件前面多余的and或or。
继续使用if标签中的需求。
1 | /** |
1 | <select id="selectByMultiConditionWithWhere" resultType="car"> |
1 |
|
运行结果:
如果所有条件都是空呢?
1 | List<Car> cars = mapper.selectByMultiConditionWithWhere("", null, ""); |
运行结果:
它可以自动去掉前面多余的and,那可以自动去掉前面多余的or吗?
1 | List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 20.0, "燃油车"); |
1 | <select id="selectByMultiConditionWithWhere" resultType="car"> |
执行结果:
它可以自动去掉前面多余的and,那可以自动去掉后面多余的and吗?
1 | <select id="selectByMultiConditionWithWhere" resultType="car"> |
1 | // 让最后一个条件为空 |
运行结果:
很显然,后面多余的and是不会被去除的。
12.3 trim标签
trim标签的属性:
- prefix:在trim标签中的语句前添加内容
- suffix:在trim标签中的语句后添加内容
- prefixOverrides:前缀覆盖掉(去掉)
- suffixOverrides:后缀覆盖掉(去掉)
1 | /** |
1 | <select id="selectByMultiConditionWithTrim" resultType="car"> |
1 |
|
如果所有条件为空,where会被加上吗?
1 | List<Car> cars = mapper.selectByMultiConditionWithTrim("", null, ""); |
执行结果:不会加
12.4 set标签
主要使用在update语句当中,用来生成set关键字,同时去掉最后多余的“,”
比如我们只更新提交的不为空的字段,如果提交的数据是空或者””,那么这个字段我们将不更新。
1 | /** |
1 | <update id="updateWithSet"> |
1 |
|
执行结果:
12.5 choose when otherwise
这三个标签是在一起使用的:
1 | <choose> |
等同于:
1 | if(){ |
只有一个分支会被选择!!!!
需求:先根据品牌查询,如果没有提供品牌,再根据指导价格查询,如果没有提供指导价格,就根据生产日期查询。
1 | /** |
1 | <select id="selectWithChoose" resultType="car"> |
1 |
|
如果所有的条件都不满足,比如都为空,就至少会执行otherwise这个语句,只不过传进sql里的是null
12.6 foreach标签
循环数组或集合,动态生成sql,比如这样的SQL:
1 | delete from t_car where id in(1,2,3); |
1 | insert into t_car values |
批量删除
- 用in来删除
1 | /** |
1 | <!-- |
1 |
|
执行结果:
- 用or来删除
1 | /** |
1 | <!--这里分隔符用or是可以的,并且会自动加上空格--> |
1 |
|
执行结果:
批量添加
1 | /** |
1 | <!--这里就不要用open和close了--> |
1 |
|
执行结果:
12.7 sql标签与include标签
sql标签用来声明sql片段
include标签用来将声明的sql片段包含到某个sql语句当中
作用:代码复用。易维护。
1 | <!--把重复使用的sql语句放在下面,然后起个名字,在需要的地方使用<include>标签就可以--> |
十三、MyBatis的高级映射及延迟加载
模块名:mybatis-009-advanced-mapping
打包方式:jar
依赖:mybatis依赖、mysql驱动依赖、junit依赖、logback依赖
配置文件:mybatis-config.xml、logback.xml、jdbc.properties
拷贝工具类:SqlSessionUtil
准备数据库表:一个班级对应多个学生。班级表:t_clazz。学生表:t_student
创建pojo:Student、Clazz
1 | package com.powernode.mybatis.pojo; |
1 | package com.powernode.mybatis.pojo; |
创建mapper接口:StudentMapper、ClazzMapper
创建mapper映射文件:StudentMapper.xml、ClazzMapper.xml
13.1 多对一
多种方式,常见的包括三种:
- 第一种方式:一条SQL语句,级联属性映射。
- 第二种方式:一条SQL语句,association。
- 第三种方式:两条SQL语句,分步查询。(这种方式常用:优点一是可复用。优点二是支持懒加载(延迟加载)。)
第一种方式:级联属性映射
多对一的话多是主表,也就是student
pojo类Student中添加一个属性:Clazz clazz; 表示学生关联的班级对象,然后生成set和get、toString方法
1 | package com.powernode.mybatis.pojo; |
1 | <mapper namespace="com.powernode.mybatis.mapper.StudentMapper"> |
1 |
|
执行结果:
第二种方式:association
其他位置都不需要修改,只需要修改resultMap中的配置:association即可。
1 | <resultMap id="studentResultMap" type="Student"> |
association翻译为:关联。
学生对象关联一个班级对象。
第三种方式:分步查询
其他位置不需要修改,只需要修改以及添加以下三处:
第一处:association中select位置填写sqlId。sqlId=namespace+id。其中column属性作为这条子sql语句的条件。
1 | <!--两条SQL语句,完成多对一的分步查询。--> |
第二处:studentmapper对应的student的查询语句,clazz差的是另外一张表,所以需要其他的接口,换成clazzmapper,在ClazzMapper接口中添加方法
1 | /** |
第三处:在ClazzMapper.xml文件中进行配置
1 | <mapper namespace="com.powernode.mybatis.mapper.ClazzMapper"> |
执行结果,可以很明显看到先后有两条sql语句执行:
分步优点:
- 第一个优点:代码复用性增强。
- 第二个优点:支持延迟加载\懒加载。【暂时访问不到的数据可以先不查询。提高程序的执行效率。】
13.2 多对一延迟加载
- 什么是延迟加载(懒加载),有什么用?
- 延迟加载的核心原理是:用的时候再执行查询语句。不用的时候不查询
- 作用:提高性能。尽可能的不查,或者说尽可能的少查。来提高效率。
- 在mybatis当中怎么开启延迟加载呢?
- association标签中添加fetchType=”lazy”
- 注意:默认情况下是没有开启延迟加载的。需要设置:fetchType=”lazy”
- 这种在association标签中配置fetchType=”lazy”,是局部的设置,只对当前的association关联的sqL语句起作用。
修改StudentMapper.xml文件:
1 | <resultMap id="studentResultMap" type="Student"> |
我们现在只查询学生名字,修改测试程序:
1 | public class StudentMapperTest { |
如果后续需要使用到学生所在班级的名称,这个时候才会执行关联的sql语句,修改测试程序:
1 | public class StudentMapperTest { |
通过以上的执行结果可以看到,只有当使用到班级名称之后,才会执行关联的sql语句,这就是延迟加载。
在mybatis中如何开启全局的延迟加载呢?需要setting配置,如下:mybatis-config
1 | <settings> |
**然后把fetchType=”lazy”去掉。**这样凡是分步的都是延迟加载
执行以下程序:
1 | public class StudentMapperTest { |
通过以上的测试可以看出,我们已经开启了全局延迟加载策略。
开启全局延迟加载之后,所有的sql都会支持延迟加载,如果某个sql你不希望它支持延迟加载怎么办呢?将fetchType设置为eager:
1 | <resultMap id="studentResultMap" type="Student"> |
这样的话,针对某个特定的sql,你就关闭了延迟加载机制。
后期我们要不要开启延迟加载机制,主要看实际的业务需求是怎样的,一般是开启。
13.3 一对多
一对多的实现,通常是在一的一方中有List集合属性。比如通过一个班级查到所有的学生
在Clazz类中添加List
1 | public class Clazz { |
一对多的实现通常包括两种实现方式:
- 第一种方式:collection
- 第二种方式:分步查询
第一种方式:collection
1 | public interface ClazzMapper { |
1 | <resultMap id="clazzResultMap" type="Clazz"> |
注意是ofType,表示“集合中的类型”。
1 | public class ClazzMapperTest { |
执行结果:
第二种方式:分步查询
修改以下三个位置即可:
1 | <resultMap id="clazzResultMap" type="Clazz"> |
1 | /** |
1 | <select id="selectByCid" resultType="Student"> |
执行结果:
我们这里也是开启了延迟加载,访问整个clazz时是两个查询语句都去查询,其他的需要什么就用什么
1 |
|
13.4 一对多延迟加载
一对多延迟加载机制和多对一是一样的。同样是通过两种方式:
- 第一种:fetchType=”lazy”
- 第二种:修改全局的配置setting,**lazyLoadingEnabled=true,**如果开启全局延迟加载,想让某个sql不使用延迟加载:fetchType=”eager”
多对多应该怎么弄?分解成两个一对多
十四、MyBatis的缓存
缓存:cache
缓存的作用:通过减少IO的方式,来提高程序的执行效率。
mybatis的缓存:将select语句的查询结果放到缓存(内存)当中,下一次还是这条select语句的话,直接从缓存中取,不再查数据库。一方面是减少了IO。另一方面不再执行繁琐的查找算法。效率大大提升。
mybatis缓存包括:
- 一级缓存:将查询到的数据存储到SqlSession(会话)中。
- 二级缓存:将查询到的数据存储到SqlSessionFactory(环境)中。
- 或者集成其它第三方的缓存:比如EhCache【Java语言开发的】、Memcache【C语言开发的】等。
缓存只针对于DQL语句,也就是说缓存机制只对应select语句。
14.1 一级缓存
一级缓存默认是开启的。不需要做任何配置。
原理:只要使用同一个SqlSession对象执行同一条SQL语句,就会走缓存。
模块名:mybatis-010-cache
1 | public interface CarMapper { |
1 | <mapper namespace="com.powernode.mybatis.mapper.CarMapper"> |
1 | public class CarMapperTest { |
什么情况下不走缓存?
- 第一种:不同的SqlSession对象。
- 第二种:查询条件不一样,不走缓存。
一级缓存失效情况包括两种:
- 第一种:第一次查询和第二次查询之间,手动清空了一级缓存。
1 | sqlSession.clearCache(); |
- 第二种:第一次查询和第二次查询之间,执行了增删改操作。【这个增删改和哪张表没有关系,只要有insert delete update操作,一级缓存就失效。】
1 | /** |
1 | <insert id="insertAccount"> |
执行结果:
14.2 二级缓存
二级缓存的范围是SqlSessionFactory。
使用二级缓存需要具备以下几个条件:
- <setting name=”cacheEnabled” value=”true”> 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是true,无需设置。
- 在需要使用二级缓存的SqlMapper.xml文件中添加配置:
- 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
- SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
测试二级缓存:
1 | <cache/>加上后会在输出的信息里多一个缓存命中率的东西 |
1 | public class Car implements Serializable { |
1 |
|
二级缓存的失效:只要两次查询之间出现了增删改操作。二级缓存就会失效。【一级缓存也会失效】
二级缓存的相关配置:
- eviction:指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
- LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象,不是不常使用只是最近很少使用。(其实还有一种淘汰算法LFU,淘汰最不常用的对象。)
- FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰,也就是最老的被淘汰。
- SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- flushInterval:
- 二级缓存的刷新时间间隔,刷新就会使缓存失效。单位毫秒。如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改。
- readOnly:
- true:多条相同的sql语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题。
- false:多条相同的sql语句执行之后返回的对象是副本,调用了clone方法。性能一般。但安全。
- size:
- 设置二级缓存中最多可存储的java对象数量。默认值1024。
14.3 MyBatis集成EhCache
集成EhCache是为了代替mybatis自带的二级缓存。一级缓存是无法替代的。
mybatis对外提供了接口,也可以集成第三方的缓存组件。比如EhCache、Memcache等。都可以。
EhCache是Java写的。Memcache是C语言写的。所以mybatis集成EhCache较为常见,按照以下步骤操作,就可以完成集成:
了解一下就好
第一步:引入mybatis整合ehcache的依赖。
1 | <!--mybatis集成ehcache的组件--> |
第二步:在类的根路径下新建echcache.xml文件,并提供以下配置信息。
1 |
|
第三步:修改SqlMapper.xml文件中的
1 | <!--集成Ehcache组件--> |
第四步:编写测试程序使用。
1 |
|
十五、MyBatis的逆向工程
所谓的逆向工程是:根据数据库表逆向生成Java的pojo类,SqlMapper.xml文件,以及Mapper接口类等。
要完成这个工作,需要借助别人写好的逆向工程插件。
思考:使用这个插件的话,需要给这个插件配置哪些信息?
- pojo类名、包名以及生成位置。
- SqlMapper.xml文件名以及生成位置。
- Mapper接口名以及生成位置。
- 连接数据库的信息。
- 指定哪些表参与逆向工程。
- ……
15.1 逆向工程配置与生成
第一步:基础环境准备
新建模块:mybatis-011-generator
打包方式:jar
第二步:在pom中添加逆向工程插件
1 | <!--定制构建过程--> |
第三步:配置generatorConfig.xml
该文件名必须叫做:generatorConfig.xml
该文件必须放在类的根路径下。
1 |
|
第四步:运行插件
15.2 测试逆向工程生成的是否好用
第一步:环境准备
- 依赖:mybatis依赖、mysql驱动依赖、junit依赖、logback依赖
- jdbc.properties
- mybatis-config.xml
- logback.xml
第二步:编写测试程序
1 | public class GeneratorTest { |
十六、MyBatis使用PageHelper
16.1 limit分页
mysql的limit后面两个数字:
- 第一个数字:startIndex(起始下标。下标从0开始。)
- 第二个数字:pageSize(每页显示的记录条数)
假设已知页码pageNum,还有每页显示的记录条数pageSize,第一个数字可以动态的获取吗?
- startIndex = (pageNum - 1) * pageSize
所以,标准通用的mysql分页SQL:
1 | select |
使用mybatis应该怎么做?
模块名:mybatis-012-page
1 | public interface CarMapper { |
1 | <mapper namespace="com.powernode.mybatis.mapper.CarMapper"> |
1 | public class CarMapperTest |
执行结果:
获取数据不难,难的是获取分页相关的数据比较难。可以借助mybatis的PageHelper插件。
16.3 PageHelper插件
使用PageHelper插件进行分页,更加的便捷。
第一步:引入依赖
1 | <dependency> |
第二步:在mybatis-config.xml文件中配置插件
typeAliases标签下面进行配置:
1 | <!--mybatis分页的拦截器--> |
第三步:编写Java代码
1 | //查询所有的Car,通过分页查询插件PageHelper元成。 |
1 | <select id="selectAll" resultType="Car"> |
关键点:
- 在查询语句之前开启分页功能。
- 在查询语句之后封装PageInfo对象。(PageInfo对象将来会存储到request域当中。在页面上展示。)
1 |
|
执行结果:
1 | PageInfo{ |
十七、MyBatis的注解式开发
mybatis中也提供了注解式开发方式,采用注解可以减少Sql映射文件的配置。
当然,使用注解式开发的话,sql语句是写在java程序中的,这种方式也会给sql语句的维护带来成本。
官方是这么说的:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
使用注解编写复杂的SQL是这样的:
原则:简单sql可以注解。复杂sql使用xml。
模块名:mybatis-013-annotation
打包方式:jar
依赖:mybatis,mysql驱动,junit,logback
配置文件:jdbc.properties、mybatis-config.xml、logback.xml
pojo:com.powernode.mybatis.pojo.Car
mapper接口:com.powernode.mybatis.mapper.CarMapper
17.1 @Insert
1 | public interface CarMapper { |
1 | public class AnnotationTest { |
17.2 @Delete
1 |
|
1 |
|
17.3 @Update
1 |
|
1 |
|
17.4 @Select
1 |
|
1 |
|
执行结果:



































































































































