使用 Redis Stack将基于 JSON 的域模型添加到 Redi2Read。
在本课中,您将学习如何
CartController
。如果您遇到困难:截至 Jedis 4.0.0 该库已弃用。其功能已合并到 Jedis 中。
我们将实现 Cart
和 CartItem
模型,它们由一个自定义 Spring 存储库支持,该存储库使用 Redis JSON API(通过 JRedisJSON 客户端库)。
我们将用户的购物车表示为一个包含购物车商品子文档的 JSON 文档。如您在类图中所见,Cart
包含零个或多个 CartItems
,并且它属于 User
。
Redis Stack 扩展了 Redis OSS 的核心功能,并为调试等提供了完整的开发者体验。除了 Redis OSS 的所有功能外,Redis Stack 还支持
截至 Jedis 4.0.0 该库已弃用。其功能已合并到 Jedis 中。
JRedisJSON (https://github.com/RedisJSON/JRedisJSON) 是一个 Java 客户端,它提供对 Redis 的 JSON API 的访问,并使用 Google 的 GSON 库提供 Java 序列化。
我们将使用 JRedisJSON 的 SNAPSHOT 版本来利用最近引入的更高级的 JSON 操作功能。将 snapshots-repo 添加到您的 Maven POM 中
<repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
And then the JRedisJSON dependency to the dependencies block:
<dependency>
<groupId>com.redislabs</groupId>
<artifactId>jrejson</artifactId>
<version>1.4.0-SNAPSHOT</version>
</dependency>
我们将从 CartItem
模型开始。它包含有关 Cart
中的 Book
的信息;它存储 Book
ISBN(id)、价格和添加到购物车的数量。添加文件 src/main/java/com/redislabs/edu/redi2read/models/CartItem.java
,其内容如下
package com.redislabs.edu.redi2read.models;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class CartItem {
private String isbn;
private Double price;
private Long quantity;
}
购物车模型包含拥有用户的 ID 和一组 CartItems
。实用方法用于返回购物车中的商品总数和总成本。添加文件 src/main/java/com/redislabs/edu/redi2read/models/Cart.java
,其内容如下
package com.redislabs.edu.redi2read.models;
import java.util.Set;
import lombok.Builder;
import lombok.Data;
import lombok.Singular;
@Data
@Builder
public class Cart {
private String id;
private String userId;
@Singular
private Set<CartItem> cartItems;
public Integer count() {
return getCartItems().size();
}
public Double getTotal() {
return cartItems //
.stream() //
.mapToDouble(ci -> ci.getPrice() * ci.getQuantity()) //
.sum();
}
}
用户结账后,我们需要跟踪用户现在拥有的书籍。为了简化起见,我们将 Set<Book>
添加到 User
模型中,并用 @Reference
注解进行注释。我们还将包括一个将书籍添加到用户拥有的书籍集合中的实用方法。对 User 模型进行以下更改
// ...
@RedisHash
public class User {
//...
@Reference
@JsonIdentityReference(alwaysAsId = true)
private Set<Role> roles = new HashSet<Role>();
public void addRole(Role role) {
roles.add(role);
}
@Reference
@JsonIdentityReference(alwaysAsId = true)
private Set<Book> books = new HashSet<Book>();
public void addBook(Book book) {
books.add(book);
}
}
在 Redis 序列化的情况下,@Reference
注解适用于我们的 Set
,但您可能已经注意到,角色被 Jackson 完整地序列化到生成的 JSON 负载中。我们将添加 @JsonIdentityReference
,并将 alwaysAsId
参数设置为 true
,在目标类(Book
和 Role
)中提供适当的元信息的情况下,这将使 Jackson 将这些对象的集合序列化为 ID。@JsonIdentityInfo
注解允许我们使用 id
属性设置一个生成器(ObjectIdGenerator.PropertyGenerator
),以指导在存在 @JsonIdentityReference
注解的情况下如何进行序列化。按照所示将注解添加到 Book
模型中
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
@RedisHash
public class Book {
//...
}
同样,我们将 @JsonIdentityInfo
添加到 Role
模型中
@Data
@Builder
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
@RedisHash
public class Role {
//...
}
现在,当在 REST 控制器中进行 JSON 序列化时,用户集合将包含角色作为角色 ID 的 JSON 数组。用户集合还将包含新添加的书籍集合,作为书籍 ID 的数组。
下面,我们提供了一个 Spring 的 CrudRepository 的实现,以便我们可以实现我们的服务和控制器。将文件 src/main/java/com/redislabs/edu/redi2read/repositories/CartRepository.java 添加到以下内容中
package com.redislabs.edu.redi2read.repositories;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import com.redislabs.edu.redi2read.models.Cart;
import com.redislabs.modules.rejson.JReJSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public class CartRepository implements CrudRepository<Cart, String> {
private JReJSON redisJson = new JReJSON();
private final static String idPrefix = Cart.class.getName();
@Autowired
private RedisTemplate<String, String> template;
private SetOperations<String, String> redisSets() {
return template.opsForSet();
}
private HashOperations<String, String, String> redisHash() {
return template.opsForHash();
}
@Override
public <S extends Cart> S save(S cart) {
// set cart id
if (cart.getId() == null) {
cart.setId(UUID.randomUUID().toString());
}
String key = getKey(cart);
redisJson.set(key, cart);
redisSets().add(idPrefix, key);
redisHash().put("carts-by-user-id-idx", cart.getUserId().toString(), cart.getId().toString());
return cart;
}
@Override
public <S extends Cart> Iterable<S> saveAll(Iterable<S> carts) {
return StreamSupport //
.stream(carts.spliterator(), false) //
.map(cart -> save(cart)) //
.collect(Collectors.toList());
}
@Override
public Optional<Cart> findById(String id) {
Cart cart = redisJson.get(getKey(id), Cart.class);
return Optional.ofNullable(cart);
}
@Override
public boolean existsById(String id) {
return template.hasKey(getKey(id));
}
@Override
public Iterable<Cart> findAll() {
String[] keys = redisSets().members(idPrefix).stream().toArray(String[]::new);
return (Iterable<Cart>) redisJson.mget(Cart.class, keys);
}
@Override
public Iterable<Cart> findAllById(Iterable<String> ids) {
String[] keys = StreamSupport.stream(ids.spliterator(), false) //
.map(id -> getKey(id)).toArray(String[]::new);
return (Iterable<Cart>) redisJson.mget(Cart.class, keys);
}
@Override
public long count() {
return redisSets().size(idPrefix);
}
@Override
public void deleteById(String id) {
redisJson.del(getKey(id));
}
@Override
public void delete(Cart cart) {
deleteById(cart.getId());
}
@Override
public void deleteAll(Iterable<? extends Cart> carts) {
List<String> keys = StreamSupport //
.stream(carts.spliterator(), false) //
.map(cart -> idPrefix + cart.getId()) //
.collect(Collectors.toList());
redisSets().getOperations().delete(keys);
}
@Override
public void deleteAll() {
redisSets().getOperations().delete(redisSets().members(idPrefix));
}
public Optional<Cart> findByUserId(Long id) {
String cartId = redisHash().get("carts-by-user-id-idx", id.toString());
return (cartId != null) ? findById(cartId) : Optional.empty();
}
public static String getKey(Cart cart) {
return String.format("%s:%s", idPrefix, cart.getId());
}
public static String getKey(String id) {
return String.format("%s:%s", idPrefix, id);
}
}
与使用 @RedisHash
注解的实体一样,我们的购物车通过 Redis JSON 对象集合和 Redis Set 来维护,以便维护键的集合。
随着 Spring 应用程序变得越来越复杂,直接在控制器上使用仓库会使控制器过于复杂,并偏离了控制路由和处理传入参数和传出 JSON 负载的职责。为了防止模型和控制器因业务逻辑而膨胀(“臃肿”模型和“臃肿”控制器),一种方法是引入一个业务逻辑服务层。我们将通过引入 CartService
来实现购物车业务逻辑。CartService
引入了四个与购物车相关的业务方法
get
: 通过 ID 查找购物车addToCart
: 将购物车商品添加到购物车removeFromCart
: 从购物车的购物车商品集合中移除 ISBNcheckout
: 给定一个购物车 ID,将内容添加到用户的已购买书籍集合中将文件 src/main/java/com/redislabs/edu/redi2read/services/CartService.java
添加到以下内容中
package com.redislabs.edu.redi2read.services;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.stream.LongStream;
import com.redislabs.edu.redi2read.models.Book;
import com.redislabs.edu.redi2read.models.Cart;
import com.redislabs.edu.redi2read.models.CartItem;
import com.redislabs.edu.redi2read.models.User;
import com.redislabs.edu.redi2read.repositories.BookRepository;
import com.redislabs.edu.redi2read.repositories.CartRepository;
import com.redislabs.edu.redi2read.repositories.UserRepository;
import com.redislabs.modules.rejson.JReJSON;
import com.redislabs.modules.rejson.Path;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CartService {
@Autowired
private CartRepository cartRepository;
@Autowired
private BookRepository bookRepository;
@Autowired
private UserRepository userRepository;
private JReJSON redisJson = new JReJSON();
Path cartItemsPath = Path.of(".cartItems");
public Cart get(String id) {
return cartRepository.findById(id).get();
}
public void addToCart(String id, CartItem item) {
Optional<Book> book = bookRepository.findById(item.getIsbn());
if (book.isPresent()) {
String cartKey = CartRepository.getKey(id);
item.setPrice(book.get().getPrice());
redisJson.arrAppend(cartKey, cartItemsPath, item);
}
}
public void removeFromCart(String id, String isbn) {
Optional<Cart> cartFinder = cartRepository.findById(id);
if (cartFinder.isPresent()) {
Cart cart = cartFinder.get();
String cartKey = CartRepository.getKey(cart.getId());
List<CartItem> cartItems = new ArrayList<CartItem>(cart.getCartItems());
OptionalLong cartItemIndex = LongStream.range(0, cartItems.size()).filter(i -> cartItems.get((int) i).getIsbn().equals(isbn)).findFirst();
if (cartItemIndex.isPresent()) {
redisJson.arrPop(cartKey, CartItem.class, cartItemsPath, cartItemIndex.getAsLong());
}
}
}
public void checkout(String id) {
Cart cart = cartRepository.findById(id).get();
User user = userRepository.findById(cart.getUserId()).get();
cart.getCartItems().forEach(cartItem -> {
Book book = bookRepository.findById(cartItem.getIsbn()).get();
user.addBook(book);
});
userRepository.save(user);
// cartRepository.delete(cart);
}
}
该服务使用 JSONPath 语法在 JSON 级本地实现 addToCart
和 removeFromCart
方法,以向购物车添加和删除商品。让我们深入研究这些方法的实现。
在 addToCart 方法中
BookRepository
通过 ISBN 搜索要添加到购物车的书籍".cartItems"
中的 JSON 数组中public void addToCart(String id, CartItem item) {
Optional<Book> book = bookRepository.findById(item.getIsbn());
if (book.isPresent()) {
String cartKey = CartRepository.getKey(id);
item.setPrice(book.get().getPrice());
redisJson.arrAppend(cartKey, cartItemsPath, item);
}
}
在 removeFromCart 方法中:我们通过 ID 搜索购物车。
public void removeFromCart(String id, String isbn) {
Optional<Cart> cartFinder = cartRepository.findById(id);
if (cartFinder.isPresent()) {
Cart cart = cartFinder.get();
String cartKey = CartRepository.getKey(cart.getId());
List<CartItem> cartItems = new ArrayList<CartItem>(cart.getCartItems());
OptionalLong cartItemIndex = LongStream.range(0, cartItems.size()).filter(i -> cartItems.get((int) i).getIsbn().equals(isbn)).findFirst();
if (cartItemIndex.isPresent()) {
redisJson.arrPop(cartKey, CartItem.class, cartItemsPath, cartItemIndex.getAsLong());
}
}
}
我们现在拥有所有必要的组件来创建一个 CommandLineRunner
,该组件可以为我们的用户生成随机购物车。与之前一样,我们将使用应用程序属性来设置要生成的购物车数量。为此,请将以下内容添加到文件 src/main/resources/application.properties
中
app.numberOfCarts=2500
CreateCarts
CommandLineRunner
如下所示。将其添加到引导包中。
package com.redislabs.edu.redi2read.boot;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.stream.IntStream;
import com.redislabs.edu.redi2read.models.Book;
import com.redislabs.edu.redi2read.models.Cart;
import com.redislabs.edu.redi2read.models.CartItem;
import com.redislabs.edu.redi2read.models.User;
import com.redislabs.edu.redi2read.repositories.BookRepository;
import com.redislabs.edu.redi2read.repositories.CartRepository;
import com.redislabs.edu.redi2read.services.CartService;
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(5)
@Slf4j
public class CreateCarts implements CommandLineRunner {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
CartRepository cartRepository;
@Autowired
BookRepository bookRepository;
@Autowired
CartService cartService;
@Value("${app.numberOfCarts}")
private Integer numberOfCarts;
@Override
public void run(String... args) throws Exception {
if (cartRepository.count() == 0) {
Random random = new Random();
// loops for the number of carts to create
IntStream.range(0, numberOfCarts).forEach(n -> {
// get a random user
String userId = redisTemplate.opsForSet()//
.randomMember(User.class.getName());
// make a cart for the user
Cart cart = Cart.builder()//
.userId(userId) //
.build();
// get between 1 and 7 books
Set<Book> books = getRandomBooks(bookRepository, 7);
// add to cart
cart.setCartItems(getCartItemsForBooks(books));
// save the cart
cartRepository.save(cart);
// randomly checkout carts
if (random.nextBoolean()) {
cartService.checkout(cart.getId());
}
});
log.info(">>>> Created Carts...");
}
}
private Set<Book> getRandomBooks(BookRepository bookRepository, int max) {
Random random = new Random();
int howMany = random.nextInt(max) + 1;
Set<Book> books = new HashSet<Book>();
IntStream.range(1, howMany).forEach(n -> {
String randomBookId = redisTemplate.opsForSet().randomMember(Book.class.getName());
books.add(bookRepository.findById(randomBookId).get());
});
return books;
}
private Set<CartItem> getCartItemsForBooks(Set<Book> books) {
Set<CartItem> items = new HashSet<CartItem>();
books.forEach(book -> {
CartItem item = CartItem.builder()//
.isbn(book.getId()) //
.price(book.getPrice()) //
.quantity(1L) //
.build();
items.add(item);
});
return items;
}
}
让我们分解一下 CreateCarts 类
CommandLineRunner
一样,我们检查是否创建了购物车。该类的底部有两个私有实用程序方法,用于获取随机数量的书籍以及从书籍集合创建购物车商品。在服务器启动后(经过一些 CPU 周期),您应该看到
2021-04-04 14:58:08.737 INFO 31459 --- [ restartedMain] c.r.edu.redi2read.boot.CreateCarts : >>>> Created Carts...
我们现在可以使用 Redis CLI 从购物车集合中获取一个随机购物车键,检查其中一个键的类型(ReJSON-RL),并使用 JSON.GET 命令检索 JSON 负载
127.0.0.1:6379> SRANDMEMBER "com.redislabs.edu.redi2read.models.Cart"
"com.redislabs.edu.redi2read.models.Cart:dcd6a6c3-59d6-43b4-8750-553d159cdeb8"
127.0.0.1:6379> TYPE "com.redislabs.edu.redi2read.models.Cart:dcd6a6c3-59d6-43b4-8750-553d159cdeb8"
ReJSON-RL
127.0.0.1:6379> JSON.GET "com.redislabs.edu.redi2read.models.Cart:dcd6a6c3-59d6-43b4-8750-553d159cdeb8"
"{\"id\":\"dcd6a6c3-59d6-43b4-8750-553d159cdeb8\",\"userId\":\"-3356969291827598172\",\"cartItems\":[{\"isbn\":\"1784391093\",\"price\":17.190000000000001,\"quantity\":1},{\"isbn\":\"3662433524\",\"price\":59.990000000000002,\"quantity\":1}]}"
CartController
基本上是 CartService
的一个传递器(正如控制器所预期的那样)。
package com.redislabs.edu.redi2read.controllers;
import com.redislabs.edu.redi2read.models.Cart;
import com.redislabs.edu.redi2read.models.CartItem;
import com.redislabs.edu.redi2read.services.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/carts")
public class CartController {
@Autowired
private CartService cartService;
@GetMapping("/{id}")
public Cart get(@PathVariable("id") String id) {
return cartService.get(id);
}
@PostMapping("/{id}")
public void addToCart(@PathVariable("id") String id, @RequestBody CartItem item) {
cartService.addToCart(id, item);
}
@DeleteMapping("/{id}")
public void removeFromCart(@PathVariable("id") String id, @RequestBody String isbn) {
cartService.removeFromCart(id, isbn);
}
@PostMapping("/{id}/checkout")
public void checkout(@PathVariable("id") String id) {
cartService.checkout(id);
}
}
让我们使用 curl 请求一个购物车,并通过其 ID
curl --location --request GET 'https://localhost:8080/api/carts/dcd6a6c3-59d6-43b4-8750-553d159cdeb8'
这应该返回一个类似于以下内容的负载
{
"id": "dcd6a6c3-59d6-43b4-8750-553d159cdeb8",
"userId": "-3356969291827598172",
"cartItems": [
{
"isbn": "1784391093",
"price": 17.19,
"quantity": 1
},
{
"isbn": "3662433524",
"price": 59.99,
"quantity": 1
}
],
"total": 77.18
}