關於MyBatis的那些事

1. MyBatis框架的作用

主要作用:簡化持久層開發。

持久層:解決項目中的數據持久化處理的相關組件。

使用MyBatis框架實現數據庫編程時,只需要指定各個功能對應的抽象方法及需要執行的SQL語句即可。

2. 創建MyBatis項目

MyBatis項目可以是本機直接運行的,不一定需要與SpringMVC框架結合起來一起使用,所以,在創建項目時,只需要創建爲jar項目即可。

當項目創建出來之後,需要添加mybatis依賴:

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.4</version>
</dependency>

然後,還需要添加MyBatis整合Spring框架的mybatis-spring依賴:

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.4</version>
</dependency>

由於需要整合Spring框架,所以,還需要添加Spring框架的spring-context依賴:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

其底層實現是基於JDBC的,所以,還需要添加spring-jdbc依賴:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

本次將使用MySQL數據庫,所以,還需要添加mysql-connector-java依賴:

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

在連接數據庫時,應該使用數據庫連接池,所以,還應該添加commons-dbcp依賴:

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.7.0</version>
</dependency>

在開發完某個功能後,應該及時檢查開發的功能是否可以正常運行,所以,還添加junit單元測試依賴:

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
    <scope>test</scope>
</dependency>

3. 案例準備工作

先登錄MySQL控制檯,創建名爲tedu_ums的數據庫:

CREATE DATABASE tedu_ums;

並使用這個數據庫:

USE tedu_ums;

在這個數據庫中,創建一張用戶數據表t_user,表中應該包含以下字段:id、用戶名(username)、密碼(password)、年齡(age)、手機號碼(phone)、電子郵箱(email):

CREATE TABLE t_user (
	id int AUTO_INCREMENT,
    username varchar(20) NOT NULL UNIQUE,
    password varchar(20) NOT NULL,
    age int,
    phone varchar(20),
    email varchar(50),
    PRIMARY KEY (id)
) DEFAULT CHARSET=utf8;

4. 測試項目是否可以正常運行

src/test/java下,創建cn.tedu.spring包,並在這個包中創建ProjectTests測試類,在測試類添加空白的測試方法,以測試JUnit環境是否正常:

package cn.tedu.spring;

import org.junit.Test;

public class ProjectTests {
	
	@Test
	public void contextLoads() {
		System.out.println("ProjectTests.contextLoads()");
	}

}

凡是在src/test下的文件,都不會參與項目最終打包、部署,所以,一般在編寫單元測試時,對代碼規範要求並不那麼嚴格,但是,仍應該儘量遵循開發規範。

關於測試方法,必須:

  • 必須添加@Test註解;
  • 必須使用public權限(從JUnit 5開始不嚴格要求測試方法的訪問權限);
  • 必須使用void表示返回值類型;
  • 必須保持參數列表爲空。

5. 連接數據庫

src/main/resources下創建jdbc.properties文件,並在其中配置連接數據庫的相關信息:

mysql.url=jdbc:mysql://localhost:3306/tedu_ums?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
mysql.driver=com.mysql.cj.jdbc.Driver
mysql.username=root
mysql.password=root
mysql.initialSize=2
mysql.maxTotal=10

接下來,需要在程序中讀取以上配置信息,則在src/main/java下創建cn.tedu.spring包,並在這個包中創建SpringConfig類,在該類中讀取以上配置,並基於這些配置信息創建javax.sql.DataSource的對象,將該對象交給Spring框架進行管理:
這裏有個坑,讀取properties文件的時候很可能出現報錯讀不到。 應該在文件名前面加上classpath: 就好了。建議以後可以經常試試。
在這裏插入圖片描述

@PropertySource("jdbc.properties")
@Configuration
public class SpringConfig {
	
	@Value("${mysql.url}")
	private String url;
	@Value("${mysql.driver}")
	private String driver;
	@Value("${mysql.username}")
	private String username;
	@Value("${mysql.password}")
	private String password;
	@Value("${mysql.initialSize}")
	private Integer initialSize;
	@Value("${mysql.maxTotal}")
	private Integer maxTotal;
	
	@Bean
	public DataSource dataSource() {
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setUrl(url);
		dataSource.setDriverClassName(driver);
		dataSource.setUsername(username);
		dataSource.setPassword(password);
		dataSource.setInitialSize(initialSize);
		dataSource.setMaxTotal(maxTotal);
		return dataSource;
	}

}

完成後,在ProjectTests測試類中添加新的測試方法,在測試方法中,讀取Spring的配置文件,並從Spring容器中獲取DataSource對象,並調用該對象的getConnection()方法以獲取Connection對象,如果能夠成功獲取對象,則表示配置信息無誤,後續MyBatis框架也可以正常連接數據庫:

@Test
public void getConnection() throws SQLException {
    AnnotationConfigApplicationContext ac
        = new AnnotationConfigApplicationContext(SpringConfig.class);

    DataSource dataSource = ac.getBean("dataSource", DataSource.class);

    Connection conn = dataSource.getConnection();
    System.out.println(conn);

    ac.close();
}

6. 接口與抽象方法

暫定需要實現的功能是:向t_user表中插入用戶數據。

在使用MyBatis時,各功能的抽象方法必須寫在接口文件中,推薦使用Mapper作爲接口名稱的後半部分,關於抽象方法的聲明:

  • 返回值類型:當需要執行的SQL語句是INSERTDELETEUPDATE類型之一時,將返回值設計爲Integer,在執行時,會返回“受影響的行數”,當然,也可以聲明爲void,表示“不關心受影響的行數”,推薦使用Integer
  • 方法名稱:自定義;
  • 參數列表:根據需要執行的SQL語句中的問號?來決定,當方法參數較多時,也可以將多個參數進行封裝,然後,使用封裝的類型作爲抽象方法的參數。

插入用戶數據時,需要執行的SQL語句大致是:

insert into t_user (username, password, age, phone, email) values (?,?,?,?,?)

所以,應該在src/main/java下的cn.tedu.spring包中創建User類,用於封裝各屬性:

public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String phone;
    private String email;
    
    // Getters & Setters
    // toString()
}

然後,在cn.tedu.spring包下創建UserMapper接口,並在接口中添加抽象方法:

public interface UserMapper {
    Integer aaa(User user);
}

在後續執行時,還需要使得MyBatis知道接口文件在哪裏,則需要在配置類(初始化Spring環境時被加載的類,有@Configuration註解的類)之前添加@MapperScan註解,以配置接口文件所在的包,所以,在SpringConfig類的聲明之前補充添加@MapperScan註解並配置接口所在的包:

@PropertySource("jdbc.properties")
@Configuration
@MapperScan("cn.tedu.spring")
public class SpringConfig {
    // ...
}

7. 配置SQL語句

在抽象方法的聲明之前,根據要執行的SQL語句的種類,使用@Insert@Delete@Update@Select註解中的某1個來配置SQL語句,由於本次需要執行的是INSERT類型的SQL語句,則需要使用@Insert註解,並在註解參數的字符串中編寫SQL語句。

在編寫SQL語句時,對於存在?佔位的位置,應該寫爲#{}格式的佔位符,佔位符的括號中寫上屬性的名稱!

例如:

public interface UserMapper {
	
	@Insert("INSERT INTO t_user (username, password, age, phone, email) VALUES (#{username}, #{password}, #{age}, #{phone}, #{email})")
	Integer aaa(User user);

}

最後,在執行之前,還得使得MyBatis明確執行時使用哪個數據源可以連接數據庫,需要在Spring環境中配置一個SqlSessionFactoryBean的對象,則在SpringConfig類中添加:

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    return bean;
}

全部完成後,在ProjectTests中添加測試方法:

@Test
public void aaa() {
    AnnotationConfigApplicationContext ac
        = new AnnotationConfigApplicationContext(SpringConfig.class);

    UserMapper userMapper = ac.getBean("userMapper", UserMapper.class);

    User user = new User();
    user.setUsername("mybatis");
    user.setPassword("1234");
    user.setAge(25);
    user.setPhone("13800138001");
    user.setEmail("[email protected]");
    Integer rows = userMapper.aaa(user);
    System.out.println("rows=" + rows);

    ac.close();
}

8. 使用XML文件配置各抽象方法對應的SQL語句

使用@Insert或相關注解配置SQL語句時,SQL語句與抽象方法的對應關係非常直觀,但是,卻不便於閱讀、管理各SQL語句!因爲在源代碼中,SQL語句的表現就是一個字符串,在實際開發過程中,經常會使用到一些較長的SQL語句,如果使用1個字符串表示較長的SQL語句,在源代碼就存在必須換行顯示,又存在字符串拼接的問題!所以,非常不推薦使用@Insert或相關注解來配置SQL語句!

在項目的src/main/resources中創建mappers文件夾。

http://doc.canglaoshi.org/config/Mapper.xml.zip下載文件,將下載得到的文件解壓,得到SomeMapper.xml文件,這個文件就是專門用於配置SQL語句的文件!

SomeMapper.xml複製到項目的mappers文件夾中!

在這個專門用於配置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">

這幾行代碼是不允許修改的,也是必須存在的!

在這個XML文件中,根節點必須是<mapper>,在根節點中,必須配置namespace屬性,該屬性值是這個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="cn.tedu.spring.UserMapper">
</mapper>

然後,在<mapper>節點的子級,根據需要執行的SQL語句的種類,在<insert><delete><update><select>這4個節點類型中選取所需要使用的節點,這些節點都需要配置id屬性,取值就是對應的抽象方法的名稱,然後,將SQL語句配置在節點的子級,例如:

<?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="cn.tedu.spring.UserMapper">

	<insert id="aaa">
		INSERT INTO t_user (
			username, password, age, phone, email
		) VALUES (
			#{username}, #{password}, #{age}, #{phone}, #{email}
		)
	</insert>

</mapper>

【概念更新】在接口中設計抽象方法時,抽象方法的名稱可以自定義,但是,不允許使用重載!

由於目前MyBatis框架還不知道這個XML文件的存在,所以,需要通過配置使框架能夠使用該XML文件!應該先在jdbc.properties中添加配置:

這一步容易搞忘,這也是用xml文件的一個缺點,需要更新配置。

mybatis.mapper-locations=classpath:mappers/*.xml

其實,此時,這個配置文件還叫jdbc.properties就已經有點不太合適了,因爲其中還有其它的配置,但是,這個演示案例就暫時不修改文件名了。

然後,在SpringConfig中讀取以上新增的配置:

@Value("${mybatis.mapper-locations}")
private Resource[] mapperLocations;

最後,在配置Spring框架獲取SqlSessionFactoryBean的對象時,爲SqlSessionFactoryBean配置以上讀取到的屬性:

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setMapperLocations(mapperLocations);
    return bean;
}

9. 使用MyBatis實現刪除與修改

預先準備測試數據:

insert into t_user (username, password, age, phone, email) values 
('root', '1234', 21, '13800138001', '[email protected]'),
('spring', '4352', 28, '13800138009', '[email protected]'),
('random', '5635', 27, '13800138002', '[email protected]'),
('mybatis', 'adef', 24, '13800138003', '[email protected]'),
('date', 'recv', 25, '13800138004', '[email protected]'),
('calendar', '8454', 30, '13800138011', '[email protected]'),
('string', 'xgvr', 23, '13800138005', '[email protected]'),
('integer', 'df34', 25, '13800138006', '[email protected]'),
('servlet', 'hjfd', 26, '13800138007', '[email protected]'),
('filter', '6xfd', 29, '13800138008', '[email protected]'),
('interceptor', '76fg', 22, '13800138010', '[email protected]');

只要是使用MyBatis框架開發持久層功能,需要開發的內容始終是:編寫抽象方法,配置SQL語句。

假設需要實現:根據id刪除用戶數據。

需要執行的SQL語句大致是:

delete from t_user where id=?

則在UserMapper接口中添加抽象方法:

Integer deleteById(Integer id);

然後,在SomeMapper<mapper>節點的子級補充配置:

<delete id="deleteById">
    DELETE FROM t_user WHERE id=#{id}
</delete>

完成後,在src/test/javacn.tedu.spring包下創建新的測試類UserMapperTests,編寫並執行單元測試:

public class UserMapperTests {

	@Test
	public void deleteById() {
		Integer id = 2;
		Integer rows = userMapper.deleteById(id);
		System.out.println("rows=" + rows);
	}
	
	// =========================================
	
	public AnnotationConfigApplicationContext ac;
	public UserMapper userMapper;
	
	@Before
	public void doBefore() {
		ac = new AnnotationConfigApplicationContext(SpringConfig.class);
		userMapper = ac.getBean("userMapper", UserMapper.class);
	}
	
	@After
	public void doAfter() {
		ac.close();
	}

}

注意:如果單元測試環境是JUnit 5,需要將@Before@After換成@BeforeEach@AfterEach

假設需要實現 :將所有用戶的密碼都改爲某個值。

需要執行的SQL語句大致是:

update t_user set password=?

則在UserMapper接口中添加抽象方法:

Integer updatePassword(String password);

然後,在SomeMapper.xml中添加配置:

<update id="updatePassword">
    UPDATE t_user SET password=#{password}
</update>

完成後,在UserMapperTests中添加測試:

@Test
public void updatePassword() {
    String password = "8888";
    Integer rows = userMapper.updatePassword(password);
    System.out.println("rows=" + rows);
}

10. 使用MyBatis實現查詢

在使用MyBatis實現查詢時,在設計抽象方法時,應該使用期望的類型作爲抽象方法的返回值類型。

假設需要實現:統計當前數據表中用戶的數量。

則可以將抽象方法設計爲:

Integer count();

在配置映射時,應該使用<select>節點,該節點必須配置resultTyperesultMap中的某1個屬性(必須二選一):

<select id="count" resultType="java.lang.Integer">
    SELECT COUNT(*) FROM t_user
</select>

完成後,測試:

@Test
public void count() {
    Integer count = userMapper.count();
    System.out.println("count=" + count);
}

假設需要實現:根據id查詢某用戶的詳情。

則抽象方法設計爲:

User findById(Integer id);

配置映射:

<select id="findById" resultType="cn.tedu.spring.User">
	SELECT * FROM t_user WHERE id=#{id}
</select>

單元測試:

@Test
public void findById() {
    Integer id = 8;
    User user = userMapper.findById(id);
    System.out.println(user);
}

假設需要實現:查詢當前數據表中所有用戶的詳情。

由於本次查詢時,可能返回多個用戶的數據,必須將返回值類型聲明爲“可以表示若干個用戶信息”的數據類型,可以使用數組,或List集合,則抽象方法可以設計爲:

List<User> findAll();

然後,配置映射,本次抽象方法的返回值類型是List<User>類型,在配置resultType屬性時,不需要告訴框架“這次返回List集合”,因爲,框架能夠根據抽象方法的返回值創建出返回值對象,只需要告訴框架“集合中的元素是什麼類型的”,以使得“框架能夠將查詢結果封裝到一個個的對象中”。

所以,配置映射的代碼爲:

<select id ="findAll" resultType="cn.tedu.spring.User">
	SELECT * FROM t_user ORDER BY id LIMIT 0, 100
</select>

完成後,單元測試:

@Test
public void findAll() {
    List<User> users = userMapper.findAll();
    System.out.println("count=" + users.size());
    for (User user : users) {
        System.out.println(user);
    }
}

* 11.後續準備工作

tedu_ums數據庫中,新增數據表t_group用於存儲“用戶組”,該數據表暫定只需要2個字段:id、組名稱(name),則創建該數據表:

CREATE TABLE t_group (
	id int AUTO_INCREMENT,
    name varchar(15) NOT NULL UNIQUE,
    PRIMARY KEY (id)
) DEFAULT CHARSET=utf8;

接下來,應該在項目中開發“用戶組”數據表的管理功能,推薦創建新的類、接口來實現相關功能,首先,應該創建Group實體類,用於對應數據表:

public class Group {
    private Integer id;
    private String name;
    
    // Getters & Setters
    // toString()
}

然後,在cn.tedu.spring包(@MapperScan註解配置的包)中創建GroupMapper接口,用於添加抽象方法,並在src/main/resourcesmappers文件夾下,粘貼得到GroupMapper.xml文件,用於配置SQL語句!

並完成以下功能:

  • 插入新的用戶組數據;
  • 顯示用戶組列表;
  • 根據id刪除某個用戶組。

用戶數據與用戶組數據應該是有關聯,即:某個用戶歸屬於某個用戶組,且每個用戶組中可以有若干個用戶。爲了表現這樣的關係,應該在用戶數據表中添加新的字段,表示用戶歸屬的組的id,所以,修改t_user表,添加group_id字段,需要執行的SQL語句大致是:

ALTER TABLE t_user ADD COLUMN group_id int;

默認情況下,現有的每個用戶的group_id字段的值都會是NULL值,應該爲現有的每個用戶數據隨機的分配一些組的id,以使得測試數據是完整有效的,例如:

update t_user set group_id=1 where id IN (14,20,22,23);
update t_user set group_id=2 where id IN (21,15,16,24);
update t_user set group_id=3 where id IN (17,18,19);

由於修改了用戶數據表的結構,還應該在User類中也添加新的屬性:

private Integer groupId; // 對應t_user表中的group_id

// Getters & Setters
// 重新生成toString()

提示:當添加了新的屬性後,也無法直接通過原有的查詢獲取到對應的“組id”的值。

12. 在抽象方法中定義多個參數

假設需要實現:根據用戶的id修改用戶的電子郵箱。

需要執行的SQL語句大致是:

update t_user set email=? where id=?

抽象方法可以設計爲:

Integer updateEmailById(Integer id, String email);

映射的SQL語句配置爲:

<update id="updateEmailById">
	UPDATE t_user SET email=#{email} WHERE id=#{id}
</update>

完成後,在UserMapperTests中編寫並執行單元測試:

@Test
public void updateEmailById() {
    Integer id = 1;
    String email = "[email protected]";
    Integer rows = userMapper.updateEmailById(id, email);
    System.out.println("rows=" + rows);
}

如果直接執行以上單元測試,會出現如下錯誤:

Caused by: 
org.apache.ibatis.binding.BindingException: Parameter 'email' not found. Available parameters are [arg1, arg0, param1, param2]

在錯誤的提示信息中,可以明確的看到:可用的參數是[arg1, arg0, param1, param2]

其實,在配置SQL映射時,可以使用arg0表示抽象方法中的第1個參數,使用arg1表示抽象方法中的第2個參數,例如:

<update id="updateEmailById">
	UPDATE t_user SET email=#{arg1} WHERE id=#{arg0}
</update>

當然,也可以使用param1表示抽象方法中的第1個參數,使用param2表示抽象方法的第2個參數,例如:

<update id="updateEmailById">
	UPDATE t_user SET email=#{param2} WHERE id=#{param1}
</update>

在默認情況下,如果抽象方法的參數超過1個,在配置SQL映射時,應該使用arg系列的參數名稱,第1個參數名使用arg0,第2個使用arg1,如果還有第3個、第4個甚至更多參數,以此類推,即使用arg2arg3……當然,也可以使用param系列的參數名稱,只不過是從1開始順序編號的,例如參數名爲param1param2param3param4……

雖然使用argparam系列的名稱可以解決多參數的問題,但是,會導致SQL映射的配置代碼不直觀的問題!MyBatis推薦的解決方法是在抽象方法的每一個參數之前添加@Param註解,在註解中配置參數名,例如:

Integer updateEmailById(
    @Param("id") Integer id, 
    @Param("email") String email
);

後續,在配置SQL映射時,在#{}佔位符中,需要使用的就是@Param註解中配置的註解參數!

使用這種做法,既保證了方法的簡單調用,又保證了XML文件中配置的SQL映射是直觀的!

小結:如果抽象方法的參數列表中的參數超過了1個(達到2個或更多個),就必須爲每一個參數添加@Param註解,並且,在#{}佔位符中,需要使用的就是@Param註解中配置的註解參數!

練習:根據用戶名和密碼查詢用戶數據。

13. 動態SQL–foreach

動態SQL:根據執行時的參數不同,最終執行的SQL語句可能不同!

假設需要實現:一次性刪除若干個用戶數據。

需要執行的SQL語句大致是:

delete from t_user where id in (?,?,?,?,?);

以上SQL語句中,IN語法中的?的數量是不確定的。

由於在SQL語句中參數的數量並不確定,同時,這些參數的類型、表現的意義卻是相同的,則可以將抽象方法聲明爲:

Integer deleteByIds(List<Integer> ids);

其實,也可以使用數組來表示若干個id值,例如:

Integer deleteByIds(Integer[] ids);

甚至,還可以將參數聲明爲可變參數,例如:

Integer deleteByIds(Integer... ids);

可變參數在被處理時,本質上就是一個數組。

然後,配置SQL映射,需要使用<foreach>節點對參數進行遍歷:

<delete id="deleteByIds">
	DELETE FROM t_user WHERE id IN (
    	<foreach collection="list" item="id" separator=",">
    		#{id}
    	</foreach>
    )
</delete>

在配置<foreach>節點,關於各屬性的配置:

  • collection:被遍歷的對象,該對象可能是一個List集合,也可能是一個數組。當抽象方法的參數只有1個,且沒有添加@Param註解時,該屬性的值取決於參數的類型,當參數是List集合類型時,取值爲list,當參數是數組或可變參數時,取值爲array;如果抽象方法的參數超過1個,則參數必然添加了@Param註解,則該屬性的值就是@Param註解的參數值。

  • item:遍歷過程中,得到的集合或數組中的元素的名稱,當確定該屬性的名稱後,在<foreach>節點的子級,就可以通過#{}佔位符中填寫這個名稱來表示集合或數組中的某個值。

  • separator:生成動態SQL中的SQL語句片段時,各值之間使用什麼符號進行分隔。

  • openclose:遍歷生成的SQL語句片段的最左側字符串與最右側字符串。

完成後,即可進行測試:

@Test
public void deleteByIds() {
    List<Integer> ids = new ArrayList<Integer>();
    ids.add(9);
    ids.add(14);
    ids.add(16);
    Integer rows = userMapper.deleteByIds(ids);
    System.out.println("rows=" + rows);
}

14. 動態SQL–判斷與選擇

在動態SQL中還可以實現if判斷的效果,需要使用<if>節點來配置,其格式是:

<if test="表達式">
    滿足表達式的判斷條件時的SQL片段
</if>

但是,並沒有匹配的相當於else作用的節點,所以,如果某次判斷,無論是true還是false都有對應的配置時,可以使用2個<if>分別使用完全相反的判斷標準來進行配置。

當然,也可以使用<choose>系列的節點實現if...else的效果,其格式是:

<choose>
	<when test="條件">
        滿足表達式的判斷條件時的SQL片段
    </when>
    <otherwise>
    	不滿足表達式的判斷條件時的SQL片段
    </otherwise>
</choose>

15. 關於#{}和${}格式的佔位符

在MyBatis中,配置SQL映射時,可以使用#{}${}格式的佔位符表示某個變量。

當需要表示的是某個值時,應該使用#{}格式的佔位符,簡單的說,在學習JDBC時,自行編寫的SQL語句中可以使用問號?的位置都應該使用#{}格式的佔位符。嚴格來說,當使用#{}格式的佔位符時,MyBatis會先使用問號?對這些位置進行佔位,然後,將SQL語句發送到MySQL服務器,MySQL服務器對例如delete from t_user where id=?這類存在問號?的SQL語句進行詞法分析、語義分析、編譯等過程,當編譯通過後,再將各個值代進去執行,所以,整個過程是預編譯處理的!由於是使用預編譯處理的,所以,在使用各個值時,並不需要關心數據類型的問題,也不存在SQL注入的風險!

當需要表示的是SQL語句中的某個片段時,應該使用${}格式的佔位符,凡在SQL語句中不可以寫成問號?的部分必須使用${}格式的佔位符。當使用${}格式的佔位符時,不可能使用預編譯的做法,因爲例如select * from t_user where ?這樣的SQL語句是不正常的,甚至有些還是不合法的!MyBatis在處理時,必須先將${}佔位符的值與所配置的SQL語句進行拼接,然後再執行詞法分析、語義分析、編譯等過程,如果編譯通過,則直接執行(值在這之前就已經代進去了)。由於在編譯之前就把${}佔位符的值已經代入到SQL語句中了,所以,必須嚴格區分在完整的SQL語句中的各個值的數據類型!同時,還存在SQL注入的風險!

懶漢式小結:當需要使用佔位符表示某個參數值是,全部使用#{}的格式,如果發現該格式的無效,則改用${}格式。

小結:使用#{}格式的佔位符只能表示SQL語句中的某個值,在處理過程中是預編譯的,可以無視值的數據類型,沒有SQL注入的風險!使用${}格式的佔位符可以表示SQL語句中的任何片段,是直接與SQL語句進行拼接再編譯、執行的,必須嚴格表現值的數據類型,且存在SQL注入的風險!

16. 解決查詢時名稱不匹配導致無法封裝數據的問題【1】

在MyBatis處理查詢時,會自動將“查詢結果中的列名”與“封裝查詢結果的屬性名”進行對照,如果一致,則會將查詢結果中的值封裝到對應的屬性中!

例如在查詢結果中存在名爲username的列,值是root,同時,該查詢返回的結果是User類型的,且User類中存在名爲username的屬性,則MyBatis會將root封裝到User類對象的username屬性中!

在配置SQL語句時,可以自定義別名,使得查詢結果中的列名與屬性名完全一致,則MyBatis就可以自動完成封裝:

<select id="findAll" resultType="cn.tedu.spring.User">
    SELECT
        id, username, password, age,
        phone, email, group_id AS groupId 
    FROM t_user ORDER BY id LIMIT 0, 100
</select>

16. 解決查詢時名稱不匹配導致無法封裝數據的問題【2】

當名稱不匹配時,還可以在XML文件中配置<resultMap>節點,以指導MyBatis如何完成正確的封裝!例如:

<!-- id:自定義的名稱 -->
<!-- type:封裝查詢結果的數據類型 -->
<resultMap type="cn.tedu.spring.User" id="UserMap">
    <!-- column:查詢結果中的列名 -->
    <!-- property:封裝查詢結果的類中的屬性名 -->
    <result column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="age" property="age"/>
    <result column="phone" property="phone"/>
    <result column="email" property="email"/>
    <result column="group_id" property="groupId"/>
</resultMap>

配置以上<resultMap>使用時,<select>節點就必須配置resultMap屬性,並且,不允許再配置resultType屬性:

<select id="findById" resultMap="UserMap">
    SELECT * FROM t_user WHERE id=#{id}
</select>

在執行單表數據查詢時,在配置<resultMap>時,如果查詢結果的列名與類中的屬性名本來就是完全一致的,則可以不必配置對應的<result>子節點!也就是以上配置可以改爲:

<resultMap type="cn.tedu.spring.User" id="UserMap">
    <result column="group_id" property="groupId"/>
</resultMap>

另外,關於主鍵的配置,推薦使用<id>節點來配置,而不要使用<result>節點,並且,無論主鍵字段的名稱是否匹配,都推薦顯式的配置出來,也就是說,以上配置的推薦代碼應該是:

<!-- id:自定義的名稱 -->
<!-- type:封裝查詢結果的數據類型 -->
<resultMap type="cn.tedu.spring.User" id="UserMap">
    <!-- column:查詢結果中的列名 -->
    <!-- property:封裝查詢結果的類中的屬性名 -->
    <id column="id" property="id"/>
    <result column="group_id" property="groupId"/>
</resultMap>

關於<resultMap>的配置小結:

  • 使用<id>節點來配置主鍵的對應關係,不要使用<result>來配置,並且,即使名稱完全一致,也應該配置;
  • 在執行單表數據查詢時,如果名稱本來就是完全一致的,則可以不必配置對應的<result>子節點!

17. 一對一關係的關聯查詢

假設需要實現:根據id查詢某個用戶詳情時,顯示該用戶歸屬的組的名稱!

需要執行的SQL語句大致是:

select 
	t_user.*, t_group.name
from t_user left join t_group on t_user.group_id=t_group.id where t_user.id=10;

項目的實體類都是不滿足關聯查詢需求的!因爲每1個實體類都是與每1張數據表相對應的,決不可能存在某1個實體類可以對應多張表的關聯查詢結果,爲了封裝關聯查詢的結果,需要創建對應的VO類:

public class UserVO {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String phone;
    private String email;
    private Integer groupId;
    private String groupName;
    
    // Getters & Setters
    // toString()
}

實體類與VO類,從代碼設計方面來看,幾乎是一樣的,只不過,這2種類的定位不同,實體類是需要與數據表相對應的,而VO類是需要與查詢結果相對應的!

設計的抽象方法爲:

UserVO findVOById(Integer id);

映射的SQL語句是:

<select id="findVOById" resultType="cn.tedu.spring.UserVO">
    SELECT 
		t_user.id, username, password, age, phone, email, group_id AS groupId, 
    	t_group.name AS groupName
	FROM 
   		t_user 
    LEFT JOIN 
    	t_group 
    ON 
    	t_user.group_id=t_group.id 
    WHERE 
    	t_user.id=#{id}
</select>

注意:在關聯查詢時,一定不要使用星號*表示字段列表!

【階段小結】當查詢時,如果出現名稱不匹配的問題(查詢結果中的列名與封裝結果的數據類型中的屬性名)時,可以使用自定義別名的方式,使得列名與屬性名一致,也可以使用<resultMap>指導MyBatis進行封裝,暫定的規則是:當查詢允許使用星號(*)表示字段列表時,應該使用<resultMap>進行配置,當查詢不允許使用星號(*)時,就需要自行窮舉字段列表,就順便自定義別名,以解決名稱不匹配的問題。


根據id查詢某個用戶組的詳情時,顯示該組的所有用戶的信息!需要執行的SQL語句大致是:

select * from t_group left join t_user on t_group.id=t_user.group_id where t_group.id=3;

18. 一對多的關聯數據查詢

假設需要實現:根據id查詢某個用戶組的詳情時,顯示該組的所有用戶的信息!

需要執行的SQL語句大致是:

select * from t_group left join t_user on t_group.id=t_user.group_id where t_group.id=1;

首先,需要在項目中創建新的GroupVO類,用於封裝查詢結果:

public class GroupVO {
    private Integer id;
    private String name;
    private List<User> users;
}

GroupMapper接口中添加抽象方法:

GroupVO findVOById(Integer id);

GroupMapper.xml文件中配置SQL映射:

<resultMap id="GroupMap" type="cn.tedu.spring.GroupVO">
	<id column="gid" property="id" />
    <result column="name" property="name" />
    <!-- collection節點:用於配置1對多的屬性,也就是List集合類型的屬性 -->
    <!-- ofType屬性:List集合中的元素的類型 -->
    <!-- 在collection節點的子級的各id、result節點中的property指的是ofType的類中的屬性名 -->
    <collection property="users" ofType="cn.tedu.spring.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="age" property="age"/>
        <result column="phone" property="phone"/>
        <result column="email" property="email"/>
        <result column="group_id" property="groupId"/>
    </collection>
</resultMap>

<select id="findVOById" resultMap="GroupMap">
	SELECT 
    	t_group.id AS gid, name,
    	t_user.* 
    FROM 
    	t_group 
    LEFT JOIN
    	t_user 
    ON 
    	t_group.id=t_user.group_id 
    WHERE 
    	t_group.id=#{id}
</select>

【MyBatis階段小結】

  • 【理解】MyBatis的主要作用:簡化持久層開發;

  • 【掌握】使用MyBatis框架時需要添加的依賴;

  • 【認識】使用MyBatis框架時必要的配置;

  • 【掌握】抽象方法的設計原則:

    • 返回值:如果是增、刪、改類型的操作,使用Integer作爲返回值類型;如果是查詢類型的操作,可以使用期望的類型作爲返回值類型,只要能把查詢結果封裝進去就行;
    • 方法名稱:自定義,但是不允許重載;
    • 參數列表:根據需要執行的SQL語句中的參數來設計抽象方法的參數列表,簡單的說,就是SQL語句中有哪些問號,在抽象方法中就設計哪些參數,當參數較多時,還可以使用封裝的類型作爲參數,使得抽象方法中的1個參數就可以表示SQL語句中的若干個參數,當抽象方法的參數超過1個時,必須爲每個參數都配置@Param註解。
  • 【掌握】配置SQL映射:

    • 通常,每個接口文件都有1個對應的XML文件,在配置SQL語句的XML中,必須在根節點<mapper>中配置namespace屬性指定對應的接口文件;
    • 需要根據所執行的SQL語句的種類來選取<insert><delete><update><select>節點,每個節點都必須配置id屬性指定對應的抽象方法的名稱,<select>還必須配置resultTyperesultMap中的某1個屬性。
  • 【理解】#{}${}格式的佔位符的區別;

  • 【掌握】解決查詢時,查詢結果中的列名與封裝結果的類的屬性名不一致的問題:

    • 在SQL語句中指定列的別名,使得查詢結果中的列名能與類的屬性名匹配;
    • 配置<resultMap>節點,以指定MyBatis框架完成封裝過程。
  • 【掌握】動態SQL的<foreach>節點的使用;

  • 【瞭解】動態SQL的<if><choose>系列節點的使用;

  • 【理解】實體類與VO類的定位與區別;

  • 【掌握】<resultMap>的配置與使用;

  • 【理解】在處理查詢時,什麼時候需要自定義別名:

    • 在設計SQL語句中,不使用星號(*)表示字段列表,且存在名稱不匹配的問題時,例如實現1對1的關聯查詢時;
    • 在關聯查詢時,查詢結果中出現了名稱完全相同的列名時,必須通過自定義別名,使得查詢結果中的每個列名都不同。
  • 【理解】在處理查詢時,什麼時候需要配置<resultMap>

    • 在SQL語句中使用了星號(*)表示字段列表,且存在名稱不匹配的問題時,配置<resultMap>便於應用到多個不同的查詢中;
    • 需要實現1對多的關聯查詢時。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章