spring - 如何修复此错误:无法写入 JSON:无法延迟初始化角色集合:com.cashregister.demo.model
问题描述
我在尝试创建 Transaction 对象然后返回交易列表时遇到问题。我在调用 http://localhost:8080/transactions 时遇到的错误是:
{
"timestamp": "2020-08-13T14:24:11.113+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Could not write JSON: failed to lazily initialize a collection of role: com.cashregister.demo.model.Transaction.items, could not initialize proxy - no Session; nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: com.cashregister.demo.model.Transaction.items, could not initialize proxy - no Session (through reference chain: java.util.HashMap[\"transactions\"]->java.util.ArrayList[0]->com.cashregister.demo.model.Transaction[\"items\"])",
"path": "/transactions"
}
我一直在寻找解决方案,但似乎没有一个能摆脱错误并返回。有人可以帮助解决此问题,以便在获取所有交易时以以下格式返回响应:
{
"transaction": {
"id": 1,
"total": 46.44,
"customer": {
"id": 1,
"phoneNumber": "9416970394",
"lastName": "Weber",
"loyaltyNumber": "2484801419"
},
"items": [
// list of each item in the transaction
]
这是我的代码:
Item.java 模型
@Entity
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "transaction_id", nullable = false)
private Transaction transaction;
@NotNull
private double total;
@NotNull
@OneToOne
private Product product;
@NotNull
private int quantity;
// Getters and setters
}
Transaction.java 模型
@Entity
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double total;
@ManyToOne
private Customer customer;
@OneToMany(mappedBy = "transaction", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Item> items = new ArrayList<>();
// Getters and setters
}
用于 DB 访问的 TransactionDaoImpl.java
@Repository
public class TransactionDaoImpl implements TransactionDao {
@Autowired
private SessionFactory sessionFactory;
@Override
public List<Transaction> findAll() {
Session session = sessionFactory.openSession();
List<Transaction> transactions = session.createCriteria(Transaction.class).list();
session.close();
return transactions;
}
@Override
public Transaction findById(Long id) {
Session session = sessionFactory.openSession();
Transaction transaction = session.get(Transaction.class, id);
session.close();
return transaction;
}
@Override
public Long save(Transaction transaction) {
Session session = sessionFactory.openSession();
session.beginTransaction();
session.saveOrUpdate(transaction);
session.getTransaction().commit();
session.close();
return transaction.getId();
}
}
TransctionController.java
@RestController
@RequestMapping("/transactions")
public class TransactionController {
@Autowired
private TransactionService transactionService;
@Autowired
private CustomerService customerService;
@Autowired
private ProductService productService;
@Autowired
private ItemService itemService;
@RequestMapping(value = "")
public Map<String, List<Transaction>> listTransactions() {
Map<String, List<Transaction>> response = new HashMap<>();
List<Transaction> transactions = transactionService.findAll();
response.put("transactions", transactions);
return response;
}
@RequestMapping(value = "{transaction_id}")
public Map<String, Transaction> findTransactionById(@PathVariable Long transaction_id) {
Map<String, Transaction> response = new HashMap<>();
Transaction transaction = transactionService.findById(transaction_id);
response.put("transaction", transaction);
return response;
}
@RequestMapping(value = "", method = RequestMethod.POST)
public Map<String, Transaction> createTransaction(@RequestBody List<Product> products, @RequestParam("user_id") Long customerId) {
Map<String, Transaction> response = new HashMap<>();
Customer customer = customerService.findById(customerId);
// Get unique skus to get the product objects from the database (source of truth).
Set<String> skus = new HashSet<>();
for (final Product product : products) {
skus.add(product.getSku());
}
List<String> skusStrings = new ArrayList<String>();
for(String sku : skus) {
skusStrings.add(sku);
}
List<Product> productsFromDb = productService.findBySkus(skusStrings);
// Loop through products in payload to calculate quantity.
List<Item> items = new ArrayList<>();
Map<String, Integer> productQuantity = new HashMap<>();
for(Product product : products) {
if(productQuantity.containsKey(product.getSku())) {
productQuantity.put(product.getSku(), productQuantity.get(product.getSku()) + 1);
} else {
productQuantity.put(product.getSku(), 1);
}
}
// Calculate total cost
double total = 0;
for(Product product : productsFromDb) {
if(!customer.getLoyaltyNumber().isEmpty()) {
total += product.getDiscountPrice() * productQuantity.get(product.getSku());
} else {
total += product.getDefaultPrice() * productQuantity.get(product.getSku());
}
}
// Calculate item total and append to Items list
Map<String, Double> productTotal = new HashMap<>();
for(Product product : productsFromDb) {
if(!customer.getLoyaltyNumber().isEmpty()) {
productTotal.put(product.getSku(), productQuantity.get(product.getSku()) * product.getDiscountPrice());
} else {
productTotal.put(product.getSku(), productQuantity.get(product.getSku()) * product.getDefaultPrice());
}
}
Transaction t = new Transaction();
t.setTotal(total);
t.setCustomer(customer);
t.setItems(items);
Long transactionId = transactionService.save(t);
for(Product product : productsFromDb) {
Item item = new Item(t, productTotal.get(product.getSku()), product, productQuantity.get(product.getSku()));
itemService.save(item);
}
Transaction transaction = transactionService.findById(transactionId);
response.put("transaction", transaction);
return response;
}
}
解决方案
这里的问题是Hibernate 的延迟加载。这实际上是 JPA 规范并由 Hibernate 实现。主要概念是不加载所有关联,从而提高性能。
为什么你面临这个问题:
- 您已将 FETCH 策略定义为双方的 LAZY(此处,从交易到项目的关系很重要,因为您获取交易)
- TransactionDaoImpl#findAll() 方法打开一个 Session 并获取记录。根据延迟加载的概念,Hibernate 不会从数据库中获取关联的项目,而是创建代理。
- 由于这些是代理对象,因此 HttpMessageConverter 无法将这些对象转换为 JSON 表示。
如何解决问题:
- 您必须在项目关系上调用 get 访问器。
@Transactional // This is required since you are not using Spring data Jpa Repository abstractions. Or else you can explicitly define the transaction boundary inside the method by opening the transaction and closing it.
@Override
public List<Transaction> findAll() {
Session session = sessionFactory.openSession();
List<Transaction> transactions = session.createCriteria(Transaction.class).list();
transactions.foreach(Transaction::getItems);
session.close();
return transactions;
}
但这会引起N+1 的问题。
主要是。对于每个事务,将为每个事务触发 N 个查询(N 是一个事务的项目数) - 因此将触发 K*N 个查询。
要解决 N+1 问题,您可以使用使用 Join 的 Native 查询,通过一个查询来获取所有数据。由于加入,性能仍然会很慢,但比第一个选项好得多。或者您可以使用Hibernate Annotation或 JPQL/HQL JOIN FETCH,这实际上为您创建了 JOIN。
无需 N+1 查询即可加载数据的另一个选项是
@EntityGraphy
. 我没用过。但是你可以在这里阅读更多关于它的信息
推荐阅读
- java - 无法在 HttpServletRequest 中获取证书 - httpServletRequest.getAttribute("javax.servlet.request.X509Certificate") 给出 null
- raku - 如何死于未定义的值?
- javascript - 输入特定字符时,在克林特写入输入字段期间更改文本颜色
- scikit-learn - 在 PLS 模型中查找每个原始描述符的整体贡献
- dataweave - 如何将数据映射到 Dataweave 中的数组?
- javascript - 创建可折叠菜单和子菜单时如何修复间距
- android - 是否可以用房间实现实时分页(aac 库)?
- java - 向异常添加数据
- python - 如何停止计时器 discord.py
- selenium - Selenium InternetExplorerDriver 代理 C#