首页 > 解决方案 > 如何修复此错误:无法写入 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;
    }
}

标签: springspring-boothibernatespring-data-jpa

解决方案


这里的问题是Hibernate 的延迟加载。这实际上是 JPA 规范并由 Hibernate 实现。主要概念是不加载所有关联,从而提高性能。

为什么你面临这个问题:

  • 您已将 FETCH 策略定义为双方的 LAZY(此处,从交易到项目的关系很重要,因为您获取交易)
  • TransactionDaoImpl#findAll() 方法打开一个 Session 并获取记录。根据延迟加载的概念,Hibernate 不会从数据库中获取关联的项目,而是创建代理。
  • 由于这些是代理对象,因此 HttpMessageConverter 无法将这些对象转换为 JSON 表示。

如何解决问题:

  1. 您必须在项目关系上调用 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 个查询。

  1. 要解决 N+1 问题,您可以使用使用 Join 的 Native 查询,通过一个查询来获取所有数据。由于加入,性能仍然会很慢,但比第一个选项好得多。或者您可以使用Hibernate Annotation或 JPQL/HQL JOIN FETCH,这实际上为您创建了 JOIN。

  2. 无需 N+1 查询即可加载数据的另一个选项是@EntityGraphy. 我没用过。但是你可以在这里阅读更多关于它的信息


推荐阅读