MyBatis 是一款优秀的持久层框架, оно поддерживает кастомизацию SQL, хранимых процедур и高级映射,而在实际开发中,我们通常会选择使用 MyBatisPlus,它是对 MyBatis 框架的进一步增强,能够极大地简化我们的持久层代码。接下来,我们将一起看看 MyBatisPlus 中的一些巧妙技巧。
说明:本文需要一定的 MyBatis 和 MyBatisPlus 基础知识。
MyBatis-Plus 官网地址 : https://baomidou.com/ 。
使用 MyBatisPlus 实现业务的增删改查非常简单,让我们一起来看看吧。
1. 首先新建一个 SpringBoot 工程,然后引入依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2. 配置数据源:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
url: jdbc:mysql:///mybatisplus?serverTimezone=UTC
password: 123456
3. 创建数据表:
CREATE DATABASE `mybatisplus`;
USE `mybatisplus`;
DROP TABLE IF EXISTS `tbl_employee`;
CREATE TABLE `tbl_employee` (
`id` bigint(20) NOT NULL,
`last_name` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`gender` char(1) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=gbk;
insert into `tbl_employee`(`id`,`last_name`,`email`,`gender`,`age`) values (1,'jack','jack@qq.com','1',35),(2,'tom','tom@qq.com','1',30),(3,'jerry','jerry@qq.com','1',40);
4. 创建对应的实体类:
@Data
public class Employee {
private Long id;
private String lastName;
private String email;
private Integer age;
}
5. 编写 Mapper 接口:
public interface EmployeeMapper extends BaseMapper<Employee> {
}
我们只需继承 MyBatisPlus 提供的 BaseMapper 接口即可,现在我们就拥有了对 Employee 进行增删改查的 API,比如:java @SpringBootTest @MapperScan("com.wwj.mybatisplusdemo.mapper") class MybatisplusDemoApplicationTests {
markdown
@Autowired
private EmployeeMapper employeeMapper;
@Test void contextLoads() { List employees = employeeMapper.selectList(null); employees.forEach(System.out::println); } }
Результат выполнения:
```java
org.springframework.jdbc.BadSqlGrammarException:
### Ошибка при выполнении запроса к базе данных. Причина: java.sql.SQLSyntaxErrorException: Таблица 'mybatisplus.employee' не существует
Программа выдала ошибку, причина которой заключается в отсутствии таблицы employee
. Это происходит потому, что наша сущность названа Employee
, и MyBatisPlus
по умолчанию использует имя класса в качестве имени таблицы. Однако, если имя класса и имя таблицы не совпадают (что действительно может быть в реальном проекте), необходимо использовать аннотацию @TableName
в классе сущности для указания имени таблицы:
@Data
@TableName("tbl_employee") // Указание имени таблицы
public class Employee {
private Long id;
private String lastName;
private String email;
private Integer age;
}
Повторное выполнение тестового кода даст следующий результат:
Employee(id=1, lastName=jack, email=jack@qq.com, age=35)
Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40)
BaseMapper
предоставляет набор стандартных методов для выполнения операций CRUD:
Детали можно изучить, изучив исходный код, который содержит комментарии на русском языке, что делает его легко понятным.В процессе разработки мы обычно используем слой Service
для вызова методов слоя Mapper
. MyBatisPlus
также предоставляет универсальный слой Service
:
public interface EmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
В действительности, нам достаточно, чтобы EmployeeServiceImpl
наследовал ServiceImpl
, чтобы получить методы слоя Service
. Почему же нам нужно реализовывать интерфейс EmployeeService
?
Это потому, что реализация интерфейса EmployeeService
позволяет нам удобно расширять бизнес-функциональность. В некоторых сложных сценариях обработки данных методы, предоставляемые MyBatisPlus
, могут быть недостаточными. В этом случае нам нужно будет самостоятельно писать код. Для этого достаточно определить свои методы в интерфейсе EmployeeService
и реализовать их в классе EmployeeServiceImpl
.
Давайте сначала протестируем методы, предоставляемые MyBatisPlus
:
public interface EmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
@SpringBootTest
@MapperScan("com.wwj.mybatisplusdemo.mapper")
class MybatisplusDemoApplicationTests {
@Autowired
private EmployeeService employeeService;
@Test
void contextLoads() {
List<Employee> list = employeeService.list();
list.forEach(System.out::println);
}
}
Результат выполнения:
Employee(id=1, lastName=jack, email=jack@qq.com, age=35)
Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40)
```Для создания пользовательского сценария нам нужно объявить методы в интерфейсе `EmployeeMapper`:
```java
public interface EmployeeMapper extends BaseMapper<Employee> {
List<Employee> selectAllByLastName(@Param("lastName") String lastName);
}
Теперь нам нужно создать конфигурационный файл для реализации этого метода. В папке resources
создайте папку mapper
, а затем создайте файл EmployeeMapper.xml
:
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wwj.mybatisplusdemo.mapper.EmployeeMapper">
<sql id="Base_Column">
id, last_name, email, gender, age
</sql>
<select id="selectAllByLastName" resultType="com.wwj.mybatisplusdemo.bean.Employee">
select <include refid="Base_Column"/>
from tbl_employee
where last_name = #{lastName}
</select>
</mapper>
MyBatisPlus
по умолчанию просматривает файлы маппера в директории mapper
классового пути. Это можно увидеть в исходном коде:
Поэтому, если файлы маппера находятся в этой директории, никаких дополнительных настроек не требуется. Если же они находятся в другой директории, нужно будет настроить их расположение, например:
mybatis-plus:
mapper-locations: classpath:xml/*.xml
После объявления методов в интерфейсе Mapper
, нам нужно определить методы в интерфейсе Service
:
public interface EmployeeService extends IService<Employee> {
List<Employee> listAllByLastName(String lastName);
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
``` @Override
public List<Employee> listAllByLastName(String lastName) {
return baseMapper.selectAllByLastName(lastName);
}
}
```В `EmployeeServiceImpl` нам не нужно внедрять `EmployeeMapper`, а использовать `BaseMapper`. Просмотрите исходный код `ServiceImpl`:```
Как видно, он внедряет объект `BaseMapper`, который является первым типом генерика, то есть типом `EmployeeMapper`. Поэтому мы можем использовать этот `baseMapper` для вызова методов `Mapper`. В этом случае напишем тестовый код:
```java
@SpringBootTest
@MapperScan("com.wwj.mybatisplusdemo.mapper")
class MybatisplusDemoApplicationTests {
@Autowired
private EmployeeService employeeService;
@Test
void contextLoads() {
List<Employee> list = employeeService.listAllByLastName("tom");
list.forEach(System.out::println);
}
}
Результат выполнения:
Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
При создании таблицы я намеренно не указал стратегию автоинкремента для первичного ключа. Теперь мы вставим одну запись и посмотрим, как будет увеличиваться первичный ключ:
@Test
void contextLoads() {
Employee employee = new Employee();
employee.setLastName("lisa");
employee.setEmail("lisa@qq.com");
employee.setAge(20);
employeeService.save(employee);
}
После успешной вставки данных запросим таблицу:
mysql> select * from tbl_employee;
+---------------------+-----------+--------------+--------+------+
| id | last_name | email | gender | age |
+---------------------+-----------+--------------+--------+------+
| 1 | jack | jack@qq.com | 1 | 35 |
| 2 | tom | tom@qq.com | 1 | 30 |
| 3 | jerry | jerry@qq.com | 1 | 40 |
| 1385934720849584129 | lisa | lisa@qq.com | NULL | 20 |
+---------------------+--------------+--------------+--------+------+
4 строки (0.00 сек)
Как видно, значение id
— это довольно длинное число. Это что означает? Скажу наперед, это распределённый ID. А что такое распределённый ID?Мы знаем, что для крупного приложения объём входящих запросов очень велик. Например, на сайте каждый день регистрируются новые пользователи, информация о которых должна быть сохранена в таблице. По мере накопления данных скорость запросов к базе данных может уменьшаться. Поэтому, как правило, когда объём данных становится достаточно большим, данные разделяют на несколько баз и таблиц. Однако, как только таблица разбивается на несколько, возникает проблема. Очевидно, что данные этих разделенных таблиц принадлежат одной и той же таблице, просто из-за большого объема данных они разделяются на несколько таблиц. Тогда как управлять первичным ключом ID этих таблиц? Каждая таблица будет иметь свой собственный ID? В этом случае данные будут иметь множество повторяющихся ID, что, конечно, недопустимо. На самом деле, мы можем использовать алгоритм для генерации уникального ID, который никогда не повторяется, и таким образом проблема будет решена. В действительности, существует множество решений для распределенных ID:
В качестве примера возьмем UUID, который генерирует строку, состоящую из цифр и букв, что явно не подходит для использования в качестве ID таблицы данных. Кроме того, поддержание ID в порядке возрастания ускоряет скорость запросов к таблице. Исходя из этого, MyBatisPlus
использует SnowFlake
(снежный алгоритм).SnowFlake
— это распределенный алгоритм генерации идентификаторов, открытый Twitter. SnowFlake
состоит из 64-битного двоичного числа, которое разделено на несколько частей, каждая из которых имеет определенное значение:
Это и есть причина, почему после вставки данных новый идентификатор данных представляет собой длинную строку чисел. Мы можем использовать @TableId
в классе сущности для установки стратегии первичного ключа:```java
@Data
@TableName("tbl_employee")
public class Employee {
@TableId(type = IdType.AUTO) // Установка стратегии первичного ключа
private Long id;
private String lastName;
private String email;
private Integer age;
}
`MyBatisPlus` предоставляет несколько стратегий первичного ключа:

где `AUTO` означает стратегию автоинкремента базы данных, при которой требуется, чтобы база данных поддерживала автоинкремент (auto_increment), `ASSIGN_ID` — это стратегия снежного алгоритма, которая по умолчанию используется, `ASSIGN_UUID` — это стратегия UUID, которая обычно не используется. Вот подробное описание, когда имя первичного ключа в сущностях и таблицах базы данных совпадает с именем `id`. В этом случае `MyBatisPlus` автоматически определяет это поле как первичный ключ. Если имя поля не `id`, необходимо использовать аннотацию `@TableId`. Если имена первичных ключей в сущности и таблице не совпадают, можно указать это явно:
```java
@TableId(value = "uid", type = IdType.AUTO) // Установка стратегии первичного ключа
private Long id;
Также можно настроить глобальную стратегию первичного ключа в конфигурационном файле:
mybatis-plus:
global-config:
db-config:
id-type: auto
Это позволяет избежать повторного задания стратегии первичного ключа для каждого класса сущности.
Для каждой таблицы должны быть определены три поля:-
id
: уникальный идентификатор
gmt_create
: время создания записиgmt_modified
: время последнего обновления записиМы можем изменить структуру таблицы следующим образом:
ALTER TABLE tbl_employee ADD COLUMN gmt_create DATETIME NOT NULL;
ALTER TABLE tbl_employee ADD COLUMN gmt_modified DATETIME NOT NULL;
Затем изменяем класс сущности:
@Data
@TableName("tbl_employee")
public class Employee {
@TableId(type = IdType.AUTO) // Установка стратегии первичного ключа
private Long id;
private String lastName;
private String email;
private Integer age;
private LocalDateTime gmtCreate;
private LocalDateTime gmtModified;
}
Теперь при вставке и обновлении данных нам нужно будет вручную управлять этими двумя полями:
@Test
void contextLoads() {
Employee employee = new Employee();
employee.setLastName("lisa");
employee.setEmail("lisa@qq.com");
employee.setAge(20);
// Установка времени создания
employee.setGmtCreate(LocalDateTime.now());
employee.setGmtModified(LocalDateTime.now());
employeeService.save(employee);
}
@Test
void contextLoads() {
Employee employee = new Employee();
employee.setId(1385934720849584130L);
employee.setAge(50);
// Установка времени последнего обновления
employee.setGmtModified(LocalDateTime.now());
employeeService.updateById(employee);
}
Ручное управление этими полями может быть неудобным, но MyBatisPlus
предоставляет функцию автоматического заполнения полей для упрощения этого процесса. Для этого используется аннотация @TableField
.```java
@Data
@TableName("tbl_employee")
public class Employee { @TableId(type = IdType.AUTO)
private Long id;
private String lastName;
private String email;
private Integer age;
@TableField(fill = FieldFill.INSERT) // Automatic filling upon insertion
private LocalDateTime gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE) // Automatic filling upon insertion and update
private LocalDateTime gmtModified;
}Затем создайте класс, который реализует интерфейс MetaObjectHandler
:
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* Реализация автоматического заполнения при вставке
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("Начало заполнения атрибутов при вставке");
this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now());
}
/**
* Реализация автоматического заполнения при обновлении
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("Начало заполнения атрибутов при обновлении");
this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now());
}
}
В этом интерфейсе есть два не реализованных метода, которые используются для заполнения атрибутов при вставке и обновлении. В этих методах вызывается метод strictInsertFill()
, который принимает четыре параметра:
metaObject
: метаобъект, входной параметр методаfieldName
: атрибут, который нужно заполнитьfieldType
: тип атрибутаfieldVal
: значение, которое нужно заполнитьТеперь, перед вставкой и обновлением данных, эти методы будут выполнены для автоматического заполнения атрибутов. С помощью логов можно проверить выполнение:
@Test
void contextLoads() {
Employee employee = new Employee();
employee.setId(1385934720849584130L);
employee.setAge(15);
employeeService.updateById(employee);
}
``````java
INFO 15584 --- [ main] c.w.m.handler.MyMetaObjectHandler : Начало заполнения атрибутов при обновлении
2021-04-24 21:32:19.788 INFO 15584 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-04-24 21:32:21.244 INFO 15584 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
```Оптимизация заполнения атрибутов может включать учет некоторых специальных случаев. Для некоторых отсутствующих атрибутов не требуется их заполнение, а для тех, которые уже имеют значение, также не требуется заполнение. Это повышает общую производительность программы:
```java
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
boolean hasGmtCreate = metaObject.hasSetter("gmtCreate");
boolean hasGmtModified = metaObject.hasSetter("gmtModified");
if (hasGmtCreate) {
Object gmtCreate = this.getFieldValByName("gmtCreate", metaObject);
if (gmtCreate == null) {
this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime.class, LocalDateTime.now());
}
}
if (hasGmtModified) {
Object gmtModified = this.getFieldValByName("gmtModified", metaObject);
if (gmtModified == null) {
this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now());
}
}
}
@Override
public void updateFill(MetaObject metaObject) {
boolean hasGmtModified = metaObject.hasSetter("gmtModified");
if (hasGmtModified) {
Object gmtModified = this.getFieldValByName("gmtModified", metaObject);
if (gmtModified == null) {
this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now());
}
}
}
}
```## Логическое удаление
Логическое удаление противопоставляется физическому удалению. Рассмотрим эти два понятия:
1. **Физическое удаление** : это реальное удаление, при котором данные из таблицы удаляются, и после этого данные больше не могут быть найдены.
1. **Логическое удаление** : это не реальное удаление, а скрытие данных для пользователя. Данные остаются в таблице, но становятся недоступными для пользователя.
В эпоху, когда данные являются ценнее всего, они представляют собой ценность. Поэтому обычно системы не удаляют важные данные полностью, а используют флаг состояния в базе данных. Этот флаг по умолчанию установлен в OnClickListener, что делает данные видимыми для пользователя. При выполнении операции удаления флаг устанавливается в 1, что делает данные невидимыми для пользователя, но они все еще остаются в таблице.
В соответствии с рекомендациями раздела 5 "MySQL база данных" из "Алибаба Java разработческого руководства", добавим поле `is_deleted` в таблицу данных:
```sql
alter table tbl_employee add column is_deleted tinyint not null;
Также следует добавить это поле в сущностный класс:
@Data
@TableName("tbl_employee")
public class Employee {
``` @TableId(type = IdType.AUTO)
private Long id;
private String lastName;
private String email;
private Integer age;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime gmtModified;
/**
* Логическое поле для удаления
*/
@TableLogic
@TableField(value = "is_deleted")
private Boolean deleted;
}
Ссылаясь на рекомендации раздела 5 "MySQL базы данных" из "Алибаба Java разработческого руководства", для булевых переменных не следует добавлять префикс is, поэтому наше поле было названо `deleted`, но теперь оно не соответствует полю в таблице данных. Поэтому мы используем аннотацию `@TableField` для объявления имени поля в таблице, а аннотация `@TableLogic` используется для настройки логического удаления. В этом случае, когда мы выполняем операцию удаления:
```java
@Test
void contextLoads() {
employeeService.removeById(3);
}
Проверим таблицу данных:
mysql> select * from tbl_employee;
+---------------------+-----------+--------------+--------+------+---------------------+---------------------+----------+
| id | last_name | email | gender | age | gmt_create | gmt_modified | deleted |
+---------------------+-----------+--------------+--------+------+---------------------+---------------------+----------+
| 1 | jack | jack@qq.com | 1 | 35 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 |
| 2 | tom | tom@qq.com | 1 | 30 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 |
| 3 | jerry | jerry@qq.com | 1 | 40 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 1 |
| 1385934720849584129 | lisa | lisa@qq.com | NULL | 20 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 |
| 1385934720849584130 | lisa | lisa@qq.com | NULL | 15 | 2021-04-24 21:14:18 | 2021-04-24 21:32:19 | 0 |
+---------------------+-----------+--------------+--------+------+---------------------+---------------------+----------+
5 строк в наборе (0.00 сек)
Можно заметить, что данные не были удалены, а только значение поля deleted
было обновлено до 1. Давайте снова выполним запрос:```java
@Test
void contextLoads() {
List list = employeeService.list();
list.forEach(System.out::println);
}
Результат выполнения:
```java
Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
Employee(id=1385934720849584129, lastName=lisa, email=lisa@qq.com, age=20, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
Employee(id=1385934720849584130, lastName=lisa, email=lisa@qq.com, age=15, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:32:19, deleted=false)
Обратите внимание, что третье значение не было возвращено. Как это реализовано? Мы можем вывести SQL-запрос, сгенерированный MyBatisPlus, для анализа. В конфигурационном файле добавьте следующие параметры:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # Выводить SQL-запросы
Результат выполнения:
==> Preparing: SELECT id, last_name, email, age, gmt_create, gmt_modified, is_deleted AS deleted FROM tbl_employee WHERE is_deleted = 0
==> Parameters:
<== Columns: id, last_name, email, age, gmt_create, gmt_modified, deleted
<== Row: 1, jack, jack@qq.com, 35, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0
<== Row: 2, tom, tom@qq.com, 30, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0
<== Row: 1385934720849584129, lisa, lisa@qq.com, 20, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0
<== Row: 1385934720849584130, lisa, lisa@qq.com, 15, 2021-04-24 21:14:18, 2021-04-24 21:32:19, 0
<== Total: 4
Оказывается, что при выполнении запроса используется условие is_deleted = 0
. Это подтверждает, что по умолчанию MyBatisPlus считает 0 значением "не удалено", а 1 значением "удалено".Если вы хотите изменить это поведение, например, установить -1 как значение "удалено" и 1 как значение "не удалено", вы можете настроить это следующим образом:
mybatis-plus:
global-config:
db-config:
id-type: auto
logic-delete-field: deleted # Поле логического удаления
logic-delete-value: -1 # Значение удаления
logic-not-delete-value: 1 # Значение не удаления
Но рекомендуется использовать конфигурацию по умолчанию, а также руководство разработки Alibaba предписывает, что 1 означает удаление, а 0 — отсутствие удаления.
Для реализации пагинации MyBatisPlus
предоставляет плагин пагинации, который можно легко настроить:
@Configuration
public class MyBatisConfig {
/**
* Регистрация плагина пагинации
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
Теперь можно использовать функции, предоставляемые плагином пагинации:
@Test
void contextLoads() {
Page<Employee> page = new Page<>(1, 2);
employeeService.page(page, null);
List<Employee> employeeList = page.getRecords();
employeeList.forEach(System.out::println);
System.out.println("Общее количество записей: " + page.getTotal());
System.out.println("Текущая страница: " + page.getCurrent());
System.out.println("Общее количество страниц: " + page.getPages());
System.out.println("Количество записей на странице: " + page.getSize());
System.out.println("Есть ли предыдущая страница: " + page.hasPrevious());
System.out.println("Есть ли следующая страница: " + page.hasNext());
}
```Объект `Page` используется для указания правил пагинации, здесь это означает пагинацию с двумя записями на страницу и запрос первой страницы. Результат выполнения:
```java
Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2 Yöntem: 2021-04-24T21:14:18, deleted=0)
Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)
Общее количество записей: 4
Текущая страница: 1
Общее количество страниц: 2
Количество записей на странице: 2
Есть ли предыдущая страница: false
Есть ли следующая страница: true
Если в процессе пагинации необходимо задать определенные условия, можно использовать QueryWrapper
для их реализации:
@Test
void contextLoads() {
Page<Employee> page = new Page<>(1, 2);
employeeService.page(page, new QueryWrapper<Employee>()
.between("age", 20, 50)
.eq("gender", 1));
List<Employee> employeeList = page.getRecords();
employeeList.forEach(System.out::println);
}
В этом случае отфильтрованные данные должны представлять информацию о сотрудниках в возрасте от 20 до 50 лет и с полем gender
, равным 1, а затем эти данные следует разбить на страницы.
Пессимистичная блокировка предполагает, что конкурирующие операции будут происходить, поэтому при изменении данных блокируется вся таблица, чтобы предотвратить изменения других пользователей. Это достигается с помощью встроенных механизмов блокировки базы данных (блокировка таблицы, блокировка строки, блокировка чтения, блокировка записи).
Оптимистичная блокировка, напротив, предполагает, что конфликты данных обычно не происходят, поэтому при изменении данных таблица не блокируется. Вместо этого при подтверждении изменений производится проверка, чтобы определить, произошли ли конфликты. Обычно это достигается путем установки поля версии.
Пример с ценой товара: в таблице устанавливается поле версии, начальное значение которого равно 1. В этот момент оба администратора A и B хотят изменить цену товара. Они сначала читают данные из таблицы, и оба администратора видят, что версия равна 1. В этот момент операция администратора B завершается, и он увеличивает версию соответствующего товара до 2. Затем администратор B проверяет, изменилась ли версия, и если нет, изменения сохраняются.Администратор A также подтверждает изменения, но в этот момент версия товара уже равна 2, что не соответствует начальной версии, которую он прочитал. Это означает, что произошёл конфликт, и администратору A следует сообщить об ошибке и предложить ему повторно получить данные.
Преимуществом оптимистичного блокирования является использование более гибкой системы блокирования, что повышает пропускную способность программы и подходит для сценариев с большим количеством операций чтения.
Теперь мы смоделируем этот процесс.
1. Создание новой таблицы данных:
CREATE TABLE shop (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
name VARCHAR(30) NOT NULL,
price INT(11) DEFAULT 0,
version INT(11) DEFAULT 1,
PRIMARY KEY (id)
);
INSERT INTO shop (id, name, price) VALUES (1, 'ноутбук', 8000);
2. Создание сущностного класса:
@Data
public class Shop {
private Long id;
private String name;
private Integer price;
private Integer version;
}
3. Создание соответствующего интерфейса Mapper
:
public interface ShopMapper extends BaseMapper<Shop> {
}
4. Написание тестового кода:
@SpringBootTest
@MapperScan("com.wwj.mybatisplusdemo.mapper")
class MybatisplusDemoApplicationTests {
@Autowired
private ShopMapper shopMapper;
}
``` /**
* Моделирование сценария параллельного выполнения
*/
@Test
void contextLoads() {
// Администраторы A и B считают данные
Shop A = shopMapper.selectById(1L);
Shop B = shopMapper.selectById(1L);
// Администратор B сначала изменяет данные
B.setPrice(9000);
int result = shopMapper.updateById(B);
if (result == 1) {
System.out.println("Администратор B успешно изменил данные!");
} else {
System.out.println("Администратор B не смог изменить данные!");
}
// Администратор A затем изменяет данные
A.setPrice(8500);
int result2 = shopMapper.updateById(A);
if (result2 == 1) {
System.out.println("Администратор A успешно изменил данные!");
} else {
System.out.println("Администратор A не смог изменить данные!");
}
// Последний запрос
System.out.println(shopMapper.selectById(cq(1L)));
}
}
```Результат выполнения:
```java
Администратор B успешно изменил данные!
Администратор A успешно изменил данные!
Shop(id=1, name=ноутбук, price=8500, version=1)
Вопрос возник: действия администратора B были перекрыты действиями администратора A. Как решить эту проблему?
На самом деле, MyBatisPlus
уже предоставляет механизм оптимистичного блокирования. Необходимо только объявить атрибут версии в сущностном классе с помощью @Version
:
@Data
public class Shop {
private Long id;
private String name;
private Integer price;
@Version // Объявление атрибута версии
private Integer version;
}
Затем зарегистрируйте плагин оптимистической блокировки:
@Configuration
public class MyBatisConfig {
/**
* Регистрация плагина
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// Плагин пагинации
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// Плагин оптимистической блокировки
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
Переходите к выполнению тестового кода, результат будет следующим:
Администратор B успешно изменил запись!
Администратор A не смог изменить запись!
Shop(id=1, name='Ноутбук', price=9000, version=2)
В этот момент изменение администратора A не прошло, ему необходимо перечитать самую свежую версию данных, чтобы снова попытаться изменить запись.## Конструктор условий
В плагине пагинации мы использовали конструктор условий (Wrapper
) в упрощенном виде. Давайте подробнее рассмотрим его.
Вот структура наследования Wrapper
:
Рассмотрим их функции:
Wrapper
: абстрактный класс конструктора условий, самый верхний родительский класс
AbstractWrapper
: абстрактный класс для упаковки условий запроса, создает условие WHERE SQL
QueryWrapper
: используется для упаковки объектаUpdateWrapper
: используется для упаковки условийAbstractLambdaWrapper
: использование Wrapper с синтаксисом Lambda
LambdaQueryWrapper
: используется для упаковки объекта, с использованием синтаксиса LambdaLambdaUpdateWrapper
: используется для упаковки условий, с использованием синтаксиса LambdaОбычно мы используем QueryWrapper
и UpdateWrapper
. Если вы хотите использовать синтаксис Lambda, вы можете использовать LambdaQueryWrapper
и LambdaUpdateWrapper
. С помощью этих конструкторов условий вы можете легко реализовать сложные операции фильтрации, например:
@SpringBootTest
@MapperScan("com.wwj.mybatisplusdemo.mapper")
class MybatisplusDemoApplicationTests {
@Autowired
private EmployeeMapper employeeMapper;
@Test
void contextLoads() {
// Поиск сотрудников, чьи фамилии содержат 'j', возраст больше 20 лет, а email не пустой
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("last_name", "j");
wrapper.gt("age", 20);
wrapper.isNotNull("email");
List<Employee> list = employeeMapper.selectList(wrapper);
list.forEach(System.out::println);
}
}
Результат выполнения:```java Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)
Конструктор условий предоставляет множество методов для построения условий, например, метод `like` создает условие для поиска по шаблону. Посмотрим на SQL-запрос, который выводится в консоль:
```sql
==> Preparing: SELECT id,last_name,email,age,gmt_create,gmt_modified,is_deleted AS deleted FROM tbl_employee WHERE is_deleted=0 AND (last_name LIKE ? AND age > ? AND email IS NOT NULL)
==> Parameters: %j%(String), 20(Integer)
Как видно, перед и после j
добавляются символы %
, что позволяет выполнять поиск по шаблону. Если требуется найти записи, начинающиеся с j
, можно использовать метод likeLeft
. Если нужно найти записи, заканчивающиеся на j
, используется метод likeRight
.
Также методы сравнения чисел работают аналогично. Метод gt
используется для поиска значений, больших заданного числа. Если требуется найти значения, меньшие заданного числа, используется метод lt
. Для поиска значений, больших или равных заданному числу, используется метод ge
. Для поиска значений, меньших или равных заданному числу, используется метод le
. Для поиска значений, не равных заданному числу, используется метод ne
. Также можно использовать метод between
для поиска значений, находящихся в определенном диапазоне.
Все эти методы возвращают ссылку на текущий объект, что позволяет использовать цепочку вызовов:```java @Test void contextLoads() { // Поиск сотрудников, у которых в имени содержится 'j', возраст больше 20 лет и email не пустой QueryWrapper wrapper = new QueryWrapper() .likeLeft("last_name", "j") .gt("age", 20) .isNotNull("email"); List list = employeeMapper.selectList(wrapper); list.forEach(System.out::println); }
Также можно использовать `LambdaQueryWrapper`:
```java
@Test
void contextLoads() {
// Поиск сотрудников, у которых в имени содержится 'j', возраст больше 20 лет и email не пустой
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<Employee>()
.like(Employee::getLastName, "j")
.gt(Employee::getAge, 20)
.isNotNull(Employee::getEmail);
List<Employee> list = employeeMapper.selectList(wrapper);
list.forEach(System.out::println);
}
Преимущество этого подхода заключается в том, что поля не задаются жестко, а используются методы ссылки, что делает код более гибким и удобным для чтения.
UpdateWrapper
отличается от QueryWrapper
тем, что используется для построения запросов на обновление данных, например:
@Test
void contextLoads() {
UpdateWrapper<Employee> wrapper = new UpdateWrapper<Employee>()
.set("age", 50)
.set("email", "emp@163.com")
.like("last_name", "j")
.gt("age", 20);
employeeMapper.update(null, wrapper);
}
Измените возраст на 50 для сотрудников, имена которых содержат букву j
и возраст которых больше 20 лет, а также измените электронную почту на emp@163.com. UpdateWrapper
может не только упаковывать данные для обновления, но и использоваться как условие для поиска. Поэтому при обновлении данных можно прямо создать UpdateWrapper
, чтобы задать условия и данные для обновления.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )