目錄
- MyBatis(一)入門
- 簡介
- MyBatis 入門
- Lombok
- MyBatis 基礎操作
- 數據準備
- 刪除
- 預編譯
- 新增
- 更新
- 查詢
- XML 映射文件
MyBatis(一)入門
簡介
MyBatis 是一款 優秀的持久層框架,它支持 自定義 SQL、存儲過程以及高級映射,是 Java 開發中連接數據庫的常用工具之一,屬于 ORM(對象關系映射)框架 的一種實現形式。
MyBatis 最初是由 Apache 團隊開發的 iBatis 項目,后來由 Google Code 遷移到 GitHub 并更名為 MyBatis。它是一個半自動化的 ORM 框架,開發者自己寫 SQL,MyBatis 負責將 SQL 的執行結果與 Java 對象進行自動映射。
MyBatis 的核心特點:
- SQL 編寫自由:開發者可以完全控制 SQL,實現靈活的數據庫操作
- 簡單易用:學習成本低、配置清晰
- 支持映射關系:支持一對一、一對多等對象映射
- 動態 SQL:支持 if、choose、where 等標簽,動態拼接 SQL
- 與 Spring 整合:配合 Spring Boot 使用非常方便
- 緩存支持:內置一級緩存,支持二級緩存插件擴展
MyBatis 工作原理:
- Java 調用 Mapper 接口
- MyBatis 根據配置 XML/注解
- 執行 SQL
- 映射結果
- 返回 Java 對象
MyBatis 入門
步驟:
- 準備工作(創建工程、數據庫表、實體類)
- 引入 MyBatis 相關依賴,配置 MyBatis
- 編寫 SQL 語句(注解/XML)
創建工程除了添加 Spring Web 依賴,還要添加 MyBatis Framework 和 MySQL Driver 依賴:
連接數據源,選擇 MySQL:
填寫用戶名、密碼和要連接的數據庫名,點擊測試連接,成功即可應用:
在配置文件 application.properties 中配置數據庫信息:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=123456
創建數據庫及表:
create database if not exists `mybatis`;
use `mybatis`;
create table if not exists user (id int primary key auto_increment,name varchar(20),age tinyint,gender tinyint comment '1-male, 2-female',phone varchar(20)
)comment '用戶表';
insert into user (name, age, gender, phone) values('趙剛', 18, 1, '12345678901'),('王芳', 19, 2, '12345678902'),('林偉', 20, 1, '12345678903'),('馬麗', 21, 2, '12345678904'),('孫浩', 22, 1, '12345678905');
對應的實體類:
public class User {private Integer id;private String name;private Short age;private Short sex;private String phone;public User() {}public User(Integer id, String name, Short age, Short sex, String phone) {this.id = id;this.name = name;this.age = age;this.sex = sex;this.phone = phone;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Short getAge() {return age;}public void setAge(Short age) {this.age = age;}public Short getSex() {return sex;}public void setSex(Short sex) {this.sex = sex;}public String getPhone() {return phone;}public void setPhone(String phone) {this.phone = phone;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +", age=" + age +", sex=" + sex +", phone='" + phone + '\'' +'}';}
}
創建 mapper 接口(原來的 dao 層):
import com.example.demo.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;@Mapper //在運行時會自動生成該接口的實現類對象(代理對象),并且將該對象交給Spring的IOC容器管理
public interface UserMapper {@Select("select * from user")public List<User> list();
}
在測試類中編寫測試代碼并運行:
import com.example.demo.mapper.UserMapper;
import com.example.demo.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;@SpringBootTest
class Demo2ApplicationTests {@Autowiredprivate UserMapper userMapper;@Testpublic void testListUser() {List<User> userList = userMapper.list();for (User user : userList) {System.out.println(user);}}
}
控制臺顯示:
Lombok
Lombok 是一個實用的 Java 類庫,能通過注解的形式自動生成構造器、getter/setter、equals、hashcode、toString 等方法,并可以自動化生成日志變量,簡化 Java 開發、提高效率。
注解 | 作用 |
---|---|
@Getter/@Setter | 為所有的屬性提供 get/set 方法 |
@ToString | 會給類自動生成易閱讀的 toString 方法 |
@EqualsAndHashCode | 根據類所擁有的非靜態字段自動重寫 equals 方法和 hashCode 方法 |
@Data | 提供了更綜合的生成代碼功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) |
@NoArgsConstructor | 為實體類生成無參的構造器方法 |
@AllArgsConstructor | 為實體類生成除了 static 修飾的字段之外帶有各參數的構造器方法。 |
Lombok 依賴:
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
在一些 IDEA 老的版本中沒有集成 Lombok 插件,需要自行前往插件市場安裝應用。
MyBatis 基礎操作
數據準備
創建數據庫表:
-- 部門管理
create table dept(id int unsigned primary key auto_increment comment '主鍵ID',name varchar(10) not null unique comment '部門名稱',create_time datetime not null comment '創建時間',update_time datetime not null comment '修改時間'
) comment '部門表';insert into dept (id, name, create_time, update_time) values(1,'學工部',now(),now()),(2,'教研部',now(),now()),(3,'咨詢部',now(),now()),(4,'就業部',now(),now()),(5,'人事部',now(),now());-- 員工管理
create table emp (id int unsigned primary key auto_increment comment 'ID',username varchar(20) not null unique comment '用戶名',password varchar(32) default '123456' comment '密碼',name varchar(10) not null comment '姓名',gender tinyint unsigned not null comment '性別, 說明: 1 男, 2 女',job tinyint unsigned comment '職位, 說明: 1 班主任,2 講師, 3 學工主管, 4 教研主管, 5 咨詢師',entrydate date comment '入職時間',dept_id int unsigned comment '部門ID',create_time datetime not null comment '創建時間',update_time datetime not null comment '修改時間'
) comment '員工表';INSERT INTO emp(id, username, password, name, gender, job, entrydate,dept_id, create_time, update_time) VALUES(1,'zhangwei','123456','張偉',1,4,'2000-01-01',2,now(),now()),(2,'liqiang','123456','李強',1,2,'2015-01-01',2,now(),now()),(3,'wangjun','123456','王軍',1,2,'2008-05-01',2,now(),now()),(4,'liuyang','123456','劉洋',1,2,'2007-01-01',2,now(),now()),(5,'chenming','123456','陳明',1,2,'2012-12-05',2,now(),now()),(6,'humin','123456','胡敏',2,3,'2013-09-05',1,now(),now()),(7,'zhuyan','123456','朱妍',2,1,'2005-08-01',1,now(),now()),(8,'guoyan','123456','郭燕',2,1,'2014-11-09',1,now(),now()),(9,'linling','123456','林玲',2,1,'2011-03-11',1,now(),now()),(10,'heqian','123456','何倩',2,1,'2013-09-05',1,now(),now()),(11,'gaoxiang','123456','高翔',1,5,'2007-02-01',3,now(),now()),(12,'liangchao','123456','梁超',1,5,'2008-08-18',3,now(),now()),(13,'luoyi','123456','羅毅',1,5,'2012-11-01',3,now(),now()),(14,'mahui','123456','馬輝',1,2,'2002-08-01',2,now(),now()),(15,'huangyong','123456','黃勇',1,2,'2011-05-01',2,now(),now()),(16,'wupeng','123456','吳鵬',1,2,'2010-01-01',2,now(),now()),(17,'zhenlei','123456','鄭磊',1,NULL,'2015-03-21',NULL,now(),now());
創建實體類:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDate;
import java.time.LocalDateTime;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {private Integer id;private String username;private String password;private String name;private Short gender;private Short job;private LocalDate entrydate;private Integer deptid;private LocalDateTime createTime;private LocalDateTime updateTime;
}
mapper 接口:
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface EmpMapper {}
后面的操作都是按照以上數據進行
刪除
在 mapper 接口中編寫刪除操作的代碼:
//根據ID刪除數據
@Delete("delete from emp where id=#{id}") // #{} 是 MyBatis 中動態獲取數據的占位符
public int deleteById(Integer id);
在測試類中編寫測試方法的代碼:
@Autowired
private EmpMapper empMapper;
@Test
public void testDelete(){empMapper.deleteById(17);
}
一般這樣寫是沒有返回值,如果需要看是否刪除了數據,可以寫成以下形式;
@Test
public void testDelete(){int deleteNum =empMapper.deleteById(17);System.out.println("刪除了"+deleteNum+"行數據");
}
運行結果如下:
預編譯
雖然前面的操作成功執行了,但是我們無法知道底層到底是怎么進行的,這個時候可以通過配置 MyBatis 日志來了解
在配置文件 application.properties 中加入以下配置即可開啟 MyBatis 日志,并輸出到控制臺中:
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
運行測試方法就可以在控制臺得到以下信息:
這樣我們就能了解到底層是怎么進行的了
==> Preparing: delete from emp where id=?
==> Parameters: 17(Integer)
這是 SQL 中的預編譯(Prepared Statement)是數據庫編程中一種常見的優化和防注入方式。它將 SQL 語句的結構與數據參數分開處理,大大提高了執行效率并增強了安全性。
SQL 預編譯是將 SQL 語句在數據庫執行前先進行編譯,生成執行計劃,在實際執行時只需提供參數即可。預編譯過程主要包括:
- 解析 SQL 結構;
- 檢查語法和語義;
- 生成執行計劃;
- 緩存該語句(可重復使用)。
使用 #{}
的方式,MyBatis 會生成使用 ?
占位符的預編譯 SQL,數據庫只需預編譯一次并可復用執行計劃,后續的不同參數只需要替換 ?
即可;而寫死參數的 SQL,每次都是新的語句,數據庫必須重新預編譯,效率低。
預編譯的優勢:
- 性能優化:SQL 結構只編譯一次,多次執行復用執行計劃,效率更高
- 防 SQL 注入:參數綁定,不會直接拼接SQL字符串,防止惡意注入
- 代碼更簡潔:統一結構 + 參數替換,代碼更清晰
那么什么又是 SQL 注入呢?用以下場景來演示:
-
用戶登錄功能就是在數據庫表中查找是否有對應的用戶名和密碼
//根據用戶名和密碼查詢用戶 @Select("select * from emp where username='zhangwei' and password='123456'") public Emp getEmpByUsernameAndPassword();
結果如下,登錄成功:
-
而現在用戶名隨便輸入,密碼輸入“
'or'1'='1
”://根據用戶名和密碼查詢用戶 @Select("select count(*) from emp where username='asfgasgasf' and password=''or'1'='1'") public int getEmpByUsernameAndPassword();
結果也是登錄成功:
這種情況就稱為 SQL 注入,出現這種情況的原因是:
- 在 mapper 接口中寫的 SQL 語句
@Select("select count(*) from emp where username='asfgasgasf' and password=''or'1'='1'")
在數據庫中解析為SELECT count(*) FROM emp WHERE username='asfgasgasf' AND password='' OR '1'='1'
,因為'1'='1'
永遠為真,整個 WHERE 條件恒為真,導致查詢返回整張表的總行數。因此,如果后臺通過count > 0
判斷用戶是否存在,就會錯誤地認為登錄驗證通過,從而實現 SQL 注入攻擊。
而用了預處理的代碼如下:
//根據用戶名和密碼查詢用戶
@Select("select count(*) from emp where username=#{username} and password=#{password}")
public int getEmpByUsernameAndPassword(String username,String password);@Test
public void testGetEmpByUsernameAndPassword(){int count = empMapper.getEmpByUsernameAndPassword("zhangsan","'or'1'='1");System.out.println(count);
}
運行測試結果如下:
顯而易見,'or'1'='1
是以一個整體來替換 ?
,不會當作 SQL 語法解析,也就無法注入了。
參數占位符;
語法格式 | 特點及說明 | 使用時機 |
---|---|---|
#{...} | 執行 SQL 時,會將#{...} 替換為? ,生成預編譯 SQL,會自動設置參數值,可有效防止 SQL 注入 | 參數傳遞場景,一般參數傳遞都使用#{...} |
${...} | 拼接 SQL,直接將參數拼接到 SQL 語句中,存在 SQL 注入問題 | 對表名、列名進行動態設置等場景(需謹慎,做好校驗避免注入風險 ) |
新增
mapper 接口的代碼:
//新增員工
@Insert("insert into emp(username,password,name,gender,job,entrydate,dept_id,create_time,update_time) " +"values(#{username},#{password},#{name},#{gender},#{job},#{entryDate},#{deptId},#{createTime},#{updateTime})")
public void insert(Emp emp);
測試類中的代碼:
@Test
public void testInsert(){Emp emp = new Emp();emp.setUsername("張三");emp.setPassword("123456");emp.setName("張三");emp.setGender((short) 1);emp.setJob((short) 1);emp.setEntryDate(LocalDate.now());emp.setDeptId(1);emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());empMapper.insert(emp);
}
運行結果如下:
主鍵返回:在數據添加成功后,需要獲取插入數據庫數據的主鍵。如:添加套餐數據時,還需要維護套餐菜品關系表數據
只需要加上 @Options 注解即可:
//新增員工
@Options(useGeneratedKeys = true,keyProperty = "id")
@Insert("insert into emp(username,password,name,gender,job,entrydate,dept_id,create_time,update_time) " +"values(#{username},#{password},#{name},#{gender},#{job},#{entryDate},#{deptId},#{createTime},#{updateTime})")
public void insert(Emp emp);
運行結果如下:
更新
mapper 接口的代碼:
//更新員工
@Update("update emp set username=#{username},password=#{password},name=#{name},gender=#{gender},job=#{job}, entrydate=#{entryDate},dept_id=#{deptId},update_time=#{updateTime} where id=#{id}")
public void update(Emp emp);
測試類中的代碼:
@Test
public void testUpdate(){Emp emp = new Emp();emp.setId(19);emp.setUsername("lisi(update)");emp.setPassword("123456");emp.setName("李四(update)");emp.setGender((short) 1);emp.setJob((short) 2);emp.setEntryDate(LocalDate.now());emp.setDeptId(1);emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());empMapper.update(emp);
}
運行結果如下:
查詢
mapper 接口的代碼:
//根據ID查詢員工
@Select("select * from emp where id=#{id}")
public Emp getById(Integer id);
測試類中的代碼:
@Test
public void testGetById(){Emp emp = empMapper.getById(1);System.out.println(emp);
}
運行結果如下:
從結果中會發現,最后三個字段在數據庫表中明明是有數據,卻沒有被獲取到,這是因為 MyBatis 數據封裝的原因。
數據封裝:
- 實體類屬性名和數據庫表查詢返回的字段名一致,MyBatis 會自動封裝。
- 如果實體類屬性名和數據庫表查詢返回的字段名不一致,不能自動封裝。
解決方法:
-
給字段起別名:
//根據ID查詢員工 @Select("select id, username, password, name, gender, job, entrydate, dept_id deptId, create_time createTime, update_time updateTime from emp where id=#{id}") public Emp getById(Integer id);
-
通過 @Results,@Result 注解手動映射封裝
//根據ID查詢員工 @Results({@Result(column = "dept_id", property = "deptId"),@Result(column = "create_time", property = "createTime"),@Result(column = "update_time", property = "updateTime") }) @Select("select * from emp where id=#{id}") public Emp getById(Integer id);
-
開啟 MyBatis 的駝峰命名自動映射開關(推薦,但是類中的屬性名必須要是駝峰命名,數據庫表字段名必須要是
_
命名)//application.properties mybatis.configuration.map-underscore-to-camel-case=true
運行結果如下:
以上根據 ID 查詢較為簡單,而下面的條件查詢則較為復雜。
mapper 接口的代碼:
//條件查詢員工
@Select("select * from emp where name like '%${name}%' and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
測試類中的代碼:
@Test
public void testList(){List<Emp> list = empMapper.list("張", (short) 1, LocalDate.of(2000, 1, 1), LocalDate.now());System.out.println(list);
}
運行結果如下:
但是接口代碼中使用的是 ${}
,存在 SQL 注入的問題,可以使用 SQL 中的 concat
函數來進行拼接:
//條件查詢員工
@Select("select * from emp where name like concat('%',#{name},'%') and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
這樣就能使用 #{}
來解決 SQL 注入的問題。
XML 映射文件
XML 映射文件規范:
- XML 映射文件的名稱與 mapper 接口名稱一致,并且將 XML 映射文件和 mapper 接口放置在相同包下(同包同名)
- XML 映射文件的 namespace 屬性為 mapper 接口全限定名一致
- XML 映射文件中 SQL 語句的 id 與 mapper 接口中的方法名一致,并保持返回類型一致
在 resources 包下創建 mapper 接口的同名包:
在新創建的包下創建 mapper 接口的同名 XML 文件 EmpMapper.xml,并添加以下約束:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
mapper 接口中的方法為:
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
EmpMapper.xml 中寫 SQL 語句:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.EmpMapper"><select id="list" resultType="com.example.demo.pojo.Emp"><!--resultType 是單條記錄封裝的類型,要用對應實體類的全類名-->select * from emp where name like concat('%',#{name},'%') and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc</select>
</mapper>
EmpMapper.xml 中的各項屬性要與 mapper 接口中的一致:
如果有不一致的,則 XML 映射文件無法匹配上對應的 mapper 接口方法。