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語句是
INSERT
、DELETE
、UPDATE
類型之一時,將返回值設計爲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/java的cn.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>
節點,該節點必須配置resultType
或resultMap
中的某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/resources的mappers文件夾下,粘貼得到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個甚至更多參數,以此類推,即使用arg2
、arg3
……當然,也可以使用param
系列的參數名稱,只不過是從1開始順序編號的,例如參數名爲param1
、param2
、param3
、param4
……
雖然使用arg
或param
系列的名稱可以解決多參數的問題,但是,會導致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語句片段時,各值之間使用什麼符號進行分隔。 -
open
與close
:遍歷生成的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>
還必須配置resultType
或resultMap
中的某1個屬性。
- 通常,每個接口文件都有1個對應的XML文件,在配置SQL語句的XML中,必須在根節點
-
【理解】
#{}
和${}
格式的佔位符的區別; -
【掌握】解決查詢時,查詢結果中的列名與封裝結果的類的屬性名不一致的問題:
- 在SQL語句中指定列的別名,使得查詢結果中的列名能與類的屬性名匹配;
- 配置
<resultMap>
節點,以指定MyBatis框架完成封裝過程。
-
【掌握】動態SQL的
<foreach>
節點的使用; -
【瞭解】動態SQL的
<if>
和<choose>
系列節點的使用; -
【理解】實體類與VO類的定位與區別;
-
【掌握】
<resultMap>
的配置與使用; -
【理解】在處理查詢時,什麼時候需要自定義別名:
- 在設計SQL語句中,不使用星號(
*
)表示字段列表,且存在名稱不匹配的問題時,例如實現1對1的關聯查詢時; - 在關聯查詢時,查詢結果中出現了名稱完全相同的列名時,必須通過自定義別名,使得查詢結果中的每個列名都不同。
- 在設計SQL語句中,不使用星號(
-
【理解】在處理查詢時,什麼時候需要配置
<resultMap>
:- 在SQL語句中使用了星號(
*
)表示字段列表,且存在名稱不匹配的問題時,配置<resultMap>
便於應用到多個不同的查詢中; - 需要實現1對多的關聯查詢時。
- 在SQL語句中使用了星號(