HOME
About the Mybatis framework
The Mybatis framework solves problems related to database programming, mainly simplifying database programming.
When using the Mybatis framework to implement database programming, you only need to:
- An abstract method that defines data manipulation functions (this abstract method must be in the interface)
- Configure the SQL statement mapped by the above abstract method
During the implementation process of the Mybatis framework, proxy objects for each interface will be automatically generated, so developers do not need to pay attention to the implementation of the interface.
Use the Mybatis framework
In the Spring Boot project, when you need to use the Mybatis framework to implement database programming, you need to add:
mybatis-spring-boot-starter
- Database dependencies such as
mysql-connector-java
So, pom.xml
add in:
<!-- Mybatis框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- MySQL依赖项,仅运行时需要 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
After adding the above dependencies, Spring Boot will read the configuration information for connecting to the database at startup. If it is not configured, it will report an error and fail at startup. You need to add the src/main/resources
necessary application.properties
configuration in the following:
spring.datasource.url=jdbc:mysql://localhast:8888
Tip: In the above configuration, the attribute name is fixed, and the above example value is an error value. However, starting Spring Boot only loads the above configuration and does not execute the connection. Therefore, the wrong configuration value does not affect the startup project.
Configuration for connecting to the database
In the Spring Boot project, the default configuration file is src/main/resources
downloaded application.properties
. When the project starts, Spring Boot will automatically read the relevant configuration information from this file.
During many configurations, application.properties
the names of the properties that need to be configured in are fixed!
When configuring the connection information of the database, you need to configure at least spring.datasource.url
3 attributes, , , which respectively represent the URL to connect to the database, the user name for logging in to the database, and the password for logging in to the spring.datasource.username
databasespring.datasource.password
spring.datasource.url=jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
In order to check whether the configuration value is correct, you src/test/java
can create DatabaseConnectionTests
a test class under the default package, write a test method in the test class, and try to connect to the database, you can check:
package com.unkeer.csmall.server;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
// 测试类必须:
// 1. 在组件扫描的包下(默认的包及其子孙包)
// 2. 只有添加了@SpringBootTest注解才会加载整个项目的运行环境(包括Spring、读取配置等)
@SpringBootTest
public class DatabaseConnectionTests {
// Spring Boot自动读取application.properties的配置,并创建了数据源对象
@Autowired
DataSource dataSource;
// 连接数据库,可用于检查application.properties中的连接参数值是否正确
@Test
public void testConnection() throws Exception {
// 获取与MySQL的连接,此方法的调用会实质的连接数据库
dataSource.getConnection();
System.out.println("当你看到这句话时,表示在application.properties中的数据库连接参数的值是正确的");
}
}
About Design Data Sheets
about id
Alibaba's suggestion is: each table should have id
a field, and it is bigint unsigned
a type, among which, corresponding to the type bigint
in Java , it means "unsigned bit", which will prevent negative numbers from appearing in this field value, and the value range is the original positive 2 times the number... Take as an example, when there is no addition , the value range is [-128, 127], after adding, the value range is [0, 255].long
unsigned
tinyint
unsigned
unsigned
When id
the type of is set bigint
to , theoretically the id value is sufficient, even if it is not added unsigned
, the id value will not be insufficient, but it is still recommended to add, the purpose is to express semantics.
about coding
The encoding should be specified when the table is created, and it is not necessary to specify the encoding when creating the library.
It is strongly recommended to use in MySQL/MariaDB utf8mb4
.
About the field type of string
If the length of the value of a string-type field may vary greatly, varchar
the type should be used, such as username, and if the length of the value of a string-type field does not vary greatly , the type should be used char
.
Note: Some data may be pure numbers, but do not have the meaning of arithmetic operation, you should also use the string type instead of the numeric type.
When in use varchar
, the specified length must be the standard of "greater than the necessary length". For example, the current standard is "the maximum length of the user name is 15 characters", it is recommended to design a value of or greater than that, but it should not be too varchar(25)
exaggerated 25
. Avoid compromising semantics.
Abstract method defined when using Mybatis
When using Mybatis, the defined abstract methods must be in the interface. Usually, the interface will use Mapper
the last word as the name, such as the command is and BrandMapper
so on.
Regarding the declaration principles of abstract methods:
- Return value type: If the SQL to be executed is of the add, delete, or modify type, use it uniformly as the
int
return value type to indicate the "number of affected rows". In fact, it can also be usedvoid
, but it is not recommended. If the SQL to be executed is For the query type, if the query returns at most one result, you only need to ensure that the return value type can hold the required query results. If the query returns more than one result, you must use a set to encapsulate it,List
and The element type of the collection still only needs to ensure that it can hold the required query results - Method name: custom
- Methods for obtaining a single object are prefixed with get
- The method of obtaining multiple objects is prefixed with list
- The method of obtaining statistical values is prefixed with count
- The insert method is prefixed with save/insert
- The delete method is prefixed with remove/delete
- The modified method is prefixed with update
- Parameter list: If there are many parameters in the SQL statement to be executed, it is recommended to encapsulate multiple parameters into a custom class
About @Mapper and @MapperScan
The Mybatis framework only requires developers to write interfaces and abstract methods, and does not require developers to write implementation classes, because Mybatis will automatically generate interface implementation objects through proxy mode, but it needs to be clear which interfaces need to generate proxy objects.
Annotations can be added to each interface @Mapper
. When the project is started, Mybatis will scan the entire project, and a proxy object will be generated for the interface to which this annotation has been added.
You can also add annotations to the configuration class@MapperScan
to specify the package where each interface is located, then Mybatis will scan all interfaces under this package and its descendants, and generate proxy objects for these interfaces.
Regarding @Mapper
these @MapperScan
two annotations, you only need to choose one of them to use, which is usually recommended @MapperScan
.
Note: @MapperScan
When using it, it must only point to the package where the Mapper interface is located, and make sure that there are no other interfaces under this package!
Tip: The Mybatis framework has nothing to do @MapperScan
with the Spring framework @ComponentScan
and will not affect each other!
Use Mybatis to insert data
Taking the implementation of "insert brand data" as an example, the SQL statement that needs to be executed is roughly:
insert into pms_brand (name, pinyin, logo, description, keywords, sort, sales, product_count, comment_count, positive_comment_count, enable, gmt_create, gmt_modified) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
First create a class under the default package of the project pojo.entity.Brand
, and the attributes in the class should correspond to the data table:
public class Brand implements Serializable {
private Long id;
private String name;
private String pinyin;
private String logo;
private String description;
private String keywords;
private Integer sort;
private Integer sales;
private Integer productCount;
private Integer commentCount;
private Integer positiveCommentCount;
private Integer enable;
private LocalDateTime gmtCreate;
private LocalDateTime gmtModified;
// 按照POJO规范补充后续代码
}
Next, prepare the interface and abstract method, create the interface under the default package of the project mapper.BrandMapper
, and add the abstract method to the interface:
package com.unkeer.csmall.server.mapper;
public interface BrandMapper {
/**
* 插入品牌数据
* @param brand 品牌数据
* @return 受影响的行数,成功插入数据时,将返回1
*/
int insert(Brand brand);
}
Regarding the SQL statement, you can use @Insert
the etc. annotations for configuration, but it is not recommended!
It is recommended to use XML files to configure SQL statements!
About the configuration of this file:
- The root section name must be
<mapper>
- The root node must be configured with
namespace
attributes, and the value is the fully qualified name of the corresponding interface - Inside the root node, according to the type of SQL statement to be executed, use
<insert>
,<delete>
,<update>
,<select>
nodes - On
<insert>
the other nodes, the attribute must be configuredid
, and the value is the name of the abstract method (excluding brackets and parameters) - Inside
<insert>
the waiting node, configure the SQL statement, and the SQL statement does not need to end with a semicolon
For example configured as:
<?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.unkeer.csmall.server.mapper.BrandMapper">
<!-- int insert(Brand brand); -->
<insert id="insert">
insert into pms_brand (
name, pinyin, logo, description, keywords,
sort, sales, product_count, comment_count, positive_comment_count,
enable, gmt_create, gmt_modified
) values (
#{name}, #{pinyin}, #{logo}, #{description}, #{keywords},
#{sort}, #{sales}, #{productCount}, #{commentCount}, #{positiveCommentCount},
#{enable}, #{gmtCreate}, #{gmtModified}
)
</insert>
</mapper>
Finally, a configuration needs to be added to tell the Mybatis framework the location of such XML files! Add in application.properties
:
mybatis.mapper-locations=classpath:mapper/*.xml
In addition, when inserting data, it can also be configured to obtain the ID value of the automatic number. The specific method is to <insert>
add configuration to the node:
<!-- int insert(Brand brand); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
暂不关心此处的SQL语句
</insert>
practise
Goal: To pms_album
insert data into the table and require that the id of the automatic number can be obtained.
Development steps:
entity
Create an entity class in the packageAlbum
, the attributes in the classpms_album
are consistent with the table, and conform to the POJO specificationmapper
CreateAlbumMapper
the interface in the packageAlbumMapper
Add an abstract method to the interface: (int insert(Album album);
remember to add a comment)- In
src/main/resources
themapper
folder, getAlbumMapper.xml
the file by copying and pasting AlbumMapper.xml
The root node in the configurationnamespace
- In
AlbumMapper.xml
, add nodes inside the root node<insert>
, and configureid
,useGeneratedKeys
,keyProperty
attributes, and<insert>
configure SQL statements inside the nodes src/test/java
Create a test class under the followingcn.tedu.csmall.server.mapper
package (your package name may be different) , automatically assemble objectsAlbumMapperTests
in the class , and write and execute test methodsAlbumMapper
Use Mybatis to delete data
Goal: delete a brand based on id
The SQL statement that needs to be executed is roughly:
delete from pms_brand where id=?
BrandMapper
Add an abstract method to the interface :
/**
* 根据品牌id,删除品牌数据
* @param id 期望删除的品牌数据的id
* @return 受影响的行数,当删除成功时,将返回1,如果无此id对应的数据,将返回0
*/
int deleteById(Long id);
Configure SQL in BrandMapper.xml
:
<!-- int deleteById(Long id); -->
<delete id="deleteById">
delete from pms_brand where id=#{id}
</delete>
BrandMapperTests
Write and execute tests in :
@Test
public void testDeleteById() {
Long id = 1L;
int rows = mapper.deleteById(id);
System.out.println("删除完成,受影响的行数=" + rows);
}
practise
Goal: to delete pms_album
the data in the table according to the id.
Use Mybatis to modify data
pms_brand
Goal: To modify the field value of a piece of data in the table according to the id name
.
The SQL statement that needs to be executed is roughly:
update pms_brand set name=? where id=?
BrandMapper
Add an abstract method to the interface :
/**
* 根据id修改品牌的名称
* @param id 品牌id
* @param name 新的品牌名称
* @return 受影响的行数,当修改成功时,将返回1,如果无此id对应的数据,将返回0
*/
int updateNameById(@Param("id") Long id, @Param("name") String name);
Configure SQL in BrandMapper.xml
:
<!-- int updateNameById(@Param("id") Long id, @Param("name") String name); -->
<update id="updateNameById">
update pms_brand set name=#{name} where id=#{id}
</update>
BrandMapperTests
Write and execute tests in :
@Test
public void testUpdateNameById() {
Long id = 3L;
String name = "白萝卜";
int rows = mapper.updateNameById(id, name);
System.out.println("修改品牌名称完成,受影响的行数=" + rows);
}
practise
pms_album
Goal: To modify the field value of a piece of data in the table according to the id name
.
Use Mybatis to delete data in batches
In Mybatis, there is a "dynamic SQL" mechanism, which allows different SQL statements to be generated according to the parameter values passed in when the method is called.
Goal: id
Delete multiple brands at once based on several.
The SQL statement that needs to be executed is roughly:
delete from pms_brand where id=? or id=? or id=?;
or:
delete from pms_brand where id in (?, ?, ?);
Note: The number of the above SQL statement id
is indeterminate.
In BrandMapper
an interface, an abstract method can be:
int deleteByIds(Long... ids); // 注意:可变参数在处理时,本质上就是数组
or:
int deleteByIds(Long[] ids);
or:
int deleteByIds(List<Long> ids);
BrandMapper.xml
Configure the SQL statement in :
<!-- int deleteByIds(Long... ids); -->
<delete id="deleteByIds">
delete from pms_brand where id in (
<foreach collection="array" item="id" separator=",">
#{id}
</foreach>
)
</delete>
Due to the need to traverse parameters ids
(several id
), it is necessary to use nodes in dynamic SQL <foreach>
, which can traverse arrays or collections! About <foreach>
the configuration:
collection
Attribute: Indicates the parameter object to be traversed. When the abstract method has only one parameter and no@Param
annotation is added, when the type of the parameter value is an array, the value of this attribute isarray
; when the type of the parameter value is ,List
the value of this attribute islist
; otherwise, this attribute value is@Param
the parameter value in the annotationitem
Attribute: Indicates the name of the element being traversed, which is a custom name. Internally<foreach>
, when using#{}
a format placeholder, this attribute is also used to represent each elementseparator
Attribute: Indicates the separator between element values during traversal
Finally, BrandMapperTests
write and execute the tests in:
@Test
public void testDeleteByIds() {
int rows = mapper.deleteByIds(1L, 3L, 5L, 7L, 9L);
System.out.println("批量删除完成,受影响的行数=" + rows);
}
Use Mybatis to realize dynamic SQL modification data
In the dynamic SQL mechanism, <if>
tags can be used to judge the value of a certain parameter, thereby generating different SQL statement fragments, which are often used to design the operation of updating data.
Goal: Use 1 method to achieve multiple different data updates (update which fields you want to update, and the values of fields you don’t want to update will remain unchanged)
The SQL statement that needs to be executed is roughly:
update pms_brand set name=?, pinyin=?, logo=? where id=?
Note: The modified field list of the above SQL statement should not be fixed, but should be determined according to the parameter value passed in.
First BrandMapper
add an abstract method to the interface:
int updateById(Brand brand);
Then, BrandMapper.xml
configure in:
<!-- int updateById(Brand brand); -->
<update id="updateById">
UPDATE
pms_brand
<set>
<if test="name != null">
name=#{name},
</if>
<if test="pinyin != null">
pinyin=#{pinyin},
</if>
<if test="logo != null">
logo=#{logo},
</if>
</set>
WHERE
id=#{id}
</update>
It should be noted that in the dynamic SQL of Mybatis, <if>
there is no corresponding one <else>
. If you must achieve an effect similar to that in Java if...else
, you need to use <choose>
tags. The basic format is:
<choose>
<when test="条件">
满足条件时的SQL片段
</when>
<otherwise>
不满足条件时的SQL片段
</otherwise>
</choose>
Alternatively, you can also use two <if>
tags with completely opposite conditions to achieve a similar effect (but the execution efficiency is low), for example:
<if test="pinyin != null">
某代码片段
</if>
<if test="pinyin == null">
某代码片段
</if>
Use Mybatis to query data
D
Goal: Count the number of data in the brand table
The SQL statement that needs to be executed is roughly:
select count(*) from pms_brand;
BrandMapper
Add an abstract method to the interface :
int count();
Configure SQL in BrandMapper.xml
:
<!-- int count(); -->
<select id="count" resultType="int">
SELECT count(*) FROM pms_brand
</select>
Note: All query nodes ( <select>
) must be configured resultType
or resultMap
1 of these 2 properties.
When using resultType
the data type that declares the encapsulated result, the value corresponds to the return value of the abstract method. If it is a basic type, just write the type name directly. For example, if it is a reference data resultType="int"
type, java.lang
you can directly write the class name under the package. Write fully qualified names under other packages.
Single-result query with specified criteria
Goal: Query brand details based on id
The SQL statement that needs to be executed is roughly:
select id, name, pinyin ... from pms_brand where id=?
Since it is not recommended to use asterisks to represent the field list, and some fields may not need to be reflected in the query results during actual query, the recommended method is to create additional types for the required query fields to encapsulate the results.
For example pojo.vo
, create BrandDetailVO
the class below:
public class BrandDetailVO implements Serializable {
/**
* 记录id
*/
private Long id;
/**
* 品牌名称
*/
private String name;
/**
* 品牌名称的拼音
*/
private String pinyin;
/**
* 品牌logo的URL
*/
private String logo;
/**
* 品牌简介
*/
private String description;
/**
* 关键词列表,各关键词使用英文的逗号分隔
*/
private String keywords;
/**
* 自定义排序序号
*/
private Integer sort;
/**
* 销量(冗余)
*/
private Integer sales;
/**
* 商品种类数量总和(冗余)
*/
private Integer productCount;
/**
* 买家评论数量总和(冗余)
*/
private Integer commentCount;
/**
* 买家好评数量总和(冗余)
*/
private Integer positiveCommentCount;
/**
* 是否启用,1=启用,0=未启用
*/
private Integer enable;
// 参考POJO规范添加其它代码
}
BrandMapper
Add an abstract method to the interface :
BrandDetailVO getById(Long id);
Then, BrandMapper.xml
configure the SQL in:
<select id="getById" resultType="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
SELECT
id, name, pinyin, logo, description,
keywords, sort, sales, product_count, comment_count,
positive_comment_count, enable
FROM
pms_brand
WHERE
id=#{id}
</select>
In addition, when inquiring, several concepts must be clarified:
- Field (Field): The name specified when creating the data table (the name can also be changed when the table structure is subsequently modified)
- Column: For each vertical row in the query result set, the column name is the field name by default. If an alias is specified during the query, the column name is the specified alias
- Property (Property): the attribute in the class
When Mybatis executes the query, it will try to automatically encapsulate the data in the result set into the object of the returned result type. However, it can only automatically process the part where the column name is the same as the attribute name. If the column name is different from the attribute name, the default is not Cannot be automatically packaged!
In the SQL statement of the query, you can customize the alias so that the column name is the same as the attribute name, and then automatic encapsulation can be achieved, for example:
<select id="getById" resultType="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
SELECT
id, name, pinyin, logo, description,
keywords, sort, sales,
product_count AS productCount,
comment_count,
positive_comment_count, enable
FROM
pms_brand
WHERE
id=#{id}
</select>
The above product_count AS productCount
custom alias can ensure that product_count
the value of the field can be automatically encapsulated!
It is more recommended to use <resultMap>
to guide Mybatis how to encapsulate the result, it will be used with the attributes <select>
of the label , for example:resultMap
<select id="xx" resultMap="DetailResultMap">
</select>
<resultMap id="DetailResultMap" type="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
</resultMap>
Then, <resultMap>
internally, use <result>
to configure the correspondence between column names and property names, for example:
<resultMap id="DetailResultMap" type="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
<result column="product_count" property="productCount" />
<result column="comment_count" property="commentCount" />
<result column="positive_comment_count" property="positiveCommentCount" />
</resultMap>
Tip: When using <resultMap>
configuration, from a specification point of view, the relationship between each column and attribute needs to be explicitly configured (even if it may not be necessary from the point of view of function implementation), in addition, you should also use the node-to-primary <id>
key to configure.
query list
Goal: Query a list of brands
The SQL statement that needs to be executed is roughly (temporarily use asterisks to indicate the field list):
select * from pms_brand order by id
Usually, when querying a list, the fields that need to be queried may be different from those of single data, so it may be necessary to customize a new VO class as the List
element type in it, in order to avoid finding that a certain VO cannot be reused after writing the code For the two functions of querying single data and querying the list, it is recommended to define the VO class used to encapsulate the result of the list item at the beginning, for example:
public class BrandListItemVO implements Serializable {
// 可暂时与BrandDetailVO使用完全相同的属性
}
BrandMapper
Add an abstract method to the interface :
List<BrandListItemVO> list();
Then, BrandMapper.xml
configure the SQL in:
<select id="list" resultMap="ListItemResultMap">
SELECT
id, name, pinyin, logo, description,
keywords, sort, sales, product_count, comment_count,
positive_comment_count, enable
FROM
pms_brand
ORDER BY id
</select>
<resultMap id="ListItemResultMap" type="com.unkeer.csmall.server.pojo.vo.BrandListItemVO">
<id column="id" property="id" />
<result column="product_count" property="productCount" />
<result column="comment_count" property="commentCount" />
<result column="positive_comment_count" property="positiveCommentCount" />
</resultMap>
about <sql>
and<resultMap>
When using XML to configure SQL statements, you can <sql>
encapsulate SQL statement fragments and use them <include>
for reference, for example:
<select id="list" resultMap="ListItemResultMap">
SELECT
<include refid="ListItemQueryFields"/>
FROM
pms_brand
ORDER BY id
</select>
<sql id="ListItemQueryFields">
id, name, pinyin, logo, description,
keywords, sort, sales, product_count, comment_count,
positive_comment_count, enable
</sql>
By using <sql>
the encapsulated field list, it <resultMap>
usually corresponds to it, so the naming of these two nodes id
will usually use the same keyword, such as <sql>
configured as id="ListItemQueryFields"
, and <resultMap id="ListItemResultMap">
, even, when coding, these two nodes will be placed in the same adjacent location.
Tip: <sql>
When using the encapsulation field list, IntelliJ IDEA may misjudge the wrong syntax, and you can avoid such error prompts by enclosing the field list <if test="true>"
(or adding this before it).<if>
practise
Complete the following query functions:
- Count the number of albums
- Number of Statistical Categories
- Query album details by id
- Except
gmt_create
andgmt_modified
the value of the field
- Except
- Query category details by id
- Except
gmt_create
andgmt_modified
the value of the field
- Except
- Query the list of albums
- Except
gmt_create
andgmt_modified
the value of the field
- Except
- According to the parent category, query the list of child categories (condition:
where parent_id=?
)- Except
gmt_create
andgmt_modified
the value of the field
- Except
about exceptions
BindingException
Binding exception, the exception prompt information is as follows:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.unkeer.csmall.server.mapper.BrandMapper.insert
This exception occurs because the SQL statement corresponding to the abstract method cannot be found, and the reasons may be:
- Wrong interface name configured in XML
<mapper>
Nodenamespace
property value is wrong
- Wrong abstract method name configured in XML
<insert>
or similar nodesid
have wrong attribute values
- The XML path specified in the configuration file is incorrectly configured
application.properties
The property configured inmybatis.mapper-locations
is wrong
- If it is very true that there is no problem with the above code, it may only be a dependency error
- First check
pom.xml
whether the dependent code in is correct, if it is correct, delete the local warehouse and download it again
- First check
Miscellaneous: placeholders for about #{}
and formatting${}
Tip: In development practice, it is not recommended to use ${}
format placeholders.
It is assumed that it needs to be realized: Query brand data by pagination.
The SQL statement that needs to be executed is roughly:
SELECT * FROM pms_brand ORDER BY id LIMIT ?, ?
In the above SQL statement, you need to use 2 parameters, which respectively represent "skip several records" and "query several records".
Add an abstract method in BrandMapper
:
List<BrandListItemVO> listPage(
@Param("offset") Integer offset, @Param("count") Integer count);
Hint: offset means "offset".
Configure SQL in BrandMapper.xml
:
<select id="listPage" resultMap="ListItemResultMap">
SELECT
<include refid="ListItemQueryFields" />
FROM
pms_brand
ORDER BY id
LIMIT #{offset}, #{count}
</select>
Once done, BrandMapperTests
test in:
@Test
public void testListPage() {
Integer offset = 5;
Integer count = 3;
List<BrandListItemVO> list = mapper.listPage(offset, count);
System.out.println("查询列表完成,结果集中的数据的数量=" + list.size());
for (BrandListItemVO item : list) {
System.out.println(item);
}
}
After testing, it can be found that when configuring SQL, the two parameters can work normally regardless of whether they are used #{}
or ${}
not!
Assumption needs to be implemented: query brand details by name
Add an abstract method in BrandMapper
:
BrandDetailVO getByName(String name);
Configure SQL in BrandMapper.xml
:
<select id="getByName" resultMap="DetailResultMap">
SELECT
<include refid="DetailQueryFields" />
FROM
pms_brand
WHERE
name=#{name}
</select>
Once done, BrandMapperTests
test in:
@Test
public void testGetByName() {
String name = "华为";
BrandDetailVO brandDetailVO = mapper.getByName(name);
System.out.println("根据name=" + name + "查询完成,结果=" + brandDetailVO);
}
After testing, it can be found that when configuring SQL, the use of parameters #{}
can run normally, but the use ${}
will make mistakes!
In fact, the processing mechanism of #{}
these ${}
two placeholders is different!
When the placeholder is #{}
, it is precompiled, and the parameters in the SQL statement will be expressed first ?
, and after the compilation is passed, the parameter values will be substituted and executed.
When the placeholder is ${}
, it is not pre-compiled. The parameter values will be spliced into the SQL statement first, and then the compilation process will be executed. After completion, the SQL statement will be executed.
Take pagination query as an example, when used #{}
, its approximate SQL is:
SELECT * FROM pms_brand ORDER BY id LIMIT ?, ?
The above SQL will execute the compilation process first, and then substitute the parameter values into and execute the SQL statement after completion.
When using ${}
it, you need to substitute the parameter value first. The assumed offset
value is 5
and count
the value is 3
, then the SQL is roughly:
SELECT * FROM pms_brand ORDER BY id LIMIT 5, 3
Then, the above SQL statement will be compiled, and finally the SQL statement will be executed.
When switching to query based on the name, when #{}
the placeholder is used, the SQL statement is roughly:
SELECT * FROM pms_brand WHERE name=?
When using ${}
a placeholder, the parameter value is still used to replace the placeholder first, and the resulting SQL statement is roughly:
SELECT * FROM pms_brand WHERE name=华为
Since 华为
these two words are not quoted to indicate that they are a string, they will be regarded as field names, and the final execution error is:
Cause: java.sql.SQLSyntaxErrorException: Unknown column '华为' in 'where clause'
Therefore, when using ${}
format placeholders and the parameter value is a string type (in fact, other non-numeric types are the same), you need to consider the data type. For example, when the above error occurs, change the test 华为
data '华为'
to Can.
Conclusion: When using ${}
format placeholders, you need to consider the data type issue yourself, but when using it #{}
, you don't need it.
In addition, if you do not use precompilation, because the parameter value can change the semantics, there is still a risk of SQL injection!
Therefore, in order to ensure that the SQL statement will not be injected, ${}
the format placeholder should not be used!
Tip: Even if ${}
the placeholder in the format has the risk of SQL injection, it is not unsolvable. You can use regular expressions to check the parameter value before executing the SQL statement. If there are keywords for SQL injection, do not Just execute the SQL statement.
Mybatis's cache mechanism
Cache: It is a mechanism for temporarily storing data, and even the data stored using this mechanism is only for temporary use.
Usually, when using cache to store data, it will be faster than some other processing mechanism (accessing data is faster and more efficient)!
When using Mybatis to implement data access for adding, deleting, modifying and checking, in essence, the program is running on the APP server, and the database is usually on another server, so the efficiency of accessing data is very low (need to be performed between two computers) In addition, it can also process SQL statements, query results, etc.), especially for data tables with a lot of query data. In the absence of mechanisms such as indexes, each query takes a very long time!
Usually, cache is used to solve the problem of low query efficiency (it has nothing to do with addition, deletion, and modification). The built-in cache mechanism of Mybatis is to temporarily store the query results on the APP server. The results can be returned, and the data in the MySQL database server will not be actually queried.
When Mybatis handles cache, it is divided into first-level cache and second-level cache (two different cache mechanisms, but they can exist at the same time).
The first-level cache of Mybatis is also called "session cache". It is enabled by default and cannot be turned off . The conditions for it to cache data are:
- must be the same session
- Must be a query executed through an object of the same Mapper interface
- Must be to execute exactly the same SQL statement
- Must be the exact same SQL parameter
When the above conditions are met, each data queried will be cached by Mybatis, and the next time this data is queried, the previously cached results will be returned directly.
The first-level cache will automatically clear the cache data when the Session is closed, the cache is cleared through the SqlSession, and the data in the table is modified through the Mapper of this session!
Mybatis's second-level cache is also called "namespace cache", which is a cache that acts on each XML configuration, that is, as long as the same query in the same XML is executed and the parameters are the same, even if different sessions are used , You can also share cached data!
In the Mybatis project integrating Spring (including Spring Boot), the second-level cache is enabled globally by default, but each namespace is not enabled by default. If you need to enable each namespace cache, you need to add a node in the XML file (this node directly belongs to the root <cache/>
node , no distinction is made between the order of other nodes at the same level).
Note: The data in the second-level cache must be sqlSession
submitted ( commit
) or closed ( close
) before it will be generated.
Note: When Mybatis executes a query, it will first look for the second-level cache. If it hits, it returns the result directly. If it misses, it queries the first-level cache. If it hits, it returns the result in the first-level cache. If it still misses, it executes The actual query (connecting to the database server to query data).
Tip: After using the second-level cache, the log in the output will contain
Cache Hit Ratio
information, which represents the "hit ratio of cached data", and will be represented by similar values at the end of the0.0
log0.5
.
In addition, an attribute <select>
can also be configured on each query , the value is / , indicating whether the query uses the cache, and the default value is .useCache
true
false
true
In addition, when the second-level cache is enabled, the type that encapsulates the query result must implement Serializable
the interface, otherwise an unserializable exception will occur!
The second-level cache will also clear the cache data due to the addition, deletion, and modification operations in the current namespace!
【Summarize】
The built-in caching mechanism of Mybatis has two types: first-level cache and second-level cache, and when executing a query, it will first look for the second-level cache and then the first-level cache.
Among them, the first-level cache is a session cache, which must be the same session, the same Mapper, execute the same SQL, and use the same SQL parameters to apply the cache, and the first-level cache will be actively cleared in the session because the session is closed Cache, the Mapper that uses this session automatically clears the cached data after any write operation is performed. The first-level cache is enabled by default, which is uncontrollable to a certain extent; the second-level cache is a Namespace cache, and the cached data can be used even in different sessions , the default is the state that is enabled globally and not enabled for each Namespace. When you need to use the Namespace cache, you need to add a node in the XML file <cache/>
to enable the second-level cache of the current Namespace. In addition, you can also <select>
configure attributes on each node useCache
to configure a certain Whether a query function uses the second-level cache, and when using the second-level cache, the type that needs to encapsulate the result implements the Serializable
interface, and the second-level cache will also be cleared due to writing data.
Since no matter whether it is the first-level cache or the second-level cache, the cached data will be cleared after the data is written to ensure the accuracy of the cached data. However, this mechanism does not necessarily meet the application requirements in development practice. In practice, it may not be necessary to pay attention to the accuracy of each data all the time. For example, the mechanism of Weibo hot search and Toutiao hot list is to update the cache data every 10 minutes or 15 minutes. The cache mechanism of Mybatis is This cannot be done, so the caching approach of custom strategies is usually used, such as using Redis to handle caching.
MyBatisSummary
Regarding the use of Mybatis, you need:
- Understand the dependencies that need to be added when using Mybatis
mybatis-spring-boot-starter
mysql-connector-java
- Understand the one-time configuration when using Mybatis (only do it once in each project)
application.properties
Configure the connection parameters for the database in- Use
@MapperScan
the package where the configuration interface file is located application.properties
The location of the configuration XML file is in
- Master the principles of declaring abstract methods
- Return value type: If the SQL is of the type of addition, deletion, and modification, it will be returned
int
. If the SQL is of the query type, when the query result is a single piece of data, it is only necessary to ensure that the result is enough to encapsulate the result. If the query result is multiple pieces of data, use,List
and The element type is still guaranteed to be sufficient to encapsulate the result (each piece of data) - Method name: refer to Ali's suggestion, do not use overloading
- Parameter list: Analyze the SQL statement that needs to be executed in advance (or draft it in advance). The parameters in the SQL statement will be the parameters of the abstract method. If there is only one parameter, the method parameter list will specify the corresponding parameter. If the parameter exceeds 1, can be declared as multiple parameters, add
@Param
annotations to each parameter to configure the parameter name, or encapsulate multiple parameters into a custom POJO
- Return value type: If the SQL is of the type of addition, deletion, and modification, it will be returned
- Master the SQL for configuring the mapping of each abstract method in XML
- This type of XML file should have a fixed declaration (the top 2 sentences), so this type of XML is usually obtained by copying and pasting, or using tools to create and generate
- The corresponding interface must be specified in the root
<mapper>
nodenamespace
<insert>
One of the four nodes should be selected according to the SQL statement to be executed- In
<insert>
the four types of nodes,id
the attribute must be configured, and the value is the name of the abstract method - When configuring
<insert>
and inserting data, you should also configureuseGeneratedKeys
andkeyProperty
attributes to obtain the id of the automatic number (if the id of this table is not an automatic number, then do not configure it) - In
<select>
the node, one of the properties inresultType
or must be configuredresultMap
- Use
<sql>
nodes to encapsulate SQL statement fragments. When you need to use this SQL statement, you can use<include>
nodes to reference - Using
<resultMap>
nodes can guide Mybatis to encapsulate query results, for example, you can configure the correspondence between columns and attributes - Master dynamic SQL
<foreach>
, usually used to achieve batch deletion, batch insertion, etc. - Master dynamic SQL
<if>
, usually combined with<set>
processing update data
- Master standard SQL statements
- In
insert
the statement, the list of fields should be explicitly specified- Positive example:
insert into user (username, password) values ('root', '1234')
- Counter example:
insert into user values (null, 'root', '1234')
- Positive example:
- When counting queries, use
count(*)
the statistics - When querying table data, do not use asterisks for field lists
order by
When there may be more than one query result, the collation of the specified result set must be explicitly used- When there may be more than one query result, you should consider whether pagination is required
- In
Supplement: Automatically update the Mybatis interceptor of gmt_create and gmt_modified
MybatisConfiguration.java
// 补充
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addInterceptor() {
Interceptor interceptor = new InsertUpdateTimeInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
InsertUpdateTimeInterceptor.java
package com.unkeer.mall.product.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>基于MyBatis的自动更新"最后修改时间"的拦截器</p>
*
* <p>需要SQL语法预编译之前进行拦截,则拦截类型为StatementHandler,拦截方法是prepare</p>
*
* <p>具体的拦截处理由内部的intercept()方法实现</p>
*
* <p>注意:由于仅适用于当前项目,并不具备范用性,所以:</p>
*
* <li>拦截所有的update方法(根据SQL语句以update前缀进行判定),无法不拦截某些update方法</li>
* <li>所有数据表中"最后修改时间"的字段名必须一致,由本拦截器的FIELD_MODIFIED属性进行设置</li>
*
* @see cn.tedu.mall.product.config.InsertUpdateTimeInterceptorConfiguration
*/
@Slf4j
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {
Connection.class, Integer.class}
)})
public class InsertUpdateTimeInterceptor implements Interceptor {
/**
* 自动添加的创建时间字段
*/
private static final String FIELD_CREATE = "gmt_create";
/**
* 自动更新时间的字段
*/
private static final String FIELD_MODIFIED = "gmt_modified";
/**
* SQL语句类型:其它(暂无实际用途)
*/
private static final int SQL_TYPE_OTHER = 0;
/**
* SQL语句类型:INSERT
*/
private static final int SQL_TYPE_INSERT = 1;
/**
* SQL语句类型:UPDATE
*/
private static final int SQL_TYPE_UPDATE = 2;
/**
* 查找SQL类型的正则表达式:INSERT
*/
private static final String SQL_TYPE_PATTERN_INSERT = "^insert\\s";
/**
* 查找SQL类型的正则表达式:UPDATE
*/
private static final String SQL_TYPE_PATTERN_UPDATE = "^update\\s";
/**
* 查询SQL语句片段的正则表达式:gmt_modified片段
*/
private static final String SQL_STATEMENT_PATTERN_MODIFIED = ",\\s*" + FIELD_MODIFIED + "\\s*=";
/**
* 查询SQL语句片段的正则表达式:gmt_create片段
*/
private static final String SQL_STATEMENT_PATTERN_CREATE = ",\\s*" + FIELD_CREATE + "\\s*[,)]?";
/**
* 查询SQL语句片段的正则表达式:WHERE子句
*/
private static final String SQL_STATEMENT_PATTERN_WHERE = "\\s+where\\s+";
/**
* 查询SQL语句片段的正则表达式:VALUES子句
*/
private static final String SQL_STATEMENT_PATTERN_VALUES = "\\)\\s*values?\\s*\\(";
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 检查方法的注解,如果方法包含 @IgnoreGmtTimeField 则不进行拦截
// if(invocation.getMethod().getDeclaredAnnotation(IgnoreGmtTimeField.class)!=null){
// return invocation.proceed();
// }
// 日志
log.debug("准备拦截SQL语句……");
// 获取boundSql,即:封装了即将执行的SQL语句及相关数据的对象
BoundSql boundSql = getBoundSql(invocation);
// 从boundSql中获取SQL语句
String sql = getSql(boundSql);
// 日志
log.debug("原SQL语句:{}", sql);
// 准备新SQL语句
String newSql = null;
// 判断原SQL类型
switch (getOriginalSqlType(sql)) {
case SQL_TYPE_INSERT:
// 日志
log.debug("原SQL语句是【INSERT】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendCreateTimeField(sql, LocalDateTime.now());
break;
case SQL_TYPE_UPDATE:
// 日志
log.debug("原SQL语句是【UPDATE】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendModifiedTimeField(sql, LocalDateTime.now());
break;
}
// 应用新SQL
if (newSql != null) {
// 日志
log.debug("新SQL语句:{}", newSql);
reflectAttributeValue(boundSql, "sql", newSql);
}
// 执行调用,即拦截器放行,执行后续部分
return invocation.proceed();
}
public String appendModifiedTimeField(String sqlStatement, LocalDateTime dateTime) {
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_MODIFIED, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("原SQL语句中已经包含gmt_modified,将不补充添加时间字段");
return null;
}
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern whereClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_WHERE, Pattern.CASE_INSENSITIVE);
Matcher whereClauseMatcher = whereClausePattern.matcher(sql);
// 查找 where 子句的位置
if (whereClauseMatcher.find()) {
int start = whereClauseMatcher.start();
int end = whereClauseMatcher.end();
String clause = whereClauseMatcher.group();
log.debug("在原SQL语句 {} 到 {} 找到 {}", start, end, clause);
String newSetClause = ", " + FIELD_MODIFIED + "='" + dateTime + "'";
sql.insert(start, newSetClause);
log.debug("在原SQL语句 {} 插入 {}", start, newSetClause);
log.debug("生成SQL: {}", sql);
return sql.toString();
}
return null;
}
public String appendCreateTimeField(String sqlStatement, LocalDateTime dateTime) {
// 如果 SQL 中已经包含 gmt_create 就不在添加这两个字段了
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_CREATE, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("已经包含 gmt_create 不再添加 时间字段");
return null;
}
// INSERT into table (xx, xx, xx ) values (?,?,?)
// 查找 ) values ( 的位置
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern valuesClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_VALUES, Pattern.CASE_INSENSITIVE);
Matcher valuesClauseMatcher = valuesClausePattern.matcher(sql);
// 查找 ") values " 的位置
if (valuesClauseMatcher.find()) {
int start = valuesClauseMatcher.start();
int end = valuesClauseMatcher.end();
String str = valuesClauseMatcher.group();
log.debug("找到value字符串:{} 的位置 {}, {}", str, start, end);
// 插入字段列表
String fieldNames = ", " + FIELD_CREATE + ", " + FIELD_MODIFIED;
sql.insert(start, fieldNames);
log.debug("插入字段列表{}", fieldNames);
// 定义查找参数值位置的 正则表达 “)”
Pattern paramPositionPattern = Pattern.compile("\\)");
Matcher paramPositionMatcher = paramPositionPattern.matcher(sql);
// 从 ) values ( 的后面位置 end 开始查找 结束括号的位置
String param = ", '" + dateTime + "', '" + dateTime + "'";
int position = end + fieldNames.length();
while (paramPositionMatcher.find(position)) {
start = paramPositionMatcher.start();
end = paramPositionMatcher.end();
str = paramPositionMatcher.group();
log.debug("找到参数值插入位置 {}, {}, {}", str, start, end);
sql.insert(start, param);
log.debug("在 {} 插入参数值 {}", start, param);
position = end + param.length();
}
if (position == end) {
log.warn("没有找到插入数据的位置!");
return null;
}
} else {
log.warn("没有找到 ) values (");
return null;
}
log.debug("生成SQL: {}", sql);
return sql.toString();
}
@Override
public Object plugin(Object target) {
// 本方法的代码是相对固定的
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
// 无须执行操作
}
/**
* <p>获取BoundSql对象,此部分代码相对固定</p>
*
* <p>注意:根据拦截类型不同,获取BoundSql的步骤并不相同,此处并未穷举所有方式!</p>
*
* @param invocation 调用对象
* @return 绑定SQL的对象
*/
private BoundSql getBoundSql(Invocation invocation) {
Object invocationTarget = invocation.getTarget();
if (invocationTarget instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) invocationTarget;
return statementHandler.getBoundSql();
} else {
throw new RuntimeException("获取StatementHandler失败!请检查拦截器配置!");
}
}
/**
* 从BoundSql对象中获取SQL语句
*
* @param boundSql BoundSql对象
* @return 将BoundSql对象中封装的SQL语句进行转换小写、去除多余空白后的SQL语句
*/
private String getSql(BoundSql boundSql) {
return boundSql.getSql().toLowerCase().replaceAll("\\s+", " ").trim();
}
/**
* <p>通过反射,设置某个对象的某个属性的值</p>
*
* @param object 需要设置值的对象
* @param attributeName 需要设置值的属性名称
* @param attributeValue 新的值
* @throws NoSuchFieldException 无此字段异常
* @throws IllegalAccessException 非法访问异常
*/
private void reflectAttributeValue(Object object, String attributeName, String attributeValue) throws NoSuchFieldException, IllegalAccessException {
Field field = object.getClass().getDeclaredField(attributeName);
field.setAccessible(true);
field.set(object, attributeValue);
}
/**
* 获取原SQL语句类型
*
* @param sql 原SQL语句
* @return SQL语句类型
*/
private int getOriginalSqlType(String sql) {
Pattern pattern;
pattern = Pattern.compile(SQL_TYPE_PATTERN_INSERT, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_INSERT;
}
pattern = Pattern.compile(SQL_TYPE_PATTERN_UPDATE, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_UPDATE;
}
return SQL_TYPE_OTHER;
}
}