refactor: application level prepare for graphql and switch to sqlite

This commit is contained in:
xushanchuan 2021-03-19 15:23:37 +08:00
parent 36e33e7730
commit 01fac42c64
No known key found for this signature in database
GPG Key ID: 44D23C44E00838D6
28 changed files with 746 additions and 416 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
*.db
### STS ###
.apt_generated

View File

@ -4,6 +4,7 @@ plugins {
id 'java'
id 'idea'
id 'eclipse'
id "com.netflix.dgs.codegen" version "4.2.0"
}
version = '0.0.1-SNAPSHOT'
@ -13,6 +14,7 @@ targetCompatibility = JavaVersion.VERSION_1_8
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
configurations {
@ -26,11 +28,11 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3'
implementation "com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:latest.release"
implementation 'org.flywaydb:flyway-core'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'joda-time:joda-time:2.10.6'
runtimeOnly 'com.h2database:h2'
implementation 'org.xerial:sqlite-jdbc:3.34.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
@ -47,3 +49,8 @@ dependencies {
test {
useJUnitPlatform()
}
generateJava {
schemaPaths = ["${projectDir}/src/main/resources/schema"] // List of directories containing schema files
packageName = 'io.spring.graphql' // The package name to use to generate sources
}

View File

@ -2,8 +2,9 @@ package io.spring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
@SpringBootApplication
@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class)
public class RealworldApplication {
public static void main(String[] args) {

View File

@ -34,25 +34,39 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
if (h2ConsoleEnabled) {
http.authorizeRequests()
.antMatchers("/h2-console", "/h2-console/**").permitAll()
.antMatchers("/h2-console", "/h2-console/**")
.permitAll()
.and()
.headers().frameOptions().sameOrigin();
.headers()
.frameOptions()
.sameOrigin();
}
http.csrf().disable()
http.csrf()
.disable()
.cors()
.and()
.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/graphiql").permitAll()
.antMatchers("/graphql").permitAll()
.antMatchers(HttpMethod.GET, "/articles/feed").authenticated()
.antMatchers(HttpMethod.POST, "/users", "/users/login").permitAll()
.antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags").permitAll()
.anyRequest().authenticated();
.antMatchers(HttpMethod.OPTIONS)
.permitAll()
.antMatchers("/graphiql")
.permitAll()
.antMatchers("/graphql")
.permitAll()
.antMatchers(HttpMethod.GET, "/articles/feed")
.authenticated()
.antMatchers(HttpMethod.POST, "/users", "/users/login")
.permitAll()
.antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags")
.permitAll()
.anyRequest()
.authenticated();
http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
@ -61,10 +75,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(asList("*"));
configuration.setAllowedMethods(asList("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowedMethods(asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
// setAllowCredentials(true) is important, otherwise:
// The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
// The value of the 'Access-Control-Allow-Origin' header in the response must not be the
// wildcard '*' when the request's credentials mode is 'include'.
configuration.setAllowCredentials(false);
// setAllowedHeaders is important! Without it, OPTIONS preflight request
// will fail with 403 Invalid CORS request

View File

@ -1,5 +1,7 @@
package io.spring.application;
import static java.util.stream.Collectors.toList;
import io.spring.application.data.ArticleData;
import io.spring.application.data.ArticleDataList;
import io.spring.application.data.ArticleFavoriteCount;
@ -7,17 +9,15 @@ import io.spring.core.user.User;
import io.spring.infrastructure.mybatis.readservice.ArticleFavoritesReadService;
import io.spring.infrastructure.mybatis.readservice.ArticleReadService;
import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static java.util.stream.Collectors.toList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ArticleQueryService {
@ -26,7 +26,8 @@ public class ArticleQueryService {
private ArticleFavoritesReadService articleFavoritesReadService;
@Autowired
public ArticleQueryService(ArticleReadService articleReadService,
public ArticleQueryService(
ArticleReadService articleReadService,
UserRelationshipQueryService userRelationshipQueryService,
ArticleFavoritesReadService articleFavoritesReadService) {
this.articleReadService = articleReadService;
@ -58,7 +59,49 @@ public class ArticleQueryService {
}
}
public ArticleDataList findRecentArticles(String tag, String author, String favoritedBy, Page page, User currentUser) {
public CursorPager<ArticleData> findRecentArticlesWithCursor(
String tag, String author, String favoritedBy, CursorPageParameter page, User currentUser) {
List<String> articleIds =
articleReadService.findArticlesWithCursor(tag, author, favoritedBy, page);
if (articleIds.size() == 0) {
return new CursorPager<>(new ArrayList<>(), page.getDirection(), false);
} else {
boolean hasExtra = articleIds.size() > page.getLimit();
if (hasExtra) {
articleIds.remove(page.getLimit());
}
if (!page.isNext()) {
Collections.reverse(articleIds);
}
List<ArticleData> articles = articleReadService.findArticles(articleIds);
fillExtraInfo(articles, currentUser);
return new CursorPager<>(articles, page.getDirection(), hasExtra);
}
}
public CursorPager<ArticleData> findUserFeedWithCursor(User user, CursorPageParameter page) {
List<String> followdUsers = userRelationshipQueryService.followedUsers(user.getId());
if (followdUsers.size() == 0) {
return new CursorPager<>(new ArrayList<>(), page.getDirection(), false);
} else {
List<ArticleData> articles =
articleReadService.findArticlesOfAuthorsWithCursor(followdUsers, page);
boolean hasExtra = articles.size() > page.getLimit();
if (hasExtra) {
articles.remove(page.getLimit());
}
if (!page.isNext()) {
Collections.reverse(articles);
}
fillExtraInfo(articles, user);
return new CursorPager<>(articles, page.getDirection(), hasExtra);
}
}
public ArticleDataList findRecentArticles(
String tag, String author, String favoritedBy, Page page, User currentUser) {
List<String> articleIds = articleReadService.queryArticles(tag, author, favoritedBy, page);
int articleCount = articleReadService.countArticle(tag, author, favoritedBy);
if (articleIds.size() == 0) {
@ -70,53 +113,6 @@ public class ArticleQueryService {
}
}
private void fillExtraInfo(List<ArticleData> articles, User currentUser) {
setFavoriteCount(articles);
if (currentUser != null) {
setIsFavorite(articles, currentUser);
setIsFollowingAuthor(articles, currentUser);
}
}
private void setIsFollowingAuthor(List<ArticleData> articles, User currentUser) {
Set<String> followingAuthors = userRelationshipQueryService.followingAuthors(
currentUser.getId(),
articles.stream().map(articleData1 -> articleData1.getProfileData().getId()).collect(toList()));
articles.forEach(articleData -> {
if (followingAuthors.contains(articleData.getProfileData().getId())) {
articleData.getProfileData().setFollowing(true);
}
});
}
private void setFavoriteCount(List<ArticleData> articles) {
List<ArticleFavoriteCount> favoritesCounts = articleFavoritesReadService.articlesFavoriteCount(articles.stream().map(ArticleData::getId).collect(toList()));
Map<String, Integer> countMap = new HashMap<>();
favoritesCounts.forEach(item -> {
countMap.put(item.getId(), item.getCount());
});
articles.forEach(articleData -> articleData.setFavoritesCount(countMap.get(articleData.getId())));
}
private void setIsFavorite(List<ArticleData> articles, User currentUser) {
Set<String> favoritedArticles = articleFavoritesReadService.userFavorites(articles.stream().map(articleData -> articleData.getId()).collect(toList()), currentUser);
articles.forEach(articleData -> {
if (favoritedArticles.contains(articleData.getId())) {
articleData.setFavorited(true);
}
});
}
private void fillExtraInfo(String id, User user, ArticleData articleData) {
articleData.setFavorited(articleFavoritesReadService.isUserFavorite(user.getId(), id));
articleData.setFavoritesCount(articleFavoritesReadService.articleFavoriteCount(id));
articleData.getProfileData().setFollowing(
userRelationshipQueryService.isUserFollowing(
user.getId(),
articleData.getProfileData().getId()));
}
public ArticleDataList findUserFeed(User user, Page page) {
List<String> followdUsers = userRelationshipQueryService.followedUsers(user.getId());
if (followdUsers.size() == 0) {
@ -128,5 +124,64 @@ public class ArticleQueryService {
return new ArticleDataList(articles, count);
}
}
}
private void fillExtraInfo(List<ArticleData> articles, User currentUser) {
setFavoriteCount(articles);
if (currentUser != null) {
setIsFavorite(articles, currentUser);
setIsFollowingAuthor(articles, currentUser);
}
}
private void setIsFollowingAuthor(List<ArticleData> articles, User currentUser) {
Set<String> followingAuthors =
userRelationshipQueryService.followingAuthors(
currentUser.getId(),
articles.stream()
.map(articleData1 -> articleData1.getProfileData().getId())
.collect(toList()));
articles.forEach(
articleData -> {
if (followingAuthors.contains(articleData.getProfileData().getId())) {
articleData.getProfileData().setFollowing(true);
}
});
}
private void setFavoriteCount(List<ArticleData> articles) {
List<ArticleFavoriteCount> favoritesCounts =
articleFavoritesReadService.articlesFavoriteCount(
articles.stream().map(ArticleData::getId).collect(toList()));
Map<String, Integer> countMap = new HashMap<>();
favoritesCounts.forEach(
item -> {
countMap.put(item.getId(), item.getCount());
});
articles.forEach(
articleData -> articleData.setFavoritesCount(countMap.get(articleData.getId())));
}
private void setIsFavorite(List<ArticleData> articles, User currentUser) {
Set<String> favoritedArticles =
articleFavoritesReadService.userFavorites(
articles.stream().map(articleData -> articleData.getId()).collect(toList()),
currentUser);
articles.forEach(
articleData -> {
if (favoritedArticles.contains(articleData.getId())) {
articleData.setFavorited(true);
}
});
}
private void fillExtraInfo(String id, User user, ArticleData articleData) {
articleData.setFavorited(articleFavoritesReadService.isUserFavorite(user.getId(), id));
articleData.setFavoritesCount(articleFavoritesReadService.articleFavoriteCount(id));
articleData
.getProfileData()
.setFollowing(
userRelationshipQueryService.isUserFollowing(
user.getId(), articleData.getProfileData().getId()));
}
}

View File

@ -1,22 +1,25 @@
package io.spring.application;
import io.spring.application.data.CommentData;
import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService;
import io.spring.core.user.User;
import io.spring.infrastructure.mybatis.readservice.CommentReadService;
import org.springframework.stereotype.Service;
import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@Service
public class CommentQueryService {
private CommentReadService commentReadService;
private UserRelationshipQueryService userRelationshipQueryService;
public CommentQueryService(CommentReadService commentReadService, UserRelationshipQueryService userRelationshipQueryService) {
public CommentQueryService(
CommentReadService commentReadService,
UserRelationshipQueryService userRelationshipQueryService) {
this.commentReadService = commentReadService;
this.userRelationshipQueryService = userRelationshipQueryService;
}
@ -26,10 +29,11 @@ public class CommentQueryService {
if (commentData == null) {
return Optional.empty();
} else {
commentData.getProfileData().setFollowing(
commentData
.getProfileData()
.setFollowing(
userRelationshipQueryService.isUserFollowing(
user.getId(),
commentData.getProfileData().getId()));
user.getId(), commentData.getProfileData().getId()));
}
return Optional.ofNullable(commentData);
}
@ -37,8 +41,14 @@ public class CommentQueryService {
public List<CommentData> findByArticleId(String articleId, User user) {
List<CommentData> comments = commentReadService.findByArticleId(articleId);
if (comments.size() > 0 && user != null) {
Set<String> followingAuthors = userRelationshipQueryService.followingAuthors(user.getId(), comments.stream().map(commentData -> commentData.getProfileData().getId()).collect(Collectors.toList()));
comments.forEach(commentData -> {
Set<String> followingAuthors =
userRelationshipQueryService.followingAuthors(
user.getId(),
comments.stream()
.map(commentData -> commentData.getProfileData().getId())
.collect(Collectors.toList()));
comments.forEach(
commentData -> {
if (followingAuthors.contains(commentData.getProfileData().getId())) {
commentData.getProfileData().setFollowing(true);
}
@ -46,4 +56,34 @@ public class CommentQueryService {
}
return comments;
}
public CursorPager<CommentData> findByArticleIdWithCursor(
String articleId, User user, CursorPageParameter page) {
List<CommentData> comments = commentReadService.findByArticleIdWithCursor(articleId, page);
if (comments.isEmpty()) {
return new CursorPager<>(new ArrayList<>(), page.getDirection(), false);
}
if (user != null) {
Set<String> followingAuthors =
userRelationshipQueryService.followingAuthors(
user.getId(),
comments.stream()
.map(commentData -> commentData.getProfileData().getId())
.collect(Collectors.toList()));
comments.forEach(
commentData -> {
if (followingAuthors.contains(commentData.getProfileData().getId())) {
commentData.getProfileData().setFollowing(true);
}
});
}
boolean hasExtra = comments.size() > page.getLimit();
if (hasExtra) {
comments.remove(page.getLimit());
}
if (!page.isNext()) {
Collections.reverse(comments);
}
return new CursorPager<>(comments, page.getDirection(), hasExtra);
}
}

View File

@ -0,0 +1,44 @@
package io.spring.application;
import io.spring.application.CursorPager.Direction;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class CursorPageParameter {
private static final int MAX_LIMIT = 1000;
private int limit = 20;
private String cursor;
private Direction direction;
public CursorPageParameter(String cursor, int limit, Direction direction) {
setLimit(limit);
setCursor(cursor);
setDirection(direction);
}
public boolean isNext() {
return direction == Direction.NEXT;
}
public int getQueryLimit() {
return limit + 1;
}
private void setCursor(String cursor) {
if (cursor == null) {
this.cursor = "";
} else {
this.cursor = cursor;
}
}
private void setLimit(int limit) {
if (limit > MAX_LIMIT) {
this.limit = MAX_LIMIT;
} else if (limit > 0) {
this.limit = limit;
}
}
}

View File

@ -0,0 +1,44 @@
package io.spring.application;
import java.util.List;
import lombok.Getter;
@Getter
public class CursorPager<T extends Node> {
private List<T> data;
private boolean next;
private boolean previous;
public CursorPager(List<T> data, Direction direction, boolean hasExtra) {
this.data = data;
if (direction == Direction.NEXT) {
this.previous = false;
this.next = hasExtra;
} else {
this.next = false;
this.previous = hasExtra;
}
}
public boolean hasNext() {
return next;
}
public boolean hasPrevious() {
return previous;
}
public String getStartCursor() {
return data.isEmpty() ? "" : data.get(0).getCursor();
}
public String getEndCursor() {
return data.isEmpty() ? "" : data.get(data.size() - 1).getCursor();
}
public enum Direction {
PREV,
NEXT
}
}

View File

@ -0,0 +1,5 @@
package io.spring.application;
public interface Node {
String getCursor();
}

View File

@ -1,12 +1,10 @@
package io.spring.application;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Data
@Getter
public class Page {
private static final int MAX_LIMIT = 100;
private int offset = 0;

View File

@ -1,17 +1,16 @@
package io.spring.application.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.joda.time.DateTime;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleData {
public class ArticleData implements io.spring.application.Node {
private String id;
private String slug;
private String title;
@ -22,7 +21,12 @@ public class ArticleData {
private DateTime createdAt;
private DateTime updatedAt;
private List<String> tagList;
@JsonProperty("author")
private ProfileData profileData;
}
@Override
public String getCursor() {
return String.valueOf(getUpdatedAt().getMillis());
}
}

View File

@ -2,6 +2,7 @@ package io.spring.application.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.spring.application.Node;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -10,13 +11,18 @@ import org.joda.time.DateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentData {
public class CommentData implements Node {
private String id;
private String body;
@JsonIgnore
private String articleId;
@JsonIgnore private String articleId;
private DateTime createdAt;
private DateTime updatedAt;
@JsonProperty("author")
private ProfileData profileData;
@Override
public String getCursor() {
return String.valueOf(createdAt.getMillis());
}
}

View File

@ -1,25 +1,42 @@
package io.spring.infrastructure.mybatis.readservice;
import io.spring.application.CursorPageParameter;
import io.spring.application.Page;
import io.spring.application.data.ArticleData;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ArticleReadService {
ArticleData findById(@Param("id") String id);
ArticleData findBySlug(@Param("slug") String slug);
List<String> queryArticles(@Param("tag") String tag, @Param("author") String author, @Param("favoritedBy") String favoritedBy, @Param("page") Page page);
List<String> queryArticles(
@Param("tag") String tag,
@Param("author") String author,
@Param("favoritedBy") String favoritedBy,
@Param("page") Page page);
int countArticle(@Param("tag") String tag, @Param("author") String author, @Param("favoritedBy") String favoritedBy);
int countArticle(
@Param("tag") String tag,
@Param("author") String author,
@Param("favoritedBy") String favoritedBy);
List<ArticleData> findArticles(@Param("articleIds") List<String> articleIds);
List<ArticleData> findArticlesOfAuthors(@Param("authors") List<String> authors, @Param("page") Page page);
List<ArticleData> findArticlesOfAuthors(
@Param("authors") List<String> authors, @Param("page") Page page);
List<ArticleData> findArticlesOfAuthorsWithCursor(
@Param("authors") List<String> authors, @Param("page") CursorPageParameter page);
int countFeedSize(@Param("authors") List<String> authors);
List<String> findArticlesWithCursor(
@Param("tag") String tag,
@Param("author") String author,
@Param("favoritedBy") String favoritedBy,
@Param("page") CursorPageParameter page);
}

View File

@ -1,14 +1,17 @@
package io.spring.infrastructure.mybatis.readservice;
import io.spring.application.CursorPageParameter;
import io.spring.application.data.CommentData;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CommentReadService {
CommentData findById(@Param("id") String id);
List<CommentData> findByArticleId(@Param("articleId") String articleId);
List<CommentData> findByArticleIdWithCursor(
@Param("articleId") String articleId, @Param("page") CursorPageParameter page);
}

View File

@ -0,0 +1 @@
spring.datasource.url=jdbc:sqlite::memory:

View File

@ -1,3 +1,7 @@
spring.datasource.url=jdbc:sqlite:dev.db
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.datasource.username=
spring.datasource.password=
spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true
image.default=https://static.productionready.io/images/smiley-cyrus.jpg
jwt.secret=nRvyYC4soFxBdZ-F-5Nnzz5USXstR1YylsTd-mA0aKtI9HUlriGrtkf-TiuDapkLiUCogO3JOK7kwZisrHp6wA
@ -9,5 +13,6 @@ mybatis.configuration.use-generated-keys=true
mybatis.type-handlers-package=io.spring.infrastructure.mybatis
mybatis.mapper-locations=mapper/*.xml
logging.level.io.spring.infrastructure.mybatis.readservice.ArticleReadService=DEBUG
logging.level.io.spring.infrastructure.mybatis.mapper=DEBUG
# Uncomment the following line to enable and allow access to the h2-console
#spring.h2.console.enabled=true

View File

@ -24,6 +24,17 @@
left join tags T on T.id = AT.tag_id
left join users U on U.id = A.user_id
</sql>
<sql id="selectArticleIds">
select
DISTINCT(A.id) articleId, A.created_at
from
articles A
left join article_tags AT on A.id = AT.article_id
left join tags T on T.id = AT.tag_id
left join article_favorites AF on AF.article_id = A.id
left join users AU on AU.id = A.user_id
left join users AFU on AFU.id = AF.user_id
</sql>
<select id="findById" resultMap="transfer.data.articleData">
<include refid="selectArticleData"/>
@ -34,15 +45,7 @@
where A.slug = #{slug}
</select>
<select id="queryArticles" resultMap="articleId">
select
DISTINCT(A.id) articleId, A.created_at
from
articles A
left join article_tags AT on A.id = AT.article_id
left join tags T on T.id = AT.tag_id
left join article_favorites AF on AF.article_id = A.id
left join users AU on AU.id = A.user_id
left join users AFU on AFU.id = AF.user_id
<include refid="selectArticleIds" />
<where>
<if test="tag != null">
T.name = #{tag}
@ -101,6 +104,55 @@
#{id}
</foreach>
</select>
<select id="findArticlesWithCursor" resultType="java.lang.String">
<include refid="selectArticleIds" />
<where>
<if test="tag != null">
T.name = #{tag}
</if>
<if test="author != null">
AND AU.username = #{author}
</if>
<if test="favoritedBy != null">
AND AFU.username = #{favoritedBy}
</if>
<if test='page.cursor != "" and page.direction.name() == "NEXT"'>
AND A.created_at &lt; #{page.cursor}
</if>
<if test='page.cursor != "" and page.direction.name() == "PREV"'>
AND A.created_at > #{page.cursor}
</if>
</where>
<if test='page.direction.name() == "NEXT"'>
order by A.created_at desc
</if>
<if test='page.direction.name() == "PREV"'>
order by A.created_at asc
</if>
limit #{page.queryLimit}
</select>
<select id="findArticlesOfAuthorsWithCursor" resultMap="transfer.data.articleData">
<include refid="selectArticleData"/>
<where>
A.user_id in
<foreach index="index" collection="authors" item="id" open="(" separator="," close=")">
#{id}
</foreach>
<if test='page.cursor != "" and page.direction.name() == "NEXT"'>
AND A.created_at &lt; #{page.cursor}
</if>
<if test='page.cursor != "" and page.direction.name() == "PREV"'>
AND A.created_at > #{page.cursor}
</if>
<if test='page.direction.name() == "NEXT"'>
order by A.created_at desc
</if>
<if test='page.direction.name() == "PREV"'>
order by A.created_at asc
</if>
</where>
limit #{page.queryLimit}
</select>
<resultMap id="articleId" type="string">
<id javaType="string" column="articleId"/>

View File

@ -21,4 +21,22 @@
<include refid="selectCommentData"/>
where C.article_id = #{articleId}
</select>
<select id="findByArticleIdWithCursor" resultMap="transfer.data.commentData">
<include refid="selectCommentData"/>
<where>
C.article_id = #{articleId}
<if test='page.cursor != "" and page.direction.name() == "NEXT"'>
AND C.created_at &lt; #{page.cursor}
</if>
<if test='page.cursor != "" and page.direction.name() == "PREV"'>
AND C.created_at > #{page.cursor}
</if>
</where>
<if test='page.direction.name() == "NEXT"'>
order by C.created_at desc
</if>
<if test='page.direction.name() == "PREV"'>
order by C.created_at asc
</if>
</select>
</mapper>

View File

@ -6,6 +6,9 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import io.spring.application.ArticleQueryService;
import io.spring.application.CursorPageParameter;
import io.spring.application.CursorPager;
import io.spring.application.CursorPager.Direction;
import io.spring.application.Page;
import io.spring.application.data.ArticleData;
import io.spring.application.data.ArticleDataList;
@ -16,6 +19,7 @@ import io.spring.core.favorite.ArticleFavoriteRepository;
import io.spring.core.user.FollowRelation;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisArticleFavoriteRepository;
import io.spring.infrastructure.repository.MyBatisArticleRepository;
import io.spring.infrastructure.repository.MyBatisUserRepository;
@ -24,21 +28,16 @@ import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@MybatisTest
@Import({
ArticleQueryService.class,
MyBatisUserRepository.class,
MyBatisArticleRepository.class,
MyBatisArticleFavoriteRepository.class
})
public class ArticleQueryServiceTest {
public class ArticleQueryServiceTest extends DbTestBase {
@Autowired private ArticleQueryService queryService;
@Autowired private ArticleRepository articleRepository;
@ -111,6 +110,40 @@ public class ArticleQueryServiceTest {
assertEquals(nodata.getArticleDatas().size(), 0);
}
@Test
public void should_get_default_article_list_by_cursor() {
Article anotherArticle =
new Article(
"new article",
"desc",
"body",
Arrays.asList("test"),
user.getId(),
new DateTime().minusHours(1));
articleRepository.save(anotherArticle);
CursorPager<ArticleData> recentArticles =
queryService.findRecentArticlesWithCursor(
null, null, null, new CursorPageParameter("", 20, Direction.NEXT), user);
assertEquals(recentArticles.getData().size(), 2);
assertEquals(recentArticles.getData().get(0).getId(), article.getId());
CursorPager<ArticleData> nodata =
queryService.findRecentArticlesWithCursor(
null,
null,
null,
new CursorPageParameter(recentArticles.getEndCursor(), 20, Direction.NEXT),
user);
assertEquals(nodata.getData().size(), 0);
assertEquals(nodata.getStartCursor(), "");
CursorPager<ArticleData> prevArticles =
queryService.findRecentArticlesWithCursor(
null, null, null, new CursorPageParameter("", 20, Direction.PREV), user);
assertEquals(prevArticles.getData().size(), 2);
}
@Test
public void should_query_article_by_author() {
User anotherUser = new User("other@email.com", "other", "123", "", "");

View File

@ -12,6 +12,7 @@ import io.spring.core.comment.CommentRepository;
import io.spring.core.user.FollowRelation;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisArticleRepository;
import io.spring.infrastructure.repository.MyBatisCommentRepository;
import io.spring.infrastructure.repository.MyBatisUserRepository;
@ -20,21 +21,16 @@ import java.util.List;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
@MybatisTest
@RunWith(SpringRunner.class)
@Import({
MyBatisCommentRepository.class,
MyBatisUserRepository.class,
CommentQueryService.class,
MyBatisArticleRepository.class
})
public class CommentQueryServiceTest {
public class CommentQueryServiceTest extends DbTestBase {
@Autowired private CommentRepository commentRepository;
@Autowired private UserRepository userRepository;

View File

@ -1,29 +1,22 @@
package io.spring.application.profile;
import static org.junit.Assert.assertTrue;
import io.spring.application.ProfileQueryService;
import io.spring.application.data.ProfileData;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisUserRepository;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Optional;
import static org.junit.Assert.assertTrue;
@RunWith(SpringRunner.class)
@MybatisTest
@Import({ProfileQueryService.class, MyBatisUserRepository.class})
public class ProfileQueryServiceTest {
@Autowired
private ProfileQueryService profileQueryService;
@Autowired
private UserRepository userRepository;
public class ProfileQueryServiceTest extends DbTestBase {
@Autowired private ProfileQueryService profileQueryService;
@Autowired private UserRepository userRepository;
@Test
public void should_fetch_profile_success() {
@ -31,7 +24,8 @@ public class ProfileQueryServiceTest {
User profileUser = new User("p@test.com", "p", "123", "", "");
userRepository.save(profileUser);
Optional<ProfileData> optional = profileQueryService.findByUsername(profileUser.getUsername(), currentUser);
Optional<ProfileData> optional =
profileQueryService.findByUsername(profileUser.getUsername(), currentUser);
assertTrue(optional.isPresent());
}
}

View File

@ -5,19 +5,15 @@ import static org.junit.Assert.assertTrue;
import io.spring.application.TagsQueryService;
import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisArticleRepository;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@MybatisTest
@Import({TagsQueryService.class, MyBatisArticleRepository.class})
public class TagsQueryServiceTest {
public class TagsQueryServiceTest extends DbTestBase {
@Autowired private TagsQueryService tagsQueryService;
@Autowired private ArticleRepository articleRepository;

View File

@ -0,0 +1,14 @@
package io.spring.infrastructure;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = Replace.NONE)
@MybatisTest
@RunWith(SpringRunner.class)
public abstract class DbTestBase {}

View File

@ -13,11 +13,13 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@SpringBootTest
@AutoConfigureTestDatabase
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@RunWith(SpringRunner.class)
public class ArticleRepositoryTransactionTest {
@Autowired private ArticleRepository articleRepository;

View File

@ -10,22 +10,18 @@ import io.spring.core.article.ArticleRepository;
import io.spring.core.article.Tag;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisArticleRepository;
import io.spring.infrastructure.repository.MyBatisUserRepository;
import java.util.Arrays;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
@MybatisTest
@RunWith(SpringRunner.class)
@Import({MyBatisArticleRepository.class, MyBatisUserRepository.class})
public class MyBatisArticleRepositoryTest {
public class MyBatisArticleRepositoryTest extends DbTestBase {
@Autowired private ArticleRepository articleRepository;
@Autowired private UserRepository userRepository;

View File

@ -1,26 +1,20 @@
package io.spring.infrastructure.comment;
import io.spring.core.comment.Comment;
import io.spring.core.comment.CommentRepository;
import io.spring.infrastructure.repository.MyBatisCommentRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@MybatisTest
@RunWith(SpringRunner.class)
import io.spring.core.comment.Comment;
import io.spring.core.comment.CommentRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisCommentRepository;
import java.util.Optional;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
@Import({MyBatisCommentRepository.class})
public class MyBatisCommentRepositoryTest {
@Autowired
private CommentRepository commentRepository;
public class MyBatisCommentRepositoryTest extends DbTestBase {
@Autowired private CommentRepository commentRepository;
@Test
public void should_create_and_fetch_comment_success() {

View File

@ -1,24 +1,19 @@
package io.spring.infrastructure.favorite;
import io.spring.core.favorite.ArticleFavorite;
import io.spring.core.favorite.ArticleFavoriteRepository;
import io.spring.infrastructure.repository.MyBatisArticleFavoriteRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@RunWith(SpringRunner.class)
@MybatisTest
import io.spring.core.favorite.ArticleFavorite;
import io.spring.core.favorite.ArticleFavoriteRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisArticleFavoriteRepository;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
@Import({MyBatisArticleFavoriteRepository.class})
public class MyBatisArticleFavoriteRepositoryTest {
@Autowired
private ArticleFavoriteRepository articleFavoriteRepository;
public class MyBatisArticleFavoriteRepositoryTest extends DbTestBase {
@Autowired private ArticleFavoriteRepository articleFavoriteRepository;
@Autowired
private io.spring.infrastructure.mybatis.mapper.ArticleFavoriteMapper articleFavoriteMapper;
@ -27,7 +22,8 @@ public class MyBatisArticleFavoriteRepositoryTest {
public void should_save_and_fetch_articleFavorite_success() {
ArticleFavorite articleFavorite = new ArticleFavorite("123", "456");
articleFavoriteRepository.save(articleFavorite);
assertNotNull(articleFavoriteMapper.find(articleFavorite.getArticleId(), articleFavorite.getUserId()));
assertNotNull(
articleFavoriteMapper.find(articleFavorite.getArticleId(), articleFavorite.getUserId()));
}
@Test

View File

@ -1,29 +1,23 @@
package io.spring.infrastructure.user;
import io.spring.core.user.FollowRelation;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.infrastructure.repository.MyBatisUserRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(SpringRunner.class)
@MybatisTest
import io.spring.core.user.FollowRelation;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.infrastructure.DbTestBase;
import io.spring.infrastructure.repository.MyBatisUserRepository;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
@Import(MyBatisUserRepository.class)
public class MyBatisUserRepositoryTest {
@Autowired
private UserRepository userRepository;
public class MyBatisUserRepositoryTest extends DbTestBase {
@Autowired private UserRepository userRepository;
private User user;
@Before