创建 Book-Category-Book-Ratings 域,加载和转换 JSON 数据,并实现 Books API。
在本课中,学生将学习
本课将首先用它们各自的 Spring 存储库填充 Book、Category 和 BookRating 模型。
我们将从 Category
开始。一个 Book
属于一个或多个类别。 Category
有一个名称,我们将从 src/main/resources/data/books
中的 JSON 数据文件中获取。与之前一样,我们将类映射到 Redis 哈希。使用以下内容添加文件 src/main/java/com/redislabs/edu/redi2read/models/Category.java
package com.redislabs.edu.redi2read.models;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@RedisHash
public class Category {
@Id
private String id;
private String name;
}
相应的存储库扩展了 Spring 的 CrudRepository。使用以下内容添加文件 src/main/java/com/redislabs/edu/redi2read/repositories/CategoryRepository.java
package com.redislabs.edu.redi2read.repositories;
import com.redislabs.edu.redi2read.models.Category;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CategoryRepository extends CrudRepository<Category, String> {
}
Book 模型直接映射到 src/main/resources/data/books
中的 *.json 文件中的 JSON 有效负载。例如,下面显示的 JSON 对象来自文件 redis_0.json
{
"pageCount": 228,
"thumbnail": "https://books.google.com/books/content?id=NsseEAAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api",
"price": 9.95,
"subtitle": "Explore Redis - Its Architecture, Data Structures and Modules like Search, JSON, AI, Graph, Timeseries (English Edition)",
"description": "Complete reference guide to Redis KEY FEATURES ● Complete coverage of Redis Modules.",
"language": "en",
"currency": "USD",
"id": "8194837766",
"title": "Redis(r) Deep Dive",
"infoLink": "https://play.google.com/store/books/details?id=NsseEAAAQBAJ&source=gbs_api",
"authors": ["Suyog Dilip Kale", "Chinmay Kulkarni"]
},
...
}
类别名称从文件名中提取为“redis”。这同样适用于来自以下文件的任何书籍:redis_0.json
、redis_1.json
、redis_2.json
和 redis_3.json
。Book 类包含一个 Set<Category>
,它目前将包含从文件名中提取的单个类别。 Set<String>
用于作者从有效负载中的“authors”JSON 数组中映射。使用以下内容添加文件 src/main/java/com/redislabs/edu/redi2read/repositories/Book.java
package com.redislabs.edu.redi2read.models;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.redis.core.RedisHash;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@RedisHash
public class Book {
@Id
@EqualsAndHashCode.Include
private String id;
private String title;
private String subtitle;
private String description;
private String language;
private Long pageCount;
private String thumbnail;
private Double price;
private String currency;
private String infoLink;
private Set<String> authors;
@Reference
private Set<Category> categories = new HashSet<Category>();
public void addCategory(Category category) {
categories.add(category);
}
}
在 BookRepository
中,我们引入了 PaginationAndSortingRepository
的使用。 PaginationAndSortingRepository
扩展了 CrudRepository
接口,并添加了其他方法以简化对实体的分页访问。当我们实现 BookController
时,我们将进一步了解 PagingAndSortingRepository
的用法。使用以下内容添加文件 src/main/java/com/redislabs/edu/redi2read/repositories/BookRepository.java
package com.redislabs.edu.redi2read.repositories;
import com.redislabs.edu.redi2read.models.Book;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BookRepository extends PagingAndSortingRepository<Book, String> {
}
BookRating
模型表示用户对书籍的评分。我们实施了传统的 5 星级评分系统,作为多对多关系。 BookRating
模型在关系模型中起着等同于连接表或桥接表的作用。 BookRating
位于多对多关系中的另外两个实体 (Book 和 User) 之间。它的目的是为这两个其他实体 (Book 和 User) 的每种组合存储一条记录。我们使用 @Reference
注释来保留与 Book 和 User 模型的链接(相当于在关系数据库中具有外键)。使用以下内容添加文件 src/main/java/com/redislabs/edu/redi2read/models/BookRating.java
package com.redislabs.edu.redi2read.models;
import javax.validation.constraints.NotNull;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.redis.core.RedisHash;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@RedisHash
public class BookRating {
@Id
private String id;
@NotNull
@Reference
private User user;
@NotNull
@Reference
private Book book;
@NotNull
private Integer rating;
}
相应的存储库只是扩展了 Spring 的 CrudRepository。使用以下内容添加文件 src/main/java/com/redislabs/edu/redi2read/repositories/BookRatingRepository.java
package com.redislabs.edu.redi2read.repositories;
import com.redislabs.edu.redi2read.models.BookRating;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BookRatingRepository extends CrudRepository<BookRating, String> {
}
现在我们已经定义了模型和存储库,让我们从提供的 JSON 数据中加载书籍,这些数据位于 src/main/resources/data/books
目录中。我们将创建一个 CommandLineRunner
来迭代 data/books
目录中的每个 .json 文件。我们将使用 Jackson 将每个文件的内容映射到 Book 对象。我们将使用文件名中最后一个下划线之前的字符创建一个类别。如果还没有该名称的类别,我们将创建一个。然后将类别添加到书籍的类别集中。使用以下内容添加文件 src/main/java/com/redislabs/edu/redi2read/boot/CreateBooks.java
package com.redislabs.edu.redi2read.boot;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.redislabs.edu.redi2read.models.Book;
import com.redislabs.edu.redi2read.models.Category;
import com.redislabs.edu.redi2read.repositories.BookRepository;
import com.redislabs.edu.redi2read.repositories.CategoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(3)
@Slf4j
public class CreateBooks implements CommandLineRunner {
@Autowired
private BookRepository bookRepository;
@Autowired
private CategoryRepository categoryRepository;
@Override
public void run(String... args) throws Exception {
if (bookRepository.count() == 0) {
ObjectMapper mapper = new ObjectMapper();
TypeReference<List<Book>> typeReference = new TypeReference<List<Book>>() {
};
List<File> files = //
Files.list(Paths.get(getClass().getResource("/data/books").toURI())) //
.filter(Files::isRegularFile) //
.filter(path -> path.toString().endsWith(".json")) //
.map(java.nio.file.Path::toFile) //
.collect(Collectors.toList());
Map<String, Category> categories = new HashMap<String, Category>();
files.forEach(file -> {
try {
log.info(">>>> Processing Book File: " + file.getPath());
String categoryName = file.getName().substring(0, file.getName().lastIndexOf("_"));
log.info(">>>> Category: " + categoryName);
Category category;
if (!categories.containsKey(categoryName)) {
category = Category.builder().name(categoryName).build();
categoryRepository.save(category);
categories.put(categoryName, category);
} else {
category = categories.get(categoryName);
}
InputStream inputStream = new FileInputStream(file);
List<Book> books = mapper.readValue(inputStream, typeReference);
books.stream().forEach((book) -> {
book.addCategory(category);
bookRepository.save(book);
});
log.info(">>>> " + books.size() + " Books Saved!");
} catch (IOException e) {
log.info("Unable to import books: " + e.getMessage());
}
});
log.info(">>>> Loaded Book Data and Created books...");
}
}
}
这里有很多东西要解包,所以让我们从头开始
ObjectMapper
和 TypeReference
来执行映射。Map
,将 String
映射到 Category
对象,以便在处理文件时收集类别,并快速确定是否已创建类别。现在,我们可以实现 BookController
的初始版本:我们的 Bookstore Catalog API。这个 BookController
的第一个版本将包含三个端点
src/main/java/com/redislabs/edu/redi2read/controllers/BookController.java
,其内容如下:package com.redislabs.edu.redi2read.controllers;
import com.redislabs.edu.redi2read.models.Book;
import com.redislabs.edu.redi2read.models.Category;
import com.redislabs.edu.redi2read.repositories.BookRepository;
import com.redislabs.edu.redi2read.repositories.CategoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/books")
public class BookController {
@Autowired
private BookRepository bookRepository;
@Autowired
private CategoryRepository categoryRepository;
@GetMapping
public Iterable<Book> all() {
return bookRepository.findAll();
}
@GetMapping("/categories")
public Iterable<Category> getCategories() {
return categoryRepository.findAll();
}
@GetMapping("/{isbn}")
public Book get(@PathVariable("isbn") String isbn) {
return bookRepository.findById(isbn).get();
}
}
要获取所有书籍,我们可以向 https://localhost:8080/api/books/
发出 GET 请求。这个端点在 all 方法中实现,该方法调用 BookRepository 的 findAll 方法。使用 curl
curl --location --request GET 'https://localhost:8080/api/books/'
结果是一个包含书籍的 JSON 对象数组
[
{
"id": "1783980117",
"title": "RESTful Java Web Services Security",
"subtitle": null,
"description": "A sequential and easy-to-follow guide which allows you to understand the concepts related to securing web apps/services quickly and efficiently, since each topic is explained and described with the help of an example and in a step-by-step manner, helping you to easily implement the examples in your own projects. This book is intended for web application developers who use RESTful web services to power their websites. Prior knowledge of RESTful is not mandatory, but would be advisable.",
"language": "en",
"pageCount": 144,
"thumbnail": "https://books.google.com/books/content?id=Dh8ZBAAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api",
"price": 11.99,
"currency": "USD",
"infoLink": "https://play.google.com/store/books/details?id=Dh8ZBAAAQBAJ&source=gbs_api",
"authors": [
"Andrés Salazar C.",
"René Enríquez"
],
"categories": [
{
"id": "f2ada1e2-7c18-4d90-bfe7-e321b650c0a3",
"name": "redis"
}
]
},
...
]
要获取特定书籍,我们可以向 https://localhost:8080/api/books/{isbn}
发出 GET 请求。这个端点在 get 方法中实现,该方法调用 BookRepository 的 findById 方法。使用 curl
curl --location --request GET 'https://localhost:8080/api/books/1680503545'
结果是一个包含书籍的 JSON 对象
{
"id": "1680503545",
"title": "Functional Programming in Java",
"subtitle": "Harnessing the Power Of Java 8 Lambda Expressions",
"description": "Intermediate level, for programmers fairly familiar with Java, but new to the functional style of programming and lambda expressions. Get ready to program in a whole new way. ...",
"language": "en",
"pageCount": 196,
"thumbnail": "https://books.google.com/books/content?id=_g5QDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api",
"price": 28.99,
"currency": "USD",
"infoLink": "https://play.google.com/store/books/details?id=_g5QDwAAQBAJ&source=gbs_api",
"authors": ["Venkat Subramaniam"],
"categories": [
{
"id": "9d5c025e-bf38-4b50-a971-17e0b7408385",
"name": "java"
}
]
}
要获取所有类别,我们可以向 https://localhost:8080/api/books/categories
发出 GET 请求。它在 getCategories
方法中实现,该方法调用 CategoriesRepository
的 findAll
方法。使用 curl
curl --location --request GET 'https://localhost:8080/api/books/categories'
结果是一个包含类别的 JSON 对象数组
[
{
"id": "2fd916fe-7ff8-44c7-9f86-ca388565256c",
"name": "mongodb"
},
{
"id": "9615a135-7472-48fc-b8ac-a5516a2c8b22",
"name": "dynamodb"
},
{
"id": "f2ada1e2-7c18-4d90-bfe7-e321b650c0a3",
"name": "redis"
},
{
"id": "08fc8148-d924-4d2e-af7e-f5fe6f2861f0",
"name": "elixir"
},
{
"id": "b6a0b57b-ebb8-4d98-9352-8236256dbc27",
"name": "microservices"
},
{
"id": "7821fd6a-ec94-4ac6-8089-a480a7c7f2ee",
"name": "elastic_search"
},
{
"id": "f2be1bc3-1700-45f5-a300-2c4cf2f90583",
"name": "hbase"
},
{
"id": "31c8ea64-cad2-40d9-b0f6-30b8ea6fcbfb",
"name": "reactjs"
},
{
"id": "5e527af7-93a1-4c00-8f20-f89e89a213e8",
"name": "apache_spark"
},
{
"id": "9d5c025e-bf38-4b50-a971-17e0b7408385",
"name": "java"
},
{
"id": "bcb2a01c-9b0a-4846-b1be-670168b5d768",
"name": "clojure"
},
{
"id": "aba53bb9-7cfa-4b65-8900-8c7e857311c6",
"name": "couchbase"
},
{
"id": "bd1b2877-1564-4def-b3f7-18871165ff10",
"name": "riak"
},
{
"id": "47d9a769-bbc2-4068-b27f-2b800bec1565",
"name": "kotlin"
},
{
"id": "400c8f5a-953b-4b8b-b21d-045535d8084d",
"name": "nosql_big_data"
},
{
"id": "06bc25ff-f2ab-481b-a4d9-819552dea0e0",
"name": "javascript"
}
]
接下来,我们将创建一组随机的书籍评分。在课程的后面部分,我们将使用它们作为示例。按照我们之前用 CommandLineRunner
播种 Redis 的方法,添加文件 src/main/java/com/redislabs/edu/redi2read/boot/CreateBookRatings.java
,其内容如下
package com.redislabs.edu.redi2read.boot;
import java.util.Random;
import java.util.stream.IntStream;
import com.redislabs.edu.redi2read.models.Book;
import com.redislabs.edu.redi2read.models.BookRating;
import com.redislabs.edu.redi2read.models.User;
import com.redislabs.edu.redi2read.repositories.BookRatingRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(4)
@Slf4j
public class CreateBookRatings implements CommandLineRunner {
@Value("${app.numberOfRatings}")
private Integer numberOfRatings;
@Value("${app.ratingStars}")
private Integer ratingStars;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private BookRatingRepository bookRatingRepo;
@Override
public void run(String... args) throws Exception {
if (bookRatingRepo.count() == 0) {
Random random = new Random();
IntStream.range(0, numberOfRatings).forEach(n -> {
String bookId = redisTemplate.opsForSet().randomMember(Book.class.getName());
String userId = redisTemplate.opsForSet().randomMember(User.class.getName());
int stars = random.nextInt(ratingStars) + 1;
User user = new User();
user.setId(userId);
Book book = new Book();
book.setId(bookId);
BookRating rating = BookRating.builder() //
.user(user) //
.book(book) //
.rating(stars).build();
bookRatingRepo.save(rating);
});
log.info(">>>> BookRating created...");
}
}
}
这个 CommandLineRunner
为一组随机的书籍和用户创建可配置数量的随机评分。我们使用 RedisTemplate.opsForSet().randomMember()
从用户和书籍集合中请求随机 ID。然后我们选择一个介于 1 和评分系统中的总星数之间的随机整数来创建评分。这个类介绍了 @Value
注解的使用,它将从应用程序的属性文件中获取 String 参数 ${foo}
内的属性。在文件 src/main/resources/application.properties
中添加以下值
app.numberOfRatings=5000
app.ratingStars=5
当我们拥有大量数据集并希望将其以更小的块呈现给用户时,分页非常有用。正如我们在本节课的前面所学到的那样,BookRepository
扩展了 PagingAndSortingRepository
,后者构建在 CrudRepository
之上。在本节中,我们将重构 BookController
的 all 方法,使其使用 PagingAndSortingRepository
的分页功能。用以下内容替换之前创建的 all 方法
@GetMapping
public ResponseEntity<Map<String, Object>> all( //
@RequestParam(defaultValue = "0") Integer page, //
@RequestParam(defaultValue = "10") Integer size //
) {
Pageable paging = PageRequest.of(page, size);
Page<Book> pagedResult = bookRepository.findAll(paging);
List<Book> books = pagedResult.hasContent() ? pagedResult.getContent() : Collections.emptyList();
Map<String, Object> response = new HashMap<>();
response.put("books", books);
response.put("page", pagedResult.getNumber());
response.put("pages", pagedResult.getTotalPages());
response.put("total", pagedResult.getTotalElements());
return new ResponseEntity<>(response, new HttpHeaders(), HttpStatus.OK);
}
让我们分解重构过程
ResponseEntity
,它是 HttpEntity
的扩展,它使我们能够控制 HTTP 状态代码、头和正文。Map<String,Object>
包裹起来,以便返回书籍集合以及分页数据。Pageable
和 PageRequest
抽象来构造分页请求。findAll
方法并传入 Pageable
分页请求,得到一个 Page<Book>
结果。ResponseEntity
中。让我们使用 curl 发起一个分页请求,如下所示
curl --location --request GET 'https://localhost:8080/api/books/?size=25&page=2'
传递页面大小为 25 并请求第 2 页,我们得到以下结果
{
"total": 2403,
"books": [
{
"id": "1786469960",
"title": "Data Visualization with D3 4.x Cookbook",
"subtitle": null,
"description": "Discover over 65 recipes to help you create breathtaking data visualizations using the latest features of D3...",
"language": "en",
"pageCount": 370,
"thumbnail": "https://books.google.com/books/content?id=DVQoDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api",
"price": 22.39,
"currency": "USD",
"infoLink": "https://play.google.com/store/books/details?id=DVQoDwAAQBAJ&source=gbs_api",
"authors": [
"Nick Zhu"
],
"categories": [
{
"id": "f2ada1e2-7c18-4d90-bfe7-e321b650c0a3",
"name": "redis"
}
]
},
{
"id": "111871735X",
"title": "Android Programming",
"subtitle": "Pushing the Limits",
"description": "Unleash the power of the Android OS and build the kinds ofbrilliant, innovative apps users love to use ...",
"language": "en",
"pageCount": 432,
"thumbnail": "https://books.google.com/books/content?id=SUWPAQAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api",
"price": 30.0,
"currency": "USD",
"infoLink": "https://play.google.com/store/books/details?id=SUWPAQAAQBAJ&source=gbs_api",
"authors": [
"Erik Hellman"
],
"categories": [
{
"id": "47d9a769-bbc2-4068-b27f-2b800bec1565",
"name": "kotlin"
}
]
},