Spring FrameworkでPaginationが用意されてるけど、情報が少ないな...

nazology.net

地球の生命はこれまでに、5回の大量絶滅を経験しています。

史上最大の大絶滅から「最初に復活した生物」は何だったのか? - ナゾロジー

⇧ Oh, my gosh...

Spring FrameworkでPaginationが用意されてるけど、情報が少ないな...

公式のドキュメントを見る限り、

docs.spring.io

This section documents Spring Data REST’s usage of the Spring Data Repository paging and sorting abstractions. To familiarize yourself with those features, see the Spring Data documentation for the repository implementation you use (such as Spring Data JPA).

https://docs.spring.io/spring-data/rest/docs/current/reference/html/#paging-and-sorting

⇧ Spring Data系を使っていれば使えるってことなんかね?

Spring Data系の情報を見てみると、

spring.io

Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store.

https://spring.io/projects/spring-data

It makes it easy to use data access technologies, relational and non-relational databases, map-reduce frameworks, and cloud-based data services. This is an umbrella project which contains many subprojects that are specific to a given database. The projects are developed by working together with many of the companies and developers that are behind these exciting technologies.

https://spring.io/projects/spring-data

⇧ 幅広く扱ってらっしゃる。

  • Spring Data
    • Spring Data JDBC
    • Spring Data JPA
    • Spring Data LDAP
    • Spring Data MongoDB
    • Spring Data Redis
    • Spring Data R2DBC
    • Spring Data REST
    • Spring Data for Apache Cassandra
    • Spring Data for Apache Geode
    • Spring Data for Apache Solr
    • Spring Data for VMware Tanzu GemFire
    • Spring Data Couchbase
    • Spring Data Elasticsearch
    • Spring Data Envers
    • Spring Data Neo4j
    • Spring Data JDBC Extensions
    • Spring for Apache Hadoop

2022年7月9日(土)時点で、17個のData Serviceに対応してる模様。

実際にPaginationしてみる

で、肝心の使い方が、微妙な説明でらっしゃる...

致し方ないので、公式以外の情報を参考にするしかないですね、一次情報だけじゃどうにもならん哀しさ...。

www.bezkoder.com

stackoverflow.com

⇧ 上記サイト様を参考にさせていただきました。

フロントエンドはVue.jsを使っていきます。

データベースはPostgreSQLを使ってます。

まずは、データベースにテーブルを作成しておきます。

create table public.shikoku_ohenro (
  id serial not null
  , name_fudasyo character varying(20) not null
  , kana_name_fudasyo character varying(50) not null
  , rome_name_fudasyo character varying(50) not null
  , name_temple character varying(20) not null
  , kana_name_temple character varying(50) not null
  , rome_name_temple character varying(50) not null
  , prefectures_id integer not null
  , created timestamp(6) with time zone default CURRENT_TIMESTAMP
  , updated timestamp(6) with time zone
  , is_deleted boolean default false
  , primary key (id)
);

で、データもINSERTしておきます。

-- お遍路データ
INSERT INTO shikoku_ohenro (id, name_fudasyo, kana_name_fudasyo, rome_name_fudasyo, name_temple, kana_name_temple, rome_name_temple, prefectures_id)
 VALUES(1, '第1番札所','だいいちばんふだしょ', 'daiichibanfudasyo', '霊山寺', 'りょうぜんじ', 'ryozenji', 36)
 ,(2, '第2番札所','だいにばんふだしょ', 'dainibanfudasyo', '極楽寺', 'ごくらくじ', 'gokurakuji', 36)
 ,(3, '第3番札所','だいさんばんふだしょ', 'daisanbanfudasyo', '金泉寺', 'こんせんじ', 'konsenji', 36)
 ,(4, '第4番札所','だいよんばんふだしょ', 'daiyonbanfudasyo', '大日寺', 'だいにちじ', 'dainichiji', 36)
 ,(5, '第5番札所','だいごばんふだしょ', 'daiigobanfudasyo', '地蔵寺', 'じぞうじ', 'jizouji', 36)
 ,(6, '第6番札所','だいろくばんふだしょ', 'dairokubanfudasyo','安楽寺', 'あんらくじ', 'anrakuji', 36)
 ,(7, '第7番札所','だいななばんふだしょ', 'dainanabanfudasyo','十楽寺', 'じゅうらくじ', 'jyurakuji', 36)
 ,(8, '第8番札所','だいはちばんふだしょ', 'daihachibanfudasyo','熊谷寺', 'くまだにじ ', 'kumadaniji', 36)
 ,(9, '第9番札所','だいきゅうばんふだしょ', 'daikyubanfudasyo','法輪寺', 'ほうりんじ', 'hourinji', 36)
 ,(10,'第10番札所','だいじゅうばんふだしょ', 'daijyubanbanfudasyo','切幡寺', 'きりはたじ', 'kiriharadai', 36)
 ,(11,'第11番札所','だいじゅういちばんふだしょ', 'daijyuichibanfudasyo', '藤井寺', 'ふじいでら', 'fujiidera', 36)
 ,(12,'第12番札所','だいじゅうにばんふだしょ', 'daijyuuibanfudasyo','焼山寺', 'しょうざんじ', 'syouzanji', 36)
 ,(13,'第13番札所','だいじゅうさんばんふだしょ', 'daijyusanbanfudasyo','大日寺', 'だいにちじ', 'dainichiji', 36)
 ,(14,'第14番札所','だいじゅうよんばんふだしょ', 'daijyuyonbanfudasyo','常楽寺', 'じょうらくじ', 'jyourakuji', 36)
 ,(15,'第15番札所','だいじゅうごばんふだしょ', 'daijyugobanfudasyo','国分寺', 'こくぶんじ', 'kokubunji', 36)
 ,(16,'第16番札所','だいじゅうろくばんふだしょ', 'daijyurokubanfudasyo','観音寺', 'かんおんじ', 'kanonji', 36)
 ,(17,'第17番札所','だいじゅうななばんふだしょ', 'daijyunanabanfudasyo','井戸寺', 'いどじ', 'idoji', 36)
 ,(18,'第18番札所','だいじゅうはちばんふだしょ', 'daijyuhachibanfudasyo','恩山寺', 'おんざんじ', 'onzanji', 36) 
 ,(19,'第19番札所','だいじゅうきゅうばんふだしょ', 'daijyukyubanfudasyo','立江寺', 'りつえじ', 'ritsuenji', 36)
 ,(20,'第20番札所','だいにじゅうばんふだしょ', 'dainijyubanfudasyo','鶴林寺', ' かくりんじ', 'kakurinji', 36)
 ,(21,'第21番札所','だいにじゅういちばんふだしょ', 'dainijyuichibanfudasyo','太龍寺', 'たいりゅうじ', 'tairyuji', 36)
 ,(22,'第22番札所','だいにじゅうにばんふだしょ', 'dainijyunibanfudasyo', '平等寺', 'びょうどうじ', 'byoudouji', 36)
 ,(23,'第23番札所','だいにじゅうさんばんふだしょ', 'dainijyusanbanfudasyo','薬王寺', 'やくおうじ', 'yakuouji', 36)
 ,(24,'第24番札所','だいにじゅうよんばんふだしょ', 'dainijyuyonbanfudasyo', '最御崎寺', 'ほつみさきじ', 'hotsumisakiji', 39)
 ,(25,'第25番札所','だいにじゅうごばんふだしょ', 'dainijyugobanfudasyo', '津照寺', 'しんしょうじ', 'shinsyouji', 39)
 ,(26,'第26番札所','だいにじゅうろくばんふだしょ', 'dainijyurokubanfudasyo','金剛頂寺', 'こんごうちょうじ', 'kongoutyouji', 39)
 ,(27,'第27番札所','だいにじゅうななばんふだしょ', 'dainijyunanabanfudasyo','神峰寺', 'こうのみねじ', 'kounomineji', 39)
 ,(28,'第28番札所','だいにじゅうはちばんふだしょ', 'dainijyuhachibanfudasyo','大日寺', 'だいにちじ', 'dainichiji', 39) 
 ,(29,'第29番札所','だいにじゅうきゅうばんふだしょ', 'dainijyukyubanfudasyo','国分寺', 'こくぶんじ', 'kokubunji', 39)
 ,(30,'第30番札所','だいさんじゅうばんふだしょ', 'daisanjyubanfudasyo','善楽寺', 'ぜんらくじ', 'zenrakuji', 39)
 ,(31,'第31番札所','だいさんじゅういちばんふだしょ', 'daisanjyuichibanfudasyo','竹林寺', 'ちくりんじ', 'chikurinji', 39)
 ,(32,'第32番札所','だいさんじゅうにばんふだしょ', 'daisanjyunibanfudasyo','禅師峰寺', 'ぜんじぶじ', 'zenjibuji', 39)
 ,(33,'第33番札所','だいさんじゅうさんばんふだしょ', 'daisanjyusanbanfudasyo','雪蹊寺', 'せっけいじ', 'sekkeiji', 39)
 ,(34,'第34番札所','だいさんじゅうよんばんふだしょ', 'daisanjyuyonbanfudasyo','種間寺', 'たねまじ', 'tamaneji', 39)
 ,(35,'第35番札所','だいさんじゅうごばんふだしょ', 'daisanjyugobanfudasyo','清滝寺', 'きよたきじ', 'kiyotakiji', 39)
 ,(36,'第36番札所','だいさんじゅうろくばんふだしょ', 'daisanjyurokubanfudasyo','青龍寺', 'しょうりゅうじ', 'syouryuji', 39)
 ,(37,'第37番札所','だいさんじゅうななばんふだしょ', 'daisanjyunanabanfudasyo','岩本寺', 'いわもとじ', 'iwamotoji', 39)
 ,(38,'第38番札所','だいさんじゅうはちばんふだしょ', 'daisanjyuhachibanfudasyo','金剛福寺', 'こんごうふくじ', 'kongoufukuji', 39)
 ,(39,'第39番札所','だいさんじゅうきゅうばんふだしょ', 'daisanjyukyubanfudasyo','延光寺', 'えんこうじ', 'enkouji', 39)
 ,(40,'第40番札所','だいよんじゅうばんふだしょ', 'daiyonjyubanfudasyo','観自在寺', 'かんじざいじ', 'kanjizaiji', 38)
 ,(41,'第41番札所','だいよんじゅういちばんふだしょ', 'daiyonjyuichibanfudasyo','龍光寺', 'りゅうこうじ', 'ryukouji', 38)
 ,(42,'第42番札所','だいよんじゅうにばんふだしょ', 'daiyonjyunibanfudasyo','佛木寺', 'ぶつもくじ', 'butsumokuji', 38)
 ,(43,'第43番札所','だいよんじゅうさんばんふだしょ', 'daiyonjyusanbanfudasyo','明石寺', 'あげいしじ', 'ageishiji', 38)
 ,(44,'第44番札所','だいよんじゅうよんばんふだしょ', 'daiyonjyuyonbanfudasyo','大宝寺', 'だいほうじ', 'daihouji', 38)
 ,(45,'第45番札所','だいよんじゅうごばんふだしょ', 'daiyonjyugobanfudasyo','岩屋寺', 'いわやじ', 'iwayaji', 38)
 ,(46,'第46番札所','だいよんじゅうろくばんふだしょ', 'daiyonjyurokubanfudasyo','浄瑠璃寺', 'じょうるりじ', 'jyoururiji', 38)
 ,(47,'第47番札所','だいよんじゅうななばんふだしょ', 'daiyonjyunanabanfudasyo','八坂寺', 'やさかじ', 'yasakaji', 38)
 ,(48,'第48番札所','だいよんじゅうはちばんふだしょ', 'daiyonjyuhachibanfudasyo','西林寺', 'さいりんじ', 'sairinji', 38)
 ,(49,'第49番札所','だいよんじゅうきゅうばんふだしょ', 'daiyonjyukyubanfudasyo','浄土寺', 'じょうどじ', 'jyoudoji', 38)
 ,(50,'第50番札所','だいごじゅうばんふだしょ', 'daigojyubanbanfudasyo','繁多寺', 'はんたじ', 'hantaji', 38)
 ,(51,'第51番札所','だいごじゅういちばんふだしょ', 'daigojyuichibanfudasyo','石手寺', 'いしてじ', 'ishiteji', 38)
 ,(52,'第52番札所','だいごじゅうにばんふだしょ', 'daigojyunibanfudasyo','太山寺', 'たいさんじ', 'taisaiji', 38)
 ,(53,'第53番札所','だいごじゅうさんばんふだしょ', 'daigojyusanbanfudasyo','円明寺', 'えんみょうじ', 'enmyouji', 38) 
 ,(54,'第54番札所','だいごじゅうよんばんふだしょ', 'daigojyuyonbanfudasyo','延命寺', 'えんめいじ', 'enmeiji', 38)
 ,(55,'第55番札所','だいごじゅうごばんふだしょ', 'daigojyugobanfudasyo','南光坊', 'なんこうぼう', 'nankoubou', 38)
 ,(56,'第56番札所','だいごじゅうろくばんふだしょ', 'daigojyurokubanfudasyo','泰山寺', 'たいさんじ', 'taisanji', 38)
 ,(57,'第57番札所','だいごじゅうななばんふだしょ', 'daigojyunanabanfudasyo','栄福寺', 'えいふくじ', 'eifukuji', 38)
 ,(58,'第58番札所','だいごじゅうはちばんふだしょ', 'daigojyuhachiichibanfudasyo','仙遊寺', 'せんゆうじ', 'senyuuji', 38)
 ,(59,'第59番札所','だいごじゅうきゅうばんふだしょ', 'daigojyukyubanfudasyo','国分寺', 'こくぶんじ', 'kokubunji', 38)
 ,(60,'第60番札所','だいろくじゅうばんふだしょ', 'dairokujyubanfudasyo','横峰寺', 'よこみねじ', 'yokomineji', 38)
 ,(61,'第61番札所','だいろくじゅういちばんふだしょ', 'dairokujyuichibanfudasyo','香園寺', 'こうおんじ', 'kouonji', 38)
 ,(62,'第62番札所','だいろくじゅうにばんふだしょ', 'dairokujyunibanfudasyo','宝寿寺', 'ほうじゅじ', 'houjyuji', 38)
 ,(63,'第63番札所','だいろくじゅうさんばんふだしょ', 'dairokujyusanbanfudasyo','吉祥寺', 'きっしょうじ', 'kissyouji', 38)
 ,(64,'第64番札所','だいろくじゅうよんばんふだしょ', 'dairokujyuyonbanfudasyo','前神寺', 'まえがみじ', 'maegamiji', 38)
 ,(65,'第65番札所','だいろくじゅうごばんふだしょ', 'dairokujyugobanfudasyo','三角寺', 'さんかくじ', 'sankakuji', 38)
 ,(66,'第66番札所','だいろくじゅうろくばんふだしょ', 'dairokujyurokubanfudasyo','雲辺寺', 'うんべんじ', 'unbenji', 37)
 ,(67,'第67番札所','だいろくじゅうななばんふだしょ', 'dairokujyunanabanfudasyo','大興寺', 'だいこうじ', 'daikouji', 37)
 ,(68,'第68番札所','だいろくじゅうはちばんふだしょ', 'dairokujyuhachibanfudasyo','神恵院', 'じんねいん', 'jinnein', 37)
 ,(69,'第69番札所','だいろくじゅうきゅうばんふだしょ', 'dairokujyukyubanfudasyo','観音寺', 'かんおんじ', 'kanonji', 37)
 ,(70,'第70番札所','だいななじゅうばんふだしょ', 'dainanajyubanfudasyo','本山寺', 'もとやまじ', 'motoyamaji', 37)
 ,(71,'第71番札所','だいななじゅういちばんふだしょ', 'dainanajyuichibanfudasyo','弥谷寺', 'いやだにじ', 'iyadaniji', 37)
 ,(72,'第72番札所','だいななじゅうにばんふだしょ', 'dainanajyunibanfudasyo','曼茶羅寺', 'まんだらじ', 'mandaraji', 37)
 ,(73,'第73番札所','だいななじゅうさんばんふだしょ', 'dainanajyusanbanfudasyo','出釈迦寺', 'しゅっしゃかじ', 'syussyakaji', 37)
 ,(74,'第74番札所','だいななじゅうよんばんふだしょ', 'dainanajyuyonbanfudasyo','甲山寺', 'こうやまじ', 'kouyamaji', 37)
 ,(75,'第75番札所','だいななじゅうごばんふだしょ', 'dainanajyugobanfudasyo','善通寺', 'ぜんつうじ', 'zentsuuji', 37)
 ,(76,'第76番札所','だいななじゅうろくばんふだしょ', 'dainanajyurokubanfudasyo','金倉寺', 'こうぞうじ', 'kouzouji', 37)
 ,(77,'第77番札所','だいななじゅうななばんふだしょ', 'dainanajyunanabanfudasyo','道隆寺', 'どうりゅうじ', 'douryuuji', 37)
 ,(78,'第78番札所','だいななじゅうはちばんふだしょ', 'dainanajyuhachibanfudasyo','郷照寺', 'ごうしょうじ', 'gousyouji', 37)
 ,(79,'第79番札所','だいななじゅうきゅうばんふだしょ', 'dainanajyukyubanfudasyo','高照院', 'こうしょういん', 'kousyouin', 37)
 ,(80,'第80番札所','だいはちじゅうばんふだしょ', 'daihachijyubanfudasyo','国分寺', 'こくぶんじ', 'kokubunji', 37)
 ,(81,'第81番札所','だいはちじゅういちばんふだしょ', 'daihachijyuichibanfudasyo','白峯寺', 'しろみねじ', 'shiromineji', 37)
 ,(82,'第82番札所','だいはちじゅうにばんふだしょ', 'daihachijyunibanfudasyo','根香寺', 'ねごろじ', 'negoroji', 37)
 ,(83,'第83番札所','だいはちじゅうさんばんふだしょ', 'daihachijyusanbanfudasyo','一宮寺', 'いちのみやじ', 'ichinomiya', 37)
 ,(84,'第84番札所','だいはちじゅうよんばんふだしょ', 'daihachijyuyonbanfudasyo','屋島寺', 'やしまじ', 'yashimaji', 37)
 ,(85,'第85番札所','だいはちじゅうごばんふだしょ', 'daihachijyugobanfudasyo','八栗寺', 'やくりじ', 'yakuriji', 37)
 ,(86,'第86番札所','だいはちじゅうろくばんふだしょ', 'daihachijyurokubanfudasyo','志度寺', 'しどじ', 'shidoji', 37)
 ,(87,'第87番札所','だいはちじゅうななばんふだしょ', 'daihachijyunanabanfudasyo','長尾寺', 'ながおじ', 'nagaoji', 37)
 ,(88,'第88番札所','だいはちじゅうはちばんふだしょ', 'daihachijyuhachibanfudasyo','大窪寺', 'おおくぼじ', 'ookuboji', 37);

そしたらば、サーバーサイドの実装。

今回、関係ありそうなところだけ、抜粋。

■async-example/build.gradle

plugins {
	id 'org.springframework.boot' version '2.7.0'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

■async-example/src/main/resources/application.properties

server.port=8088

spring.datasource.url=jdbc:postgresql://localhost:5434/test
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver

spring.jpa.properties.hibernate.default_schema=public

■async-example/src/main/java/com/example/demo/config/CorsConfig.java

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
	@Override
	public void addCorsMappings(CorsRegistry registry) {

		registry.addMapping("/**")
		  .allowedOrigins("http://localhost:8080")
		  .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD")
		  .allowCredentials(true);
	}
}

■async-example/src/main/java/com/example/demo/domain/entity/ShikokuOhenro.java

package com.example.demo.domain.entity;

import java.time.OffsetDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;

@Entity
@Table(name="shikoku_ohenro")
@Data
public class ShikokuOhenro {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="id", columnDefinition="serial")
	private Long id;
	
	@Column(name="name_fudasyo", columnDefinition="varchar(20)")
	private String nameFudasyo;
	
	@Column(name="kana_name_fudasyo", columnDefinition="varchar(50)")
	private String kanaNameFudasyo;
	
	@Column(name="rome_name_fudasyo", columnDefinition="varchar(50)")
	private String romeNameFudasyo;
	
	@Column(name="name_temple", columnDefinition="varchar(20)")
	private String nameTemple;
	
	@Column(name="kana_name_temple", columnDefinition="varchar(50)")
	private String kanaNameTemple;
	
	@Column(name="rome_name_temple", columnDefinition="varchar(50)")
	private String romeNameTemple;
	
	@Column(name="prefectures_id", columnDefinition="int")
	private Integer prefecturesId;
	
	@Column(name="created", columnDefinition="timestamp(6) with timezone")
	private OffsetDateTime created;
	
	@Column(name="updated", columnDefinition="timestamp(6) with timezone")
	private OffsetDateTime updated;	
	
	@Column(name="is_deleted", columnDefinition="boolean")
	private boolean isDeleted;
}

■async-example/src/main/java/com/example/demo/domain/repository/ShikokuOhenroRepository.java

package com.example.demo.domain.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import com.example.demo.domain.entity.ShikokuOhenro;

public interface ShikokuOhenroRepository extends JpaRepository<ShikokuOhenro, Long>, JpaSpecificationExecutor<ShikokuOhenro> {
	
	Page<ShikokuOhenro> findAll(Pageable pageable);
}    

■async-example/src/main/java/com/example/demo/service/shikoku_ohenro/ShikokuOhenroServiceImpl.java

package com.example.demo.service.shikoku_ohenro;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.example.demo.domain.entity.ShikokuOhenro;
import com.example.demo.domain.repository.ShikokuOhenroRepository;

@Service
public class ShikokuOhenroServiceImpl {

	@Autowired
	private ShikokuOhenroRepository shikokuOhenroRepository;
	
	public Page<ShikokuOhenro> getAll(Pageable pageable) {
		return shikokuOhenroRepository.findAll(pageable);
	}
	
}

■async-example/src/main/java/com/example/demo/controller/shikoku_ohenro/ShikokuOhenroController.java

package com.example.demo.controller.shikoku_ohenro;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.domain.entity.ShikokuOhenro;
import com.example.demo.service.shikoku_ohenro.ShikokuOhenroServiceImpl;

@RestController
@RequestMapping("/shikoku_ohenro")
public class ShikokuOhenroController {

	@Autowired
	private ShikokuOhenroServiceImpl shikokuOhenroServiceImpl;
	
	@GetMapping("/list")
	public Page<ShikokuOhenro> getAll(@RequestParam(name="page", defaultValue = "0", required = false) Integer page
			, @RequestParam(name="pageSize", defaultValue = "20", required = false) Integer pageSize) {
		Pageable paging = PageRequest.of(page, pageSize);
		return shikokuOhenroServiceImpl.getAll(paging);
	}
	
}

■async-example/src/main/java/com/example/demo/AsyncExampleApplication.java

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AsyncExampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(AsyncExampleApplication.class, args);
	}

}    

⇧ で保存。

続いて、フロントエンド。

 

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\package.json

{
  "name": "my-project-vue",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "vue": "^2.6.14",
    "vue-class-component": "^7.2.3",
    "vue-property-decorator": "^9.1.2",
    "vue-router": "^3.5.1"
  },
  "devDependencies": {
    "@types/node": "^18.0.0",
    "@types/video.js": "^7.3.40",
    "@typescript-eslint/eslint-plugin": "^5.4.0",
    "@typescript-eslint/parser": "^5.4.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-plugin-router": "~5.0.0",
    "@vue/cli-plugin-typescript": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "@vue/eslint-config-typescript": "^9.1.0",
    "axios": "^0.26.1",
    "element-ui": "^2.15.8",
    "eslint": "^7.32.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-vue": "^8.0.3",
    "fs": "0.0.1-security",
    "node-polyfill-webpack-plugin": "^2.0.0",
    "prettier": "^2.4.1",
    "sass": "^1.32.7",
    "sass-loader": "^12.0.0",
    "typescript": "~4.5.5",
    "v-idle": "^0.2.1",
    "video.js": "^7.19.2",
    "vue-template-compiler": "^2.6.14",
    "webpack-node-externals": "^3.0.0"
  }
}

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\router\index.ts

import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import HomeView from "../views/HomeView.vue";

Vue.use(VueRouter);

const routes: Array<RouteConfig> = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  {
    path: "/about",
    name: "about",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
  },
  // {
  //   path: "/test",
  //   name: "test",
  //   component: () => import("../views/NavigationGuard.vue"),
  // },
  // {
  //   path: "/testdom",
  //   name: "testdom",
  //   component: () => import("../views/TestDom.vue"),
  // },
  {
    path: "/testui",
    name: "testui",
    component: () => import("../views/TestElementUi.vue"),
  },
  {
    path: "/user/list",
    component: () => import("../views/user/list.vue"),
  },
  {
    path: "/user/edit/:id",
    name: "/user/edit",
    component: () => import("../views/user/edit/index.vue"),
    //props: (route) => ({ id: Number(route.params.id) }),
  },
  {
    path: "/user/confirm",
    name: "/user/confirm",
    component: () => import("../views/user/confirm/index.vue"),
    //props: (route) => ({ id: Number(route.params.id) }),
  },
  {
    path: "/chat",
    name: "chat",
    component: () => import("../views/chat/TestChat.vue"),
  },
  {
    path: "/upload",
    name: "upload",
    component: () => import("../views/upload/index.vue"),
  },
  {
    path: "/pageable",
    name: "pageable",
    component: () => import("../views/pageable/index.vue"),
  },
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

export default router;

⇧ Vue Routerは、63行目~67行目が今回関係してるところ。

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\api\request.ts

import axios, { AxiosInstance } from "axios";

const apiClient: AxiosInstance = axios.create({
  // APIのURI
  baseURL: "http://localhost:8088",
  // リクエストヘッダ
  headers: {
    "Content-type": "application/json",
  },
});

export default apiClient;

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\views\pageable\index.vue

<template>
  <div>
    <el-table :data="tableData" style="width: 100%">
      <el-table-column prop="id" label="ID" width="55"></el-table-column>
      <el-table-column
        prop="nameFudasyo"
        label="札所"
        width="160"
      ></el-table-column>
      <el-table-column
        prop="kanaNameFudasyo"
        label="札所(かな)"
        width="260"
      ></el-table-column>
      <el-table-column
        prop="romeNameFudasyo"
        label="札所(ローマ字)"
        width="200"
      ></el-table-column>
      <el-table-column prop="nameTemple" label="お寺"></el-table-column>
      <el-table-column
        prop="kanaNameTemple"
        label="お寺(かな)"
      ></el-table-column>
      <el-table-column
        prop="romeNameTemple"
        label="お寺(ローマ字)"
      ></el-table-column>
    </el-table>
    <el-pagination
      layout="prev, pager, next"
      :total="this.total"
      @current-change="setPage"
    >
    </el-pagination>
  </div>
</template>

<script lang="ts">
/* eslint-disable no-console */
import { Component, Vue, Watch } from "vue-property-decorator";
import request from "@/api/request";

@Component
export default class PageableView extends Vue {
  private tableData: Array<object> = [];
  private page = 1;
  private pageSize = 20;
  private total = 1;

  created() {
    return request({
      method: "get",
      url: "/shikoku_ohenro/list",
    }).then((response: any) => {
      console.dir(response);
      this.total = response.data.totalElements / 2;
      response.data.content.forEach((element: object) => {
        this.tableData.push(element);
        //Object.assign(this.tableData, element);
        // Object.assign(this.tableData, {
        //   id: element.id,
        //   nameFudasyo: element.nameFudasyo,
        //   kanaNameFudasyo: element.kanaNameFudasyo,
        //   romeNameFudasyo: element.romeNameFudasyo,
        //   nameTemplae: element.nameTemplate,
        //   kanaNameTemple: element.kanaNameTemple,
        //   romeNameTemplae: element.romeNameTemple,
        // });
      });
      console.dir(this.tableData);
    });
  }

  private pagedTableData() {
    return this.tableData.slice(
      this.pageSize * this.page - this.pageSize,
      this.pageSize * this.page
    );
  }

  private setPage(val) {
    this.page = val--;
    console.log(val);
    return request({
      method: "get",
      url: "/shikoku_ohenro/list",
      params: {
        page: val,
        pageSize: this.pageSize,
      },
    }).then((response: any) => {
      console.dir(response);
      this.total = response.data.totalElements / 2;
      this.tableData = [];
      response.data.content.forEach((element: any) => {
        // this.tableData.push(element);
        // Object.assign(this.tableData, element);
        //let tmp = (element.nameFudasyo as string).concat("1000", "年");
        let variable = "おお勇者よ(ハローワーク.mp4)";
        const tmp = variable.match(/\((.+)\)/) as object;
        console.log(tmp[1]);

        this.tableData.push(
          Object.assign([], {
            id: element.id,
            nameFudasyo: (element.nameFudasyo as string).concat("1000", "年"),
            kanaNameFudasyo: element.kanaNameFudasyo,
            romeNameFudasyo: element.romeNameFudasyo,
            nameTemple: element.nameTemple,
            kanaNameTemple: element.kanaNameTemple,
            romeNameTemplae: element.romeNameTemple,
          })
        );
      });
      console.dir(this.tableData);
    });
  }
}
</script>

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\App.vue

<template>
  <div id="app">
    <layout></layout>
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <!--
      <router-link to="/testdom">dom</router-link> |
      -->
      <router-link to="/testui">UI</router-link> |
      <router-link to="/user/list">User</router-link> |
      <router-link to="/chat">Chat</router-link> |
      <router-link to="/upload">Upload</router-link> |
      <router-link to="/pageable">Pageable</router-link>
    </nav>
    <router-view />
    <v-idle @idle="onIdle" :loop="true" :duration="60 * 1" />
  </div>
</template>

<script lang="ts">
/* eslint-disable no-console */
import { Component, Vue, Watch } from "vue-property-decorator";
//import Layout from "@/views/layout/index.vue";

@Component({
  components: {
    //Layout,
  },
})
export default class TestElementUi extends Vue {
  private localArr: Array<string> = [];
  private onIdle() {
    console.log("■idle");
    this.localArr.push(Math.random().toString(36).slice(-8));
    localStorage.setItem("movie", JSON.stringify(this.localArr));
    let value = localStorage.getItem("movie");
    this.localArr = JSON.parse(value as string);
    if (this.localArr && this.localArr.length) {
      console.log(this.localArr);
      this.localArr.shift();
    }
    console.log(value);
    localStorage.removeItem("movie");
  }
}
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}
</style>

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\main.ts

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import Element from "element-ui";
// ElementUIでの言語設定、datePickerとかで適用される
import locale from "element-ui/lib/locale/lang/ja";
import "element-ui/lib/theme-chalk/index.css";

import "video.js/dist/video-js.css";

import Vidle from "v-idle";

Vue.config.productionTip = false;
Vue.use(Element, { locale });
Vue.use(Vidle);

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");
    

で、保存して、サーバーサイド、フロントエンド、どちらもサーバーを起動しておきます。

で、ブラウザからhttp://localhost:8080にアクセスしたら、Pageableのリンクをクリックでページ遷移し、

ページネーションが表示されてるので、2をクリックしてみると、

21番目から40番目の20個の要素が取得できました。

というわけで、ページネーションは何かと分かりにくい、というかドキュメントもっと充実させて欲しい…
毎度モヤモヤ感が半端ない...

今回はこのへんで。