Разумное и стандартизированное использование PageHelper в SpringBoot+Mybatis

Java

PageHelper

1. Подготовка к разработке

оригинальный логотип

1. Инструменты разработки

2. Среда разработки

3. Зависимости развития

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.3</version>
</dependency>
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
	<version>1.3.0</version>
</dependency>

2. Техническая документация

оригинальный логотип

1. На основе SpringBoot

2. На основе MyBatis

3. Интегрируйте PageHelper

3. Объяснение применения

оригинальный логотип

1. Основное использование

В реальном приложении проекта,PageHelperочень удобен и быстр в использовании, толькоPageInfo + PageHelperДва класса достаточны для завершения функции пейджинга, но часто этот простейший интегрированный метод использования не был полностью разработан и использован во многих практических сценариях приложений.

Далее наше наиболее распространенное использование:

public PageInfo<ResponseEntityDto> page(RequestParamDto param) {
	PageHelper.startPage(param.getPageNum(), param.getPageSize());
	List<ResoinseEntityDto> list = mapper.selectManySelective(param);
	PageInfo<ResponseEntityDto> pageInfo = (PageInfo<ResponseEntityDto>)list;
	return pageInfo;
}

В определенной степени написанное выше действительно соответствует спецификации использования PageHelper:

Использовать перед запросом коллекцииPageHelper.startPage(pageNum,pageSize),а такжеНикакой другой SQL не может быть выполнен с вкраплениями посередине

Разработчик, но как нам часто удается найти прорыв только в погоне за совершенными и экстремальными дорогами и возможностями; Ниже приведены разумные и основные нормы употребления:

public PageInfo<ResponseEntityDto> page(RequestParamDto param) {
	return PageHelper.startPage(param.getPageNum(), param.getPageSize())
			  .doSelectPageInfo(() -> list(param))
}
public List<ResponseEntityDto> list(RequestParamDto param) {
	return mapper.selectManySelective(param);
}

FAQ

1. Зачем повторно объявлять функцию списка?

Ответ: часто во многих практических сценариях бизнес-приложений,Пейджинговый запросОн основан на требованиях к отображению таблицы большого объема данных.

Однако много раз,Например: взаимные звонки внутренних служб,OpenAPIпри условии.

Даже в некоторых бизнес-сценариях, где внешний и внутренний интерфейсы разделены и отлажены, для предоставления услуг также требуется нестраничный интерфейс запроса коллекции.

Кроме того, помимо вышеупомянутых факторов, на данный момент мы можем определить и стандартизировать некоторые вещи в соответствии с приведенным выше письмом.

Например: для запросов на разбиение на страницы и коллекцииразделение и разъединение(Подробности о развязке см. в разделе Расширенное использование),

Запрос и ответ на пейджинговый запрос и фактический бизнесРазделение параметров(Подробности см. в разделе Расширенное использование) и так далее...

2. doSelectPageInfoчто это такое?

отвечать:doSelectPageInfoдаPageHelper.startPage()Значение по умолчанию, возвращаемое функциейPageВстроенные функции экземпляра

Эту функцию можно использовать дляLambdaв виде доп.Functionзапросить без лишнегоPageInfoа такжеListконвертировать

а такжеdoSelectPageInfoПараметрыPageHelperВстроенныйFunction(ISelect)интерфейс для достижения конверсииPageInfoцель

3. Объем кода при таком способе написания вроде бы много но много?

Ответ: Как описано в ①, на самом деле нет дальнейшего упрощения с точки зрения размера кода.

Однако в некоторых бизнес-сценарияхlistВ случае функционального интерфейса это более интуитивно понятная оптимизация (подробности оптимизации см. в разделе «Расширенное использование»).

оригинальный логотип

2. Расширенное использование

Сначала посмотрите на код, а потом говорите об анализе:

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;

import java.util.List;

/**
 * @param <Param> 泛型request
 * @param <Result> 泛型response
 */
public interface BaseService<Param, Result> {
    /**
     * 分页查询
     *
     * @param param 请求参数DTO
     * @return 分页集合
     */
    default PageInfo<Result> page(PageParam<Param> param) {
        return PageHelper.startPage(param).doSelectPageInfo(() -> list(param.getParam()));
    }
    /**
     * 集合查询
     *
     * @param param 查询参数
     * @return 查询响应
     */
    List<Result> list(Param param);
}

можно увидетьBaseServiceможно использовать как глобальноеServiceИнкапсуляция и объявление универсального интерфейса

В качестве общего пейджингового интерфейса здесь используется функция страницыinterfaceУникальное ключевое словоdefaultПрямой заявленpageтело метода функцииbody

import com.github.pagehelper.IPage;
import lombok.Data;
import lombok.experimental.Accessors;

@Data	// 为省略冗余代码使用lombok 实际应有常规Getter/Setter Construction toString等
@Accessors(chain = true) // 此lombok注解是为了实现 Entity伪Build 譬如: entity.setX(x).setY(y)
public class PageParam<T>  implements IPage {

   	//  description = "页码", defaultValue =  1
    private Integer pageNum = 1;

    //	description = "页数", defaultValue = 20
    private Integer pageSize = 20;

    //	description = "排序", example = "id desc"
    private String orderBy;

    //  description = "参数"
    private T param;

    public PageParam<T> setOrderBy(String orderBy) {
        this.orderBy = orderBy; // 此处可优化 优化详情且看解析
        return this;
    }
}

существуетBaseServiceв мы видим новыйPageParam,СсылкаPageInfoИспользуется для переноса/объявления/разделения параметров пейджинга и бизнес-параметров, а тип параметра является универсальным, то есть бизнес-параметры, которые поддерживают любой тип данных.

Это также можно увидетьPageParamДостигнутоIPageинтерфейс и еще кое-чтоorderByполе свойства

import common.base.BaseService;
import dto.req.TemplateReqDto;
import dto.resp.TemplateRespDto;

public interface TemplateService extends BaseService<TemplateReqDto, TemplateeRespDto> {
    // 同为interface接口, 业务Service只需要继承BaseService
    // 并根据实际使用场景声明请求参数和响应结果的Entity实体即可
}

В практических приложениях нам нужно только объявить наши общие параметры запроса бизнес-запроса и результаты ответа.

import dto.req.TemplateReqDto;
import dto.resp.TemplateRespDto;
import service.TemplateService;
import persistence.mapper.TemplateMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j // 基于lombok自动生成logger日志记录实例
@Service	// SpringBoot中注册Service Bean的注解
@RequiredArgsConstructor	// 基于lombok根据类所有final属性生成构造函数 即可完成Spring构造注入
public class TemplateServiceImpl implements TemplateService {

    private final TemplateMapper mapper;

    @Override
    public List<TemplateRespDto> list(TemplateReqDto param) {
        return mapper.selectManySelective(param) // 可根据实际情况将实体做转换
    }
}

В классе реализации вам нужно только переопределитьlistТело метода, которое записывает методы обработки бизнес-логики и запросов, которые необходимо обрабатывать в реальном бизнес-сценарии, не должно заботиться о функции подкачки.

@Slf4j	// 同上
@RestController	// SpringBoot中注册Controller Bean的注解
@RequiredArgsConstructor	// 同上
public class TemplateController {

    public final TemplateService service;

    /**
     * 分页查询
     *
     * @param pageParam 分页查询参数
     * @return 分页查询响应
     */
    @PostMapping(path = "page")
    public PageInfo<Result> page(@RequestBody PageParam<Param> pageParam) {
        return service.page(pageParam);
    }

    /**
     * 集合查询
     *
     * @param listParam 集合查询参数
     * @return 集合查询响应
     */
    @PostMapping(path = "list")
    public List<Result> list(@RequestBody Param listParam) {
        return service.list(listParam);
    }
}

окончательный кодControllerинтерфейс, вам нужно только напрямую позвонитьservice.pageВы можете использовать параметры запроса напрямуюPageParamУпаковка, разделение параметров пейджинга и бизнес-параметров и поддержка этой == спецификации разделения == при совместной отладке внешнего и внутреннего интерфейсов может значительно снизить затраты на связь и разработку.

оригинальный логотип

FAQ

1. BaseServiceтак какinterface,pageПочему может быть объявлено тело метода?

отвечать:Java8Одной из новых функций являетсяinterfaceДобавлен класс интерфейсаstatic/defaultметоды, то есть после объявления метода его подклассы или реализации будут иметь эти методы по умолчанию и могут быть вызваны напрямую

и здесь дляPageобъявление методаdefaultПотому чтоpageфункция заботится толькоПараметры пагинации и ответы на пагинацию, отделенное от бизнес-сценария, тело метода сильно отличается, поэтому просто определяется абстрактно, исключая сложный и избыточный процесс его реализации

2. PageParamВ чем смысл декларации?IPageза что?

отвечать:PageParamявляется ссылкойPageInfoПисьменный класс (не уверен в будущемPageHelperБудет ли он инкапсулировать этот класс, может быть, я могу упомянуть об этомIssueПоднимайтесь, а также участвуйте в разработке open source фреймворков)

Целью написания этого класса является разделение пейджинга и бизнес-данных, что позволяет разработчикам сосредоточиться на бизнес-реализации и разработке, а также выполнять запросы пейджинга.APIСпецификация, будь то запрос или ответ, данные, относящиеся к пейджингу, извлекаются и используются отдельно.

реализоватьIPageПотому чтоIPageтак какPageHelperВстроенныйinterface, прежде чем мы узнаем больше о его роли, его можно использовать в качестве спецификации для нашего объявления параметра подкачки, иIPageВ файле объявлено только три метода.pageNum/pageSize/orderByизGetterметода, и в анализе исходного кода я упомяну более глубокий смысл реализации этого интерфейса

3. PageParamв дополнение к обычномуpageNum/pageSize, зачем тебеorderBy?

Ответ: В обычных запросах на пейджинг толькоpageNum/pageSizeЦель пейджинга может быть выполнена, но фильтрация и сортировка часто сопровождаются пейджинговыми запросами.

а такжеorderByОсновное внимание уделяется динамической сортировке параметров на основе SQL.

4. orderByКак пользоваться?Не будет ли проблем?

отвечать:orderByа такжеpageNum/pageSize, Они естьPagehelperпройти черезMyBatisПерехватчик внедряется в запрос запроса, поэтому при передаче параметров во внешнем интерфейсеorderByпараметр должен быть базой данныхcolumn desc/ascВ таком виде сортировка по нескольким полям может быть разделена запятыми (,), например:columnA desc,columnB,

Но, с другой стороны, есть две проблемы: первая заключается в том, что при разработке большинства полей таблицы базы данных используется ==snake case== вместо ==camel case== в обычной разработке, поэтому существует слой Преобразование, и это преобразование может быть назначено времени передачи внешних параметров или времени подключения внутренних параметров.

Во-вторых, выставить поле сортировки интерфейсу настолько голым, что будетзаказ с помощью SQL-инъекциириск, поэтому в процессе фактического использования нам необходимо проверить и устранить неполадки некоторыми способамиorderByДопустимы ли переданные параметры, например, использование регулярных выражений для сопоставления значения параметра может содержать толькоorder byНеобходимые значения в синтаксисе, такие как имена полей,desc or asc, специальные символы/ключевые слова базы данных и т. д. не допускаются

5. pageNum/pageSizeДолжен ли я указывать значения по умолчанию?

Ответ: ЧитаяPageHelperисходный код, мы знаем, что вPageКогда параметры запроса нулевые, он не дает им значения по умолчанию, и не выполняет дополнительную обработку, так что подкачка не удалась, а значения по умолчанию даются для защиты от различных аварий, которые могут произойти во время фронтенда и внутренние интерфейсы отладки.

3. Анализ исходного кода

Сначала мы видимPageHelper.startPage(param)Что произошло в процессе:

public static <E> Page<E> startPage(Object params) {
	Page<E> page = PageObjectUtil.getPageFromObject(params, true);
	Page<E> oldPage = getLocalPage();
	if (oldPage != null && oldPage.isOrderByOnly()) {
		page.setOrderBy(oldPage.getOrderBy());
	}
	setLocalPage(page);
	return page;
}

ЭтоPageHelperАбстрактный класс, который расширяется (extend)PageMethodстатический метод в

Посмотрите на первую строку кодаPage<E> page = PageObjectUtil.getPageFromObject(params, true)что случилось:

public static <T> Page<T> getPageFromObject(Object params, boolean required) {
	if (params == null) {
		throw new PageException("无法获取分页查询参数!");
	} else if (params instanceof IPage) {
		IPage pageParams = (IPage)params;
		Page page = null;
		if (pageParams.getPageNum() != null && pageParams.getPageSize() != null) {
			page = new Page(pageParams.getPageNum(), pageParams.getPageSize());
		}
		if (StringUtil.isNotEmpty(pageParams.getOrderBy())) {
			if (page != null) {
				page.setOrderBy(pageParams.getOrderBy());
			} else {
				page = new Page();
				page.setOrderBy(pageParams.getOrderBy());
				page.setOrderByOnly(true);
			}
		}
		return page;
	} else {
        ... // 此处我只截取了部分代码片段, 以上是较为重要的一块
	}
}

Видно, что в этом методе он будет оцениваться в первую очередьparamsЯвляется ли он нулевым, а затем проходитinstanceofопределитьIPageподкласс или класс реализации Если два вышеуказанныхif/elseне устраивает, тоPageHelperЭто будет получено с помощью большого количества кода отражения в коде, который я не опубликовал.pageNum/pageSizeтак же какorderBy. Как мы все знаем, хотя рефлексия широко используется в Java, и как одна из уникальных особенностей языка, она глубоко любима большинством разработчиков, но рефлексия в некоторой степени требует затрат на производительность, и даже многие на данном этапе становятся мейнстримом. , Чтобы предотвратить слишком низкую производительность фрейма, он исключается рынком. Итак, на этом этапе мы, наконец, объясняем и знаем, почемуPageParamдостигатьIPageИнтерфейс, в коде здесь параметры подкачки могут быть получены непосредственно через интерфейс, без необходимости получения отрицательного для производительности отражения.PageHelperНужны параметры

Продолжай читатьstartPageПоследующий код в:

public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
    protected static boolean DEFAULT_COUNT = true;

    public PageMethod() {
    }

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

    public static <T> Page<T> getLocalPage() {
        return (Page)LOCAL_PAGE.get();
    }
	...
	...
}

можно увидетьPageHelperУнаследованный абстрактный классPageMethodобъявляетPageлокальная переменная потока, аgetLocalPage()это получить текущий потокPage

и далееif (oldPage != null && oldPage.isOrderByOnly())Это чтобы определить, есть ли старые данные пейджинга

здесьisOrderByOnlyпройти черезgetPageFromObject()функции мы можем знать, что когда есть толькоorderByпараметр, этоtrue

То есть, когда имеются старые данные поискового вызова, а старые данные поискового вызова имеют только параметры сортировки, параметры сортировки старых данных поискового вызова включаются в параметры сортировки новых данных поискового вызова.

Затем поместите новые данные с разбивкой на страницыpageХранится в локальной переменной потока

В сценариях практического применения такая ситуация встречается относительно редко, только сортировка без подкачки, поэтому с определенной точки зрения нам нужно только понять

оригинальный логотип

см. далееdoSelectPageInfo(ISelect)Что произошло в:

public <E> PageInfo<E> doSelectPageInfo(ISelect select) {
	select.doSelect();
	return this.toPageInfo();
}

Как видите, реализация этого способа очень проста и понятна, то есть через регистрационную декларациюISelectИнтерфейс разрабатывается с помощью пользовательского метода запроса коллекции и выполняется им внутри, а затем возвращаетPageInfoорганизация

Мы упоминали ранее,PageHelperна основеMyBatisПерехватчик достигает цели пейджинга, так почемуISelect.doSelect()выполнить, вернутьPageInfoКак насчет сущностей?

На самом деле это магия перехватчика, вselect.doSelect()При выполнении срабатываетPageHelperиндивидуальныеMyBatisПерехватчик запросов и анализ SQL и параметров SQL, разбиение по страницам в соответствии с типом базы данных, например MySQL.limit, ОракулRownumЖдать,

В то же время перед запросом SQL мы определяем,PageHelperSQL-запрос select count(*) будет перегенерирован и выполнен первым, и он достиг своего определения.PageНазначение встроенных параметров пагинации

@Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class PageInterceptor implements Interceptor {
    private volatile Dialect dialect;
    private String countSuffix = "_COUNT";
    protected Cache<String, MappedStatement> msCountMap = null;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";

    public PageInterceptor() {
    }

    public Object intercept(Invocation invocation) throws Throwable {
    ...
    ...
    }
}

ВышеупомянутоеPageHelperВстроенная настройкаMyBatisПерехватчик, из-за чрезмерного количества кода, чтобы гарантировать, что он не нарушает принцип не нарушать заголовок этого сообщения в блоге, здесь не будет даваться лишнее объяснение.При необходимости я могу написать отдельный блог, чтобы объяснить и объяснить это отдельно.MyBatisКонцепция и принцип перехватчика, глубокий анализMyBatisисходный код

расширять

PageHelperне толькоpageNum/pageSize/orderByЭти параметры и многое другоеpageSizeZero, reasonableПараметры и т. д. используются для более сложных определений запросов на подкачку. Если вам нужно более глубокое понимание, я могу написать другое расширенное использование ==PageHelper==. Эта статья используется только в качестве объяснения для обычной разработки и использования.

оригинальный логотип

4. Резюме

Как фреймворк для подкачки с открытым исходным кодом, имеющий почти 10 000 на GitHub, PageHelper может быть не таким глубоким и широким, как основные рыночные фреймворки и технологии, хотя построить колесо несложно с точки зрения реализации функций и принципа, а исходный код тоже очень понятно, но в значительной мере решило многие технические проблемы пейджинга на основе MyBatis, упростило и подтолкнуло к эффективности большинство разработчиков.Это направление и дорога, к которой разработчики должны стремиться и стремиться на пути разработка. Как бенефициары, мы должны не только использовать его в основном, Помимо разработки, мы также должны обратить внимание на расширение некоторых фреймворков, иметь определенное представление о нижнем слое фреймворка, расширять и оптимизировать его.

положи сюда еще разPageHelperрепозиторий с открытым исходным кодом!

яЧен Бу Эр, вы знаете меня?