關(guān)于Mybatis和JDBC的使用及區(qū)別
基于java的持久層框架,它內(nèi)部封裝了jdbc,使開(kāi)發(fā)者只需要關(guān)注sql語(yǔ)句本身,而不需要花費(fèi)精力去處理加載驅(qū)動(dòng)、創(chuàng)建連接、創(chuàng)建statement等繁雜的過(guò)程。
1、JDBC
1.1、流程
下面是一個(gè)簡(jiǎn)單的示例,演示如何使用 JDBC 直接與數(shù)據(jù)庫(kù)進(jìn)行交互:
1. Maven 依賴(如果使用 Maven)
如果您的項(xiàng)目使用 Maven,確保在 pom.xml
中包含 JDBC 驅(qū)動(dòng)的依賴,例如使用 MySQL:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> <!-- 根據(jù)您需要的版本選擇 --> </dependency>
2. 加載數(shù)據(jù)庫(kù)驅(qū)動(dòng)和管理 SQL
import java.sql.*; public class JdbcExample { public static void main(String[] args) { Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { // 1. 加載數(shù)據(jù)庫(kù)驅(qū)動(dòng) Class.forName("com.mysql.cj.jdbc.Driver"); // 替換為相應(yīng)驅(qū)動(dòng) // 2. 建立數(shù)據(jù)庫(kù)連接 String url = "jdbc:mysql://localhost:3306/yourdatabase"; // 數(shù)據(jù)庫(kù) URL String user = "yourusername"; // 數(shù)據(jù)庫(kù)用戶名 String password = "yourpassword"; // 數(shù)據(jù)庫(kù)密碼 connection = DriverManager.getConnection(url, user, password); // 3. 創(chuàng)建 PreparedStatement String sql = "SELECT * FROM users WHERE id = ?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setLong(1, 1); // 設(shè)置參數(shù) // 4. 執(zhí)行查詢 resultSet = preparedStatement.executeQuery(); // 5. 處理結(jié)果 while (resultSet.next()) { System.out.println("User ID: " + resultSet.getInt("id")); System.out.println("User Name: " + resultSet.getString("name")); System.out.println("User Email: " + resultSet.getString("email")); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } finally { // 6. 關(guān)閉資源 try { if (resultSet != null) resultSet.close(); if (preparedStatement != null) preparedStatement.close(); if (connection != null) connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
從以上代碼展示,可以看到j(luò)dbc的流程:
- 加載數(shù)據(jù)庫(kù)驅(qū)動(dòng):加載與所使用數(shù)據(jù)庫(kù)對(duì)應(yīng)的 JDBC 驅(qū)動(dòng)。
- 建立數(shù)據(jù)庫(kù)連接:使用
DriverManager.getConnection()
方法建立與數(shù)據(jù)庫(kù)的連接。 - 創(chuàng)建
Statement
或PreparedStatement
:用于執(zhí)行 SQL 查詢或更新。 - 執(zhí)行 SQL 語(yǔ)句:通過(guò)
executeQuery()
或executeUpdate()
方法執(zhí)行 SQL 語(yǔ)句。 - 處理結(jié)果:處理查詢結(jié)果 (如果有)。
- 關(guān)閉資源:關(guān)閉
ResultSet
、Statement
、Connection
等資源,防止資源泄漏。
1.2、優(yōu)缺點(diǎn)
1.優(yōu)點(diǎn):
- 直接靈活:可以完全控制 SQL 語(yǔ)句和數(shù)據(jù)庫(kù)交互的細(xì)節(jié)。
- 簡(jiǎn)單:適合小型應(yīng)用或簡(jiǎn)單查詢。
2.缺點(diǎn):
- 冗長(zhǎng):代碼冗長(zhǎng),尤其是在處理事務(wù)和異常時(shí),容易導(dǎo)致代碼重復(fù)。
- 不易維護(hù):SQL 語(yǔ)句與業(yè)務(wù)邏輯混雜,導(dǎo)致難以維護(hù)和管理。
- 手動(dòng)管理:需要手動(dòng)處理資源的關(guān)閉,容易出現(xiàn)資源泄漏。
- 缺乏抽象:不支持對(duì)象關(guān)系映射,處理復(fù)雜數(shù)據(jù)結(jié)構(gòu)時(shí)不夠方便。
因此基于以上的現(xiàn)象,mybatis或者Hibernate就可以滿足更復(fù)雜的要求,且統(tǒng)一管理,便于維護(hù)。
2、Mybatis
2.1、執(zhí)行流程
2.2、使用
下面是一個(gè)使用 MyBatis 連接 MySQL 數(shù)據(jù)庫(kù)的完整示例。這個(gè)示例展示了如何使用 MyBatis 進(jìn)行基本的 CRUD 操作,同時(shí)使用 MySQL 作為數(shù)據(jù)庫(kù)。
項(xiàng)目結(jié)構(gòu):
mybatis-mysql-example
│
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ ├── Main.java
│ │ │ ├── User.java
│ │ │ ├── UserMapper.java
│ │ │ └── MyBatisUtils.java
│ │ └── resources
│ │ ├── mybatis-config.xml
│ │ └── mapper
│ │ └── UserMapper.xml
└── pom.xml
1. Maven 依賴
確保在 pom.xml
中添加 MyBatis 和 MySQL 的 JDBC 驅(qū)動(dòng)的依賴:
<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.example</groupId> <artifactId>mybatis-mysql-example</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>11</java.version> <mybatis.version>3.5.7</mybatis.version> <mysql.version>8.0.25</mysql.version> <!-- 使用適合你的版本 --> </properties> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
2. 創(chuàng)建用戶實(shí)體類(User.java)
package com.example; public class User { private Long id; private String username; private String email; // Getters 和 Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
3. 創(chuàng)建 Mapper 接口(UserMapper.java)
package com.example; import org.apache.ibatis.annotations.*; import java.util.List; @Mapper public interface UserMapper { void insertUser(User user); User getUser(Long id); List<User> getAllUsers(); List<User> getUsersByPage(@Param("offset") int offset, @Param("limit") int limit); // 分頁(yè)查詢 void updateUser(User user); void deleteUser(Long id); }
4. 創(chuàng)建 MyBatis 配置文件(mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <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/yourdatabase?useSSL=false&serverTimezone=UTC"/> <property name="username" value="yourusername"/> <property name="password" value="yourpassword"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>
5. 創(chuàng)建 Mapper XML 文件(UserMapper.xml)
在 src/main/resources/mapper
目錄下創(chuàng)建 UserMapper.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="com.example.UserMapper"> <insert id="insertUser" parameterType="com.example.User"> INSERT INTO users (username, email) VALUES (#{username}, #{email}) </insert> <select id="getUser" resultType="com.example.User" parameterType="long"> SELECT * FROM users WHERE id = #{id} </select> <select id="getAllUsers" resultType="com.example.User"> SELECT * FROM users </select> <select id="getUsersByPage" resultType="com.example.User"> SELECT * FROM users LIMIT #{limit} OFFSET #{offset} <!-- 分頁(yè)查詢 --> </select> <update id="updateUser" parameterType="com.example.User"> UPDATE users SET username = #{username}, email = #{email} WHERE id = #{id} </update> <delete id="deleteUser" parameterType="long"> DELETE FROM users WHERE id = #{id} </delete> </mapper>
6. 創(chuàng)建 MyBatis 實(shí)用工具類(MyBatisUtils.java)
package com.example; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.InputStream; public class MyBatisUtils { private static SqlSessionFactory sqlSessionFactory; static { try { // 讀取 mybatis-config.xml 配置文件 InputStream inputStream = MyBatisUtils.class.getClassLoader().getResourceAsStream("mybatis-config.xml"); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static SqlSession getSqlSession() { return sqlSessionFactory.openSession(); } }
7. 創(chuàng)建主類(Main.java)
這是程序的入口,用于執(zhí)行 CRUD 操作:
您可以創(chuàng)建一個(gè)服務(wù)類來(lái)處理業(yè)務(wù)邏輯,包括分頁(yè)查詢:
package com.example; import org.apache.ibatis.session.SqlSession; import java.util.List; public class UserService { public List<User> getUsersByPage(int pageNumber, int pageSize) { SqlSession sqlSession = MyBatisUtils.getSqlSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); int offset = (pageNumber - 1) * pageSize; // 計(jì)算偏移量 return userMapper.getUsersByPage(offset, pageSize); // 調(diào)用分頁(yè)方法 } finally { sqlSession.close(); // 關(guān)閉 SQL 會(huì)話 } } }
調(diào)用分頁(yè)查詢方法并打印結(jié)果:
package com.example; import org.apache.ibatis.session.SqlSession; import java.util.List; public class Main { public static void main(String[] args) { // Initialize MyBatis and data UserService userService = new UserService(); // 進(jìn)行分頁(yè)查詢 int pageNumber = 1; // 頁(yè)碼 int pageSize = 5; // 每頁(yè)條數(shù) List<User> users = userService.getUsersByPage(pageNumber, pageSize); // 打印結(jié)果 System.out.println("Users on Page " + pageNumber + ":"); for (User user : users) { System.out.println("- " + user.getUsername()); } } }
其他接口可以使用以下方法查詢:
package com.example; import org.apache.ibatis.session.SqlSession; import java.util.List; public class Main { public static void main(String[] args) { SqlSession sqlSession = MyBatisUtils.getSqlSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 插入用戶 User user = new User(); user.setUsername("john_doe"); user.setEmail("john@example.com"); userMapper.insertUser(user); sqlSession.commit(); // 查詢用戶 User retrievedUser = userMapper.getUser(1L); System.out.println("Retrieved User: " + retrievedUser.getUsername()); // 查詢所有用戶 List<User> users = userMapper.getAllUsers(); System.out.println("All Users:"); for (User u : users) { System.out.println(u.getUsername()); } // 更新用戶 user.setEmail("john_doe_updated@example.com"); userMapper.updateUser(user); sqlSession.commit(); // 刪除用戶 userMapper.deleteUser(1L); sqlSession.commit(); } finally { sqlSession.close(); // 確保資源關(guān)閉 } } }
8. 創(chuàng)建 MySQL 數(shù)據(jù)庫(kù)和表
在 MySQL 數(shù)據(jù)庫(kù)中,您需要?jiǎng)?chuàng)建數(shù)據(jù)庫(kù)和用戶表。假設(shè)您創(chuàng)建了一個(gè)名為 yourdatabase
的數(shù)據(jù)庫(kù),執(zhí)行以下 SQL 語(yǔ)句:
CREATE DATABASE yourdatabase; USE yourdatabase; CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100), email VARCHAR(100) );
9、運(yùn)行程序。
2.3、實(shí)現(xiàn)方式
1、xml配置文件
通過(guò) .xml
文件定義 SQL 語(yǔ)句和映射關(guān)系,與 Mapper 接口綁定。
<!-- UserMapper.xml --> <select id="getUserById" resultType="User"> SELECT * FROM user WHERE id = #{id} </select>
2、注解
直接在 Mapper 接口的方法上使用注解(如 @Select
、@Update
)編寫 SQL。
@Select("SELECT * FROM user WHERE id = #{id}") User getUserById(Long id);
3、動(dòng)態(tài) SQL
早期 MyBatis 注解對(duì)動(dòng)態(tài) SQL 支持較弱,但通過(guò) @SelectProvider
、@UpdateProvider
或者xml方式等注解,現(xiàn)已能實(shí)現(xiàn)復(fù)雜邏輯。
XML 動(dòng)態(tài)標(biāo)簽
通過(guò) XML 中的 <if>
、<choose>
、<foreach>
等標(biāo)簽實(shí)現(xiàn)條件邏輯。
代碼示例:
<select id="searchUsers"> SELECT * FROM user <where> <if test="name != null">AND name = #{name}</if> <if test="role != null">AND role = #{role}</if> </where> </select>
注解結(jié)合 Provider 類
使用 @SelectProvider
、@UpdateProvider
注解,調(diào)用工具類生成動(dòng)態(tài) SQL:
代碼示例:
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUserByName") User getUserByName(@Param("name") String name); class UserSqlBuilder { public String buildGetUserByName(Map<String, Object> params) { return new SQL() {{ SELECT("*"); FROM("user"); WHERE("name = #{name}"); if (params.get("role") != null) { AND().WHERE("role = #{role}"); } }}.toString(); } }
4、MyBatis-Plus 擴(kuò)展實(shí)現(xiàn)
基于 MyBatis 的增強(qiáng)工具,提供通用 Mapper(BaseMapper
)和 ActiveRecord 模式,減少手寫 SQL。
// 繼承 BaseMapper 直接調(diào)用內(nèi)置方法 public interface UserMapper extends BaseMapper<User> {} // 使用 ActiveRecord 模式 User user = new User(); user.setName("John"); user.insert();
總結(jié)
3、緩存
緩存的查找順序:二級(jí)緩存 => 一級(jí)緩存 => 數(shù)據(jù)庫(kù)。
3.1. 一級(jí)緩存
一級(jí)緩存是SqlSession級(jí)別的緩存,默認(rèn)開(kāi)啟,默認(rèn)是 SESSION 級(jí)
1.原理
每個(gè)SqlSession中持有了Executor,每個(gè)Executor中有一個(gè)LocalCache。
當(dāng)用戶發(fā)起查詢時(shí),MyBatis根據(jù)當(dāng)前執(zhí)行的語(yǔ)句生成MappedStatement,在Local Cache進(jìn)行查詢,如果緩存命中的話,直接返回結(jié)果給用戶,如果緩存沒(méi)有命中的話,查詢數(shù)據(jù)庫(kù),結(jié)果寫入Local Cache,最后返回結(jié)果給用戶。
Local Cache 其實(shí)是一個(gè) hashmap 的結(jié)構(gòu):
private Map<Object, Object> cache = new HashMap<Object, Object>();
2.核心流程
- 第一次查詢:從數(shù)據(jù)庫(kù)讀取數(shù)據(jù),存入一級(jí)緩存。
- 第二次相同查詢:直接讀取緩存,不訪問(wèn)數(shù)據(jù)庫(kù)。
- 執(zhí)行更新操作:清空一級(jí)緩存,后續(xù)查詢重新訪問(wèn)數(shù)據(jù)庫(kù)。
代碼示例:
// 示例代碼 public static void main(String[] args) { SqlSessionFactory factory = ...; // 獲取 SqlSessionFactory try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); System.out.println("--- 第一次查詢 ---"); User user1 = mapper.getUserById(1L); // 查詢數(shù)據(jù)庫(kù) System.out.println("查詢結(jié)果: " + user1); System.out.println("--- 第二次相同查詢 ---"); User user2 = mapper.getUserById(1L); // 從一級(jí)緩存讀取 System.out.println("查詢結(jié)果: " + user2); System.out.println("user1 == user2? " + (user1 == user2)); // 輸出 true System.out.println("--- 執(zhí)行更新操作 ---"); user1.setName("NewName"); mapper.updateUser(user1); // 清空一級(jí)緩存 session.commit(); // 提交事務(wù) System.out.println("--- 第三次查詢 ---"); User user3 = mapper.getUserById(1L); // 重新查詢數(shù)據(jù)庫(kù) System.out.println("查詢結(jié)果: " + user3); } }
預(yù)期輸出:
--- 第一次查詢 ---
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
查詢結(jié)果: User(id=1, name=OldName)--- 第二次相同查詢 ---
查詢結(jié)果: User(id=1, name=OldName)
user1 == user2? true // 直接從緩存獲取,無(wú) SQL 日志--- 執(zhí)行更新操作 ---
DEBUG [main] - ==> Preparing: UPDATE user SET name = ? WHERE id = ?
DEBUG [main] - ==> Parameters: NewName(String), 1(Long)
DEBUG [main] - <== Updates: 1--- 第三次查詢 ---
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
查詢結(jié)果: User(id=1, name=NewName)
關(guān)鍵點(diǎn)
- 兩次查詢結(jié)果相同:第二次未觸發(fā) SQL,直接返回緩存對(duì)象(
user1 == user2
)。 - 更新操作后緩存失效:第三次查詢重新訪問(wèn)數(shù)據(jù)庫(kù)。
3.緩存失效
不同的SqlSession對(duì)應(yīng)不同的一級(jí)緩存
- 同一個(gè)SqlSession但是查詢條件不同
- 同一個(gè)SqlSession兩次查詢期間執(zhí)行了任何一次增刪改操作
- 同一個(gè)SqlSession兩次查詢期間手動(dòng)清空了緩存
3.2. 二級(jí)緩存
二級(jí)緩存是NameSpace級(jí)別(Mapper)的緩存,多個(gè)SqlSession可以共享,使用時(shí)需要進(jìn)行配置開(kāi)啟,同一個(gè)namespace影響同一個(gè)cache。
1.執(zhí)行順序
- 先查詢二級(jí)緩存,因?yàn)槎?jí)緩存中可能會(huì)有其他程序已經(jīng)查出來(lái)的數(shù)據(jù),可以拿來(lái)直接使用。
- 如果二級(jí)緩存沒(méi)有命中,再查詢一級(jí)緩存
- 如果一級(jí)緩存也沒(méi)有命中,則查詢數(shù)據(jù)庫(kù)
- SqlSession關(guān)閉之后,一級(jí)緩存中的數(shù)據(jù)會(huì)寫入二級(jí)緩存。
代碼示例:
public static void main(String[] args) { SqlSessionFactory factory = ...; // 已開(kāi)啟二級(jí)緩存 // 第一個(gè)會(huì)話(查詢并提交) try (SqlSession session1 = factory.openSession()) { UserMapper mapper1 = session1.getMapper(UserMapper.class); System.out.println("--- SqlSession1 查詢 ---"); User user1 = mapper1.getUserById(1L); // 查詢數(shù)據(jù)庫(kù) System.out.println("查詢結(jié)果: " + user1); session1.commit(); // 提交后同步到二級(jí)緩存 } // 第二個(gè)會(huì)話(相同查詢) try (SqlSession session2 = factory.openSession()) { UserMapper mapper2 = session2.getMapper(UserMapper.class); System.out.println("--- SqlSession2 查詢 ---"); User user2 = mapper2.getUserById(1L); // 從二級(jí)緩存讀取 System.out.println("查詢結(jié)果: " + user2); } // 第三個(gè)會(huì)話(更新數(shù)據(jù)) try (SqlSession session3 = factory.openSession()) { UserMapper mapper3 = session3.getMapper(UserMapper.class); System.out.println("--- SqlSession3 更新 ---"); User user = new User(1L, "UpdatedName"); mapper3.updateUser(user); // 清空二級(jí)緩存 session3.commit(); } // 第四個(gè)會(huì)話(重新查詢) try (SqlSession session4 = factory.openSession()) { UserMapper mapper4 = session4.getMapper(UserMapper.class); System.out.println("--- SqlSession4 查詢 ---"); User user4 = mapper4.getUserById(1L); // 重新查詢數(shù)據(jù)庫(kù) System.out.println("查詢結(jié)果: " + user4); } }
預(yù)期輸出:
--- SqlSession1 查詢 ---
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
查詢結(jié)果: User(id=1, name=OldName)--- SqlSession2 查詢 ---
查詢結(jié)果: User(id=1, name=OldName) // 無(wú) SQL 日志,直接讀二級(jí)緩存--- SqlSession3 更新 ---
DEBUG [main] - ==> Preparing: UPDATE user SET name = ? WHERE id = ?
DEBUG [main] - ==> Parameters: UpdatedName(String), 1(Long)
DEBUG [main] - <== Updates: 1--- SqlSession4 查詢 ---
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
查詢結(jié)果: User(id=1, name=UpdatedName)
關(guān)鍵點(diǎn)
- 跨會(huì)話共享緩存:
SqlSession2
未訪問(wèn)數(shù)據(jù)庫(kù)。 - 更新操作清空緩存:
SqlSession3
更新后,SqlSession4
重新查詢數(shù)據(jù)庫(kù)。
2、配置二級(jí)緩存(關(guān)鍵步驟)
1.實(shí)體類實(shí)現(xiàn) Serializable
:
public class User implements Serializable { // ... }
2.Mapper XML 中添加 <cache/>
:
<mapper namespace="com.example.UserMapper"> <cache eviction="LRU" flushInterval="60000" size="1024"/> </mapper>
3.3. 緩存區(qū)別
4、Sql注入
4.1.#{}參數(shù)占位符
#{}
是 MyBatis 中的 參數(shù)占位符,用于將參數(shù)傳遞到 SQL 語(yǔ)句中。 會(huì)將 #{}
中的參數(shù)值自動(dòng)進(jìn)行轉(zhuǎn)義,將 sql 中的#{}
替換為 ? 號(hào)。
作用是將參數(shù)值進(jìn)行處理并填充到 SQL 語(yǔ)句中,在傳遞參數(shù)時(shí)會(huì)進(jìn)行預(yù)處理,避免 SQL 注入風(fēng)險(xiǎn)。
4.2.${}字符串拼接
${}
是 MyBatis 中的 字符串拼接占位符,用于直接將參數(shù)值拼接到 SQL 語(yǔ)句中。
它會(huì)將傳入的參數(shù)值直接替換到 SQL 中的 ${}
所在的位置,而不會(huì)進(jìn)行任何的處理或轉(zhuǎn)義。
由下圖所說(shuō):
顯然#{}直接去數(shù)據(jù)庫(kù)查詢,是查不到這個(gè)username,而${}由于沒(méi)有進(jìn)行轉(zhuǎn)義處理,就會(huì)執(zhí)行1=1;
4.3.SQL注入
MyBatis的#{}之所以能夠預(yù)防SQL注入是因?yàn)榈讓邮褂昧薖reparedStatement類的setString()方法來(lái)設(shè)置參數(shù),此方法會(huì)獲取傳遞進(jìn)來(lái)的參數(shù)的每個(gè)字符,然后進(jìn)行循環(huán)對(duì)比,如果發(fā)現(xiàn)有敏感字符(如:?jiǎn)我?hào)、雙引號(hào)等),則會(huì)在前面加上一個(gè)'/'代表轉(zhuǎn)義此符號(hào),讓其變?yōu)橐粋€(gè)普通的字符串,不參與SQL語(yǔ)句的生成,達(dá)到防止SQL注入的效果。
其次${}本身設(shè)計(jì)的初衷就是為了參與SQL語(yǔ)句的語(yǔ)法生成,自然而然會(huì)導(dǎo)致SQL注入的問(wèn)題(不會(huì)考慮字符過(guò)濾問(wèn)題)。
setString方法為什么可以解決。
public void setString(int parameterIndex, String x) throws SQLException { synchronized(this.checkClosed().getConnectionMutex()) { if (x == null) { this.setNull(parameterIndex, 1); } else { this.checkClosed(); int stringLength = x.length(); StringBuilder buf; if (this.connection.isNoBackslashEscapesSet()) { boolean needsHexEscape = this.isEscapeNeededForString(x, stringLength); Object parameterAsBytes; byte[] parameterAsBytes; if (!needsHexEscape) { parameterAsBytes = null; buf = new StringBuilder(x.length() + 2); buf.append('\''); buf.append(x); buf.append('\''); if (!this.isLoadDataQuery) { parameterAsBytes = StringUtils.getBytes(buf.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor()); } else { parameterAsBytes = StringUtils.getBytes(buf.toString()); } this.setInternal(parameterIndex, parameterAsBytes); } else { parameterAsBytes = null; if (!this.isLoadDataQuery) { parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor()); } else { parameterAsBytes = StringUtils.getBytes(x); } this.setBytes(parameterIndex, parameterAsBytes); } return; } String parameterAsString = x; boolean needsQuoted = true; if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) { needsQuoted = false; buf = new StringBuilder((int)((double)x.length() * 1.1D)); buf.append('\''); for(int i = 0; i < stringLength; ++i) { //遍歷字符串,獲取到每個(gè)字符 char c = x.charAt(i); switch(c) { case '\u0000': buf.append('\\'); buf.append('0'); break; case '\n': buf.append('\\'); buf.append('n'); break; case '\r': buf.append('\\'); buf.append('r'); break; case '\u001a': buf.append('\\'); buf.append('Z'); break; case '"': if (this.usingAnsiMode) { buf.append('\\'); } buf.append('"'); break; case '\'': buf.append('\\'); buf.append('\''); break; case '\\': buf.append('\\'); buf.append('\\'); break; case '¥': case '?': if (this.charsetEncoder != null) { CharBuffer cbuf = CharBuffer.allocate(1); ByteBuffer bbuf = ByteBuffer.allocate(1); cbuf.put(c); cbuf.position(0); this.charsetEncoder.encode(cbuf, bbuf, true); if (bbuf.get(0) == 92) { buf.append('\\'); } } buf.append(c); break; default: buf.append(c); } } buf.append('\''); parameterAsString = buf.toString(); } buf = null; byte[] parameterAsBytes; if (!this.isLoadDataQuery) { if (needsQuoted) { parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor()); } else { parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor()); } } else { parameterAsBytes = StringUtils.getBytes(parameterAsString); } this.setInternal(parameterIndex, parameterAsBytes); this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 12; } } }
4.4.用法總結(jié)
1)#{}在使用時(shí),會(huì)根據(jù)傳遞進(jìn)來(lái)的值來(lái)選擇是否加上雙引號(hào),因此我們傳遞參數(shù)的時(shí)候一般都是直接傳遞,不用加雙引號(hào),${}則不會(huì),我們需要手動(dòng)加。
2)在傳遞一個(gè)參數(shù)時(shí),我們說(shuō)了#{}中可以寫任意的值,${}則必須使用value;即:${value}
3)#{}針對(duì)SQL注入進(jìn)行了字符過(guò)濾,${}則只是作為普通傳值,并沒(méi)有考慮到這些問(wèn)題
4)#{}的應(yīng)用場(chǎng)景是為給SQL語(yǔ)句的where字句傳遞條件值,${}的應(yīng)用場(chǎng)景是為了傳遞一些需要參與SQL語(yǔ)句語(yǔ)法生成的值。
5、懶加載
MyBatis 的懶加載功能允許在需要時(shí)才加載關(guān)聯(lián)的對(duì)象,從而提高系統(tǒng)的性能。懶加載是一種常用的策略,尤其適用于處理大型數(shù)據(jù)集時(shí),避免在查詢時(shí)加載不必要的數(shù)據(jù)。
5.1、原理
在 MyBatis 中,懶加載是通過(guò)延遲加載關(guān)聯(lián)對(duì)象的策略實(shí)現(xiàn)的。當(dāng)你訪問(wèn)一個(gè)未初始化的屬性時(shí),MyBatis 會(huì)自動(dòng)從數(shù)據(jù)庫(kù)加載相關(guān)的數(shù)據(jù),而不是在首次加載父對(duì)象時(shí)就將所有的關(guān)聯(lián)數(shù)據(jù)一起加載。
5.2、開(kāi)啟
全局配置: 你可以在 MyBatis 的全局配置文件 mybatis-config.xml
中設(shè)置懶加載功能。
<configuration> <settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> <!-- 控制是否在訪問(wèn)懶加載對(duì)象時(shí)立即加載 --> </settings> </configuration>
lazyLoadingEnabled
: 設(shè)置為true
時(shí)啟用懶加載。aggressiveLazyLoading
: 如果設(shè)置為true
,當(dāng)你訪問(wèn)一個(gè)對(duì)象的任何屬性時(shí),會(huì)立即加載所有懶加載的屬性。如果設(shè)置為false
,那么只有在訪問(wèn)特定的懶加載屬性時(shí)才會(huì)加載。
5.3、實(shí)現(xiàn)
假設(shè)我們有兩個(gè)實(shí)體:User
和 Order
,每個(gè)用戶可以有多個(gè)訂單。這是一個(gè)常見(jiàn)的一對(duì)多關(guān)系的例子。
1. 數(shù)據(jù)庫(kù)表結(jié)構(gòu)
CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL ); CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_number VARCHAR(50) NOT NULL, user_id INT, FOREIGN KEY (user_id) REFERENCES users(user_id) );
2. POJO 類
User.java
import java.util.List; public class User { private Integer userId; private String username; private List<Order> orders; // 一對(duì)多關(guān)聯(lián) // Getters 和 Setters // ... }
Order.java
public class Order { private Integer orderId; private String orderNumber; // Getters 和 Setters // ... }
3. Mapper 接口
我們創(chuàng)建一個(gè) Mapper 接口來(lái)定義查詢方法。
package com.example.mapper; import com.example.model.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; @Mapper public interface UserMapper { @Select("SELECT * FROM users WHERE user_id = #{userId}") User findUserById(Integer userId); }
4. XML 映射文件
為了懶加載訂單數(shù)據(jù),我們可以在 User
類中定義一個(gè)方法來(lái)獲取訂單。如果你不希望在獲取用戶時(shí)立即加載用戶的所有訂單數(shù)據(jù),可以通過(guò)以下方式進(jìn)行設(shè)計(jì)。
UserMapper.xml
<mapper namespace="com.example.mapper.UserMapper"> <resultMap id="userResultMap" type="User"> <id property="userId" column="user_id"/> <result property="username" column="username"/> <collection property="orders" ofType="Order" select="com.example.mapper.OrderMapper.findOrdersByUserId" fetchType="lazy"/> <!-- 使用懶加載 --> </resultMap> <select id="findUserById" resultMap="userResultMap"> SELECT * FROM users WHERE user_id = #{userId} </select> </mapper>
- 在
UserMapper.xml
中,<collection>
標(biāo)簽表示與Order
的一對(duì)多關(guān)系的懶加載配置。通過(guò)select
屬性指定查詢訂單的 SQL。 fetchType="lazy"
表示此集合數(shù)據(jù)將延遲加載。
OrderMapper.java
package com.example.mapper; import com.example.model.Order; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; @Mapper public interface OrderMapper { @Select("SELECT * FROM orders WHERE user_id = #{userId}") List<Order> findOrdersByUserId(Integer userId); }
5. 使用懶加載的情況
在你的服務(wù)層中,你可以調(diào)用 findUserById
方法。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired private UserMapper userMapper; public User getUserWithOrders(Integer userId) { User user = userMapper.findUserById(userId); // Orders 還沒(méi)有被加載,直到你調(diào)用 user.getOrders() return user; } }
6. 控制器層
你可以使用 Spring MVC 來(lái)創(chuàng)建一個(gè)控制器,處理 HTTP 請(qǐng)求。
UserController.java:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public User getUser(@PathVariable Integer id) { return userService.getUserWithOrders(id); } }
7. 使用示例
當(dāng)你訪問(wèn) http://localhost:8080/users/1
,會(huì)調(diào)用 getUser()
方法。
- 一開(kāi)始,
findUserById
方法會(huì)查詢用戶信息,但訂單還不會(huì)被加載。 - 當(dāng)你訪問(wèn)
user.getOrders()
時(shí),MyBatis 會(huì)自動(dòng)調(diào)用OrderMapper.findOrdersByUserId
方法來(lái)查詢與該用戶相關(guān)的訂單。
8. 總結(jié)
以上代碼演示了如何在 MyBatis 中實(shí)現(xiàn)懶加載功能。通過(guò)配置 XML 文件中的 <collection>
標(biāo)簽結(jié)合 fetchType="lazy"
設(shè)置,可以確保在訪問(wèn)相關(guān)集合屬性時(shí)才加載數(shù)據(jù)。這能幫助提高性能,減少不必要的數(shù)據(jù)加載。
懶加載機(jī)制在處理大數(shù)據(jù)量、頻繁不使用的關(guān)聯(lián)對(duì)象時(shí)尤為重要,有效節(jié)約系統(tǒng)資源。
6、常用問(wèn)題
6.1、映射不一致
當(dāng)實(shí)體類中的屬性名和表中的字段名不一樣 ,怎么辦 ?
使用 @Results
注解或者 <resultMap>
來(lái)明確映射關(guān)系。
假設(shè)我們有一個(gè)數(shù)據(jù)庫(kù)表 users
,其結(jié)構(gòu)如下:
在項(xiàng)目中,有一個(gè)對(duì)應(yīng)的 POJO 類 User
,其屬性使用駝峰命名風(fēng)格:
public class User { private Integer id; // 對(duì)應(yīng) user_id private String username; // 對(duì)應(yīng) user_name private String email; // 對(duì)應(yīng) user_email get set方法略 }
1、使用 XML(resultMap)
1. 創(chuàng)建 MyBatis 映射文件
將使用 XML 文件 UserMapper.xml
來(lái)定義 SQL 操作和字段映射。這個(gè)文件通常位于 resources
目錄下。
UserMapper.xml:
<mapper namespace="com.example.mapper.UserMapper"> <!-- 定義結(jié)果映射 --> <resultMap id="UserResultMap" type="User"> <result property="id" column="user_id"/> <result property="username" column="user_name"/> <result property="email" column="user_email"/> </resultMap> <!-- 查詢用戶的 SQL 操作 --> <select id="getUserById" resultMap="UserResultMap"> SELECT user_id, user_name, user_email FROM users WHERE user_id = #{id} </select> </mapper>
2、使用(Results)
如果你想使用注解的方式,可以直接在 Mapper 接口中使用 @Select
和 @Results
注解來(lái)實(shí)現(xiàn)。下面是一個(gè)示例:
UserMapper.java(注解方式):
package com.example.mapper; import com.example.model.User; import org.apache.ibatis.annotations.*; @Mapper public interface UserMapper { @Select("SELECT user_id, user_name, user_email FROM users WHERE user_id = #{id}") @Results({ @Result(property = "id", column = "user_id"), @Result(property = "username", column = "user_name"), @Result(property = "email", column = "user_email") }) User getUserById(Integer id); }
3、JsonProperty
@JsonProperty("user_id") // 將 JSON 中的 user_id 映射到 id private Integer id; @JsonProperty("user_name") // 將 JSON 中的 user_name 映射到 username private String username; @JsonProperty("user_email") // 將 JSON 中的 user_email 映射到 email private String email;
import com.fasterxml.jackson.databind.ObjectMapper; public class Main { public static void main(String[] args) throws Exception { ObjectMapper objectMapper = new ObjectMapper(); // 創(chuàng)建一個(gè) User 對(duì)象 User user = new User(); user.setId(1); user.setUsername("alice"); user.setEmail("alice@example.com"); // 將 User 對(duì)象序列化為 JSON 字符串 String jsonString = objectMapper.writeValueAsString(user); System.out.println("Serialized JSON: " + jsonString); } } 輸出: {"user_id":1,"user_name":"alice","user_email":"alice@example.com"}
package com.example.mapper; import com.example.model.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface UserMapper { List<User> findUsersByUsernameLike(@Param("username") String username); }
@JsonProperty
注解幫助在序列化和反序列化過(guò)程中,指定 JSON 字段與 POJO 屬性之間的映射關(guān)系。
6.2、模糊查詢
1.CONCAT
創(chuàng)建一個(gè) Mapper 接口,并添加一個(gè)模糊查詢的方法。
package com.example.mapper; import com.example.model.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface UserMapper { List<User> findUsersByUsernameLike(@Param("username") String username); }
在 Mapper XML 文件中定義模糊查詢的 SQL 語(yǔ)句。我們將使用 LIKE
關(guān)鍵字進(jìn)行模糊匹配。
UserMapper.xml:
<mapper namespace="com.example.mapper.UserMapper"> <select id="findUsersByUsernameLike" resultType="User"> SELECT user_id, username, email FROM users WHERE username LIKE CONCAT('%', #{username}, '%') </select> </mapper>
2.傳遞參數(shù)%value%
在傳入?yún)?shù)的時(shí)候比如username:%value% 傳入到mapper文件里面;
6.3、動(dòng)態(tài)標(biāo)簽
1. <if>
用于判斷條件,如果條件滿足則會(huì)生成 SQL 語(yǔ)句中的代碼。
示例:
<select id="findUserByCriteria" resultType="User"> SELECT * FROM users WHERE 1=1 <if test="username != null"> AND username = #{username} </if> <if test="email != null"> AND email = #{email} </if> </select>
在這個(gè)例子中,只有當(dāng) username
或 email
不為 null
時(shí),相應(yīng)的條件才會(huì)被添加到 SQL 中。
2. <choose>
、<when>
和 <otherwise>
用于多條件選擇的情況,類似于 Java 中的 switch-case 語(yǔ)句。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserService { @Autowired private UserMapper userMapper; public List<User> findUsers(String status) { return userMapper.findUsersByStatus(status); } }
<select id="findUserByStatus" resultType="User"> SELECT * FROM users WHERE <choose> <when test="status == 'active'"> status = 'active' </when> <when test="status == 'inactive'"> status = 'inactive' </when> <otherwise> status IS NOT NULL </otherwise> </choose> </select>
在這個(gè)例子中,SQL 查詢將會(huì)根據(jù) status
的值選擇生成查詢條件。
3.<foreach>
用于循環(huán)遍歷集合,通常用于生成 IN 列表或多條 SQL 語(yǔ)句。
示例:
<select id="findUsersByIds" resultType="User"> SELECT * FROM users WHERE user_id IN <foreach item="id" collection="userIds" open="(" separator="," close=")"> #{id} </foreach> </select>
4. <trim>
用于去掉 SQL 語(yǔ)句前后可能出現(xiàn)的多余的字符,主要是在構(gòu)建可選條件時(shí)使用,查詢參數(shù)可以是可選的。
<select id="findUsersByCondition" resultType="User"> SELECT * FROM users <trim prefix="WHERE" prefixOverrides="AND |OR "> <if test="username != null"> AND username = #{username} </if> <if test="email != null"> AND email = #{email} </if> </trim> </select>
<trim>
可以去掉多余的前綴,比如如果沒(méi)有設(shè)定 username
和 email
,可以避免生成 WHERE
后面緊跟著的 AND
。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
推薦一款I(lǐng)ntelliJ IDEA提示快捷鍵的Key Promoter X插件
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA提示快捷鍵的Key Promoter X插件,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10SpringBoot集成Sharding Jdbc使用復(fù)合分片的實(shí)踐
數(shù)據(jù)庫(kù)分庫(kù)分表中間件是采用的 apache sharding。本文主要介紹了SpringBoot集成Sharding Jdbc使用復(fù)合分片的實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2021-09-09redis防止重復(fù)提交的實(shí)現(xiàn)示例
在開(kāi)發(fā)中我們都需要處理重復(fù)提交的問(wèn)題,本文主要介紹了redis防止重復(fù)提交的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-06-06使用postman傳遞list集合后臺(tái)springmvc接收
這篇文章主要介紹了使用postman傳遞list集合后臺(tái)springmvc接收的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08MyBatis 中 ${}和 #{}的正確使用方法(千萬(wàn)不要亂用)
這篇文章主要介紹了MyBatis 中 ${}和 #{}的正確使用方法,本文給大家提到了MyBatis 中 ${}和 #{}的區(qū)別,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07SpringBoot項(xiàng)目運(yùn)行一段時(shí)間后自動(dòng)關(guān)閉的坑及解決
這篇文章主要介紹了SpringBoot項(xiàng)目運(yùn)行一段時(shí)間后自動(dòng)關(guān)閉的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09SpringBoot靜態(tài)資源css,js,img配置方案
這篇文章主要介紹了SpringBoot靜態(tài)資源css,js,img配置方案,下文給大家分享了三種解決方案,需要的朋友可以參考下2017-07-07