杰克逊 ObjectMapper 休眠问题

Posted

技术标签:

【中文标题】杰克逊 ObjectMapper 休眠问题【英文标题】:Jackson ObjectMapper Hibernate issue 【发布时间】:2019-04-16 02:47:41 【问题描述】:

我正在开发一个Spring Boot 2.0 / Java 8 购物车在线应用程序。我使用Hibernate 作为ORM 框架。 我有两个实体,OrderOrderDetail,如下所示:

@Entity
@Table(name = "orders")
public class Order extends AbstractEntityUuid 

    @Column(name = "order_number", unique = true)
    private String orderNumber;

    @JsonBackReference
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @Column(name = "total_amount")
    private BigDecimal totalAmount = BigDecimal.ZERO;

    @CreatedDate
    @Column(name = "created_on", columnDefinition = "DATETIME", updatable = false)
    protected LocalDateTime created;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @JsonManagedReference
    private Set<OrderDetail> items = new HashSet<>();

    @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
    @JoinColumn(nullable = false, name = "card_details_id")
    private CardDetails card;

    @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
    @JoinColumn(nullable = false, name = "shipping_address_id")
    private Address shippingAddress;

    @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
    @JoinColumn(name = "billing_address_id")
    private Address billingAddress;

    //getters and setters


@Entity
@Table(name = "order_detail")
public class OrderDetail extends AbstractPersistable<Long> 

    @Column(name = "quantity")
    private Integer quantity;

    @Column(name = "total_amount")
    private BigDecimal totalAmount = BigDecimal.ZERO;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    @JsonBackReference
    private Order order;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    //getters and setters

当用户前往他的订单时,他应该能够看到仅与订单本身相关的信息(没有详细信息)。 出于这个原因,我只从订单表中检索数据。以下是我的存储库:

public interface OrderRepository extends CrudRepository<Order, Long> 

    @Query("FROM Order o WHERE o.user.email = ?1")
    List<Order> findOrdersByUser(String email);

在服务中,我所做的只是调用上述方法并转换为 dto 对应项。

@Transactional(readOnly = true)
public List<OrdersPreviewDTO> getOrdersPreview(String email) 
    List<Order> orders = orderRepository.findOrdersByUser(email);
    return orderConverter.convertToOrderPreviewDTOs(orders);

转换器在后台使用 Jackson ObjectMapper 对象。

List<OrdersPreviewDTO> convertToOrderPreviewDTOs(List<Order> orders) 
    return orders.stream()
     .map(o -> objectMapper.convertValue(o, OrdersPreviewDTO.class))
     .collect(toList());

objectMapperSpring 注入并在配置类中定义:

@Bean
public ObjectMapper objectMapper() 
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.findAndRegisterModules();
    return objectMapper;

OrdersPreviewDTO dto 对象仅包含 Order 实体的一个子集,因为正如我已经提到的,在 Orders 页面中,我只想显示用户订单的高级属性,而与他们的详细信息无关。

@JsonIgnoreProperties(ignoreUnknown = true)
public class OrdersPreviewDTO 

    private String orderNumber;

    @JsonFormat(pattern = "dd/MM/yyyy HH:mm")
    private LocalDateTime created;

    private BigDecimal totalAmount;

    @JsonCreator
    public OrdersPreviewDTO(
            @JsonProperty("orderNumber") String orderNumber,
            @JsonProperty("created") LocalDateTime created,
            @JsonProperty("totalAmount") BigDecimal totalAmount) 
        this.orderNumber = orderNumber;
        this.created = created;
        this.totalAmount = totalAmount;
    

    //getters and setters

一切正常,订单实体由 Jackson 自动转换为其 dto 对应物。 当查看 Hibernate 在后台执行的查询时,问题就出现了。 Hibernate 解包每个订单的订单详细信息集合并执行查询以从每个子集合中检索数据:

select carddetail0_.id as id1_2_0_, carddetail0_.brand as brand2_2_0_, carddetail0_.created as created3_2_0_, carddetail0_.exp_month as exp_mont4_2_0_, carddetail0_.exp_year as exp_year5_2_0_, carddetail0_.last4 as last6_2_0_ from card_details carddetail0_ where carddetail0_.id=?

select address0_.id as id1_1_0_, address0_.created as created2_1_0_, address0_.last_modified as last_mod3_1_0_, address0_.city as city4_1_0_, address0_.country as country5_1_0_, address0_.first_name as first_na6_1_0_, address0_.last_name as last_nam7_1_0_, address0_.postal_code as postal_c8_1_0_, address0_.state as state9_1_0_, address0_.street_address as street_10_1_0_, address0_.telephone as telepho11_1_0_ from address address0_ where address0_.id=?

select items0_.order_id as order_id4_4_0_, items0_.id as id1_4_0_, items0_.id as id1_4_1_, items0_.order_id as order_id4_4_1_, items0_.product_id as product_5_4_1_, items0_.quantity as quantity2_4_1_, items0_.total_amount as total_am3_4_1_ from order_detail items0_ where items0_.order_id=?

还有很多其他的。

无论我通过以下方式修改代码,Hibernate 只对 Order 表运行预期的查询:

这行代码:

objectMapper.convertValue(o, OrdersPreviewDTO.class)

替换为以下脏修复:

new OrdersPreviewDTO(o.getOrderNumber(), o.getCreated(), o.getTotalAmount())

由 Hibernate 运行的查询:

select order0_.id as id1_5_, order0_.billing_address_id as billing_6_5_, order0_.card_details_id as card_det7_5_, order0_.created_on as created_2_5_, order0_.one_address as one_addr3_5_, order0_.order_number as order_nu4_5_, order0_.shipping_address_id as shipping8_5_, order0_.total_amount as total_am5_5_, order0_.user_id as user_id9_5_
from orders order0_ cross join user user1_
where order0_.user_id=user1_.id and user1_.user_email=?

我的问题是。有没有办法告诉 Jackson 只映射 Dtos 字段,这样它就不会触发通过 Hibernate 对非必需字段进行延迟加载获取?

谢谢

【问题讨论】:

【参考方案1】:

简短的回答是否定的,不要试图那么聪明。手动创建 DTO 以控制任何延迟加载,然后在事务外的 DTO 上使用 Jackson。

答案是肯定的,您可以覆盖 MappingJackson2HttpMessageConverter 并控制从实体调用哪些字段。

@Configuration
public class MixInWebConfig extends WebMvcConfigurationSupport 

    @Bean
    public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter2() 
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.addMixIn(DTO1.class, FooMixIn.class);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        jsonConverter.setObjectMapper(objectMapper);
        return jsonConverter;
    

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) 
        converters.add(customJackson2HttpMessageConverter2());
    

然后

    @Override
    protected void processViews(SerializationConfig config, BeanSerializerBuilder builder) 
        super.processViews(config, builder);
        if (classes.contains(builder.getBeanDescription().getBeanClass())) 
            List<BeanPropertyWriter> originalWriters = builder.getProperties();
            List<BeanPropertyWriter> writers = new ArrayList<BeanPropertyWriter>();
            for (BeanPropertyWriter writer : originalWriters) 
                String propName = writer.getName();
                if (!fieldsToIgnore.contains(propName)) 
                    writers.add(writer);
                
            
            builder.setProperties(writers);
        
    

here 是一个工作示例。

【讨论】:

【参考方案2】:

为Essex Boy answer +1。我只想补充一点,您可以直接从 JPQL 查询返回 DTO,而不是使用 Jackson。它避免了从数据库到您的对象Order 的转换,然后再从Order 对象到OrdersPreviewDTO 对象的转换。

例如,您需要更改存储库中的查询来执行此操作。它会是这样的:

public interface OrderRepository extends CrudRepository<Order, Long> 

    @Query("SELECT new OrdersPreviewDTO(o.order_number, o.created_on, o.total_amount)) FROM Order o WHERE o.user.email = ?1")
    List<OrdersPreviewDTO> findOrdersByUser(String email);

【讨论】:

【参考方案3】:

如果OrdersPreviewDTO 严格来说是Order 类的子集,为什么不简单地使用@JsonView 注释在控制器中自动创建一个简单的视图?例如,请参阅https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring。

如果您的输入和输出都需要 DTO,也可以考虑使用http://mapstruct.org/

【讨论】:

以上是关于杰克逊 ObjectMapper 休眠问题的主要内容,如果未能解决你的问题,请参考以下文章

如何在 JUnitTests 中使用 ObjectMapper - Spring Boot 应用程序

如何使用 Spring application.properties 自定义 Jackson ObjectMapper?

杰克逊在没有双引号的情况下序列化和反序列化对象

杰克逊 - 序列化日期对象

使用杰克逊创建一个 json 对象

如何使用jackson序列化通用对象