[Advanced Java] Mybatis framework

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 asmysql-connector-java

So, pom.xmladd 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/resourcesnecessary application.propertiesconfiguration 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/resourcesdownloaded application.properties. When the project starts, Spring Boot will automatically read the relevant configuration information from this file.

During many configurations, application.propertiesthe 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.url3 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.usernamedatabasespring.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/javacan create DatabaseConnectionTestsa 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 ida field, and it is bigint unsigneda type, among which, corresponding to the type bigintin 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].longunsignedtinyintunsignedunsigned

When idthe type of is set bigintto , 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, varcharthe 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 Mapperthe last word as the name, such as the command is and BrandMapperso 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 intreturn value type to indicate the "number of affected rows". In fact, it can also be used void, 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, Listand 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 @Mapperthese @MapperScantwo annotations, you only need to choose one of them to use, which is usually recommended @MapperScan.

Note: @MapperScanWhen 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 @MapperScanwith the Spring framework @ComponentScanand 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 @Insertthe 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 namespaceattributes, 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 configured id, 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_albuminsert data into the table and require that the id of the automatic number can be obtained.

Development steps:

  • entityCreate an entity class in the package Album, the attributes in the class pms_albumare consistent with the table, and conform to the POJO specification
  • mapperCreate AlbumMapperthe interface in the package
  • AlbumMapperAdd an abstract method to the interface: ( int insert(Album album);remember to add a comment)
  • In src/main/resourcesthe mapperfolder, get AlbumMapper.xmlthe file by copying and pasting
  • AlbumMapper.xmlThe root node in the configurationnamespace
  • In AlbumMapper.xml, add nodes inside the root node <insert>, and configure id, useGeneratedKeys, keyPropertyattributes, and <insert>configure SQL statements inside the nodes
  • src/test/javaCreate a test class under the following cn.tedu.csmall.server.mapperpackage (your package name may be different) , automatically assemble objects AlbumMapperTestsin 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=?

BrandMapperAdd 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>

BrandMapperTestsWrite 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_albumthe data in the table according to the id.

Use Mybatis to modify data

pms_brandGoal: 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=?

BrandMapperAdd 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>

BrandMapperTestsWrite 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_albumGoal: 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: idDelete 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 idis indeterminate.

In BrandMapperan interface, an abstract method can be:

int deleteByIds(Long... ids); // 注意:可变参数在处理时,本质上就是数组

or:

int deleteByIds(Long[] ids);

or:

int deleteByIds(List<Long> ids);

BrandMapper.xmlConfigure 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:

  • collectionAttribute: Indicates the parameter object to be traversed. When the abstract method has only one parameter and no @Paramannotation is added, when the type of the parameter value is an array, the value of this attribute is array; when the type of the parameter value is , Listthe value of this attribute is list; otherwise, this attribute value is @Paramthe parameter value in the annotation
  • itemAttribute: 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 element
  • separatorAttribute: Indicates the separator between element values ​​during traversal

Finally, BrandMapperTestswrite 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 BrandMapperadd an abstract method to the interface:

int updateById(Brand brand);

Then, BrandMapper.xmlconfigure 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;

BrandMapperAdd 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 resultTypeor resultMap1 of these 2 properties.

When using resultTypethe 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.langyou 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 BrandDetailVOthe 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规范添加其它代码
    
}

BrandMapperAdd an abstract method to the interface :

BrandDetailVO getById(Long id);

Then, BrandMapper.xmlconfigure 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 productCountcustom alias can ensure that product_countthe 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 Listelement 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使用完全相同的属性
}

BrandMapperAdd an abstract method to the interface :

List<BrandListItemVO> list();

Then, BrandMapper.xmlconfigure 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 idwill 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_createand gmt_modifiedthe value of the field
  • Query category details by id
    • Except gmt_createand gmt_modifiedthe value of the field
  • Query the list of albums
    • Except gmt_createand gmt_modifiedthe value of the field
  • According to the parent category, query the list of child categories (condition: where parent_id=?)
    • Except gmt_createand gmt_modifiedthe value of the field

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>Node namespaceproperty value is wrong
  • Wrong abstract method name configured in XML
    • <insert>or similar nodes idhave wrong attribute values
  • The XML path specified in the configuration file is incorrectly configured
    • application.propertiesThe property configured in mybatis.mapper-locationsis 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.xmlwhether the dependent code in is correct, if it is correct, delete the local warehouse and download it again

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, BrandMapperTeststest 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, BrandMapperTeststest 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 offsetvalue is 5and countthe 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.

insert image description here

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 sqlSessionsubmitted ( 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 Ratioinformation, which represents the "hit ratio of cached data", and will be represented by similar values ​​at the end of the 0.0log 0.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 .useCachetruefalsetrue

In addition, when the second-level cache is enabled, the type that encapsulates the query result must implement Serializablethe 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 useCacheto 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 Serializableinterface, 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.propertiesConfigure the connection parameters for the database in
    • Use @MapperScanthe package where the configuration interface file is located
    • application.propertiesThe 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, Listand 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 @Paramannotations to each parameter to configure the parameter name, or encapsulate multiple parameters into a custom POJO
  • 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, idthe attribute must be configured, and the value is the name of the abstract method
    • When configuring <insert>and inserting data, you should also configure useGeneratedKeysand keyPropertyattributes 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 in resultTypeor 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 insertthe 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')
    • When counting queries, use count(*)the statistics
    • When querying table data, do not use asterisks for field lists
    • order byWhen 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

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;
    }

}

Guess you like

Origin blog.csdn.net/u013488276/article/details/126095561