jpa - 从 Spring mvc Controller 方法更新 JPA 实体以及使用隐藏输入字段存储 ID 的问题

标签 jpa spring-mvc spring-data-jpa

假设我有一个实体/javabean,它具有大量属性

此外,我有一个 html 表单(jsp 或 thymeleaf 格式),用于更新该实体。

当我的应用程序出现在这里时,我将如何继续更新实体:

  1. 我在表单的隐藏 html 字段中设置了实体的 JPA ID
  2. 在 Spring Controller 中,我使用该隐藏 ID 从数据库中检索实体
  3. 仍然在 Controller 方法中,我使用作为参数传递给 Controller ​​方法的 spring mvc ModelAttribute 的字段来设置先前检索到的实体的每个字段。
  4. 然后,我使用实体管理器保留/更新实体。

这是我的 Controller 方法的示例:

@RequestMapping(value = "/family/advertisement/edit", method = RequestMethod.POST, produces = "text/html")
    public String editFamilyAdvertisement(@ModelAttribute @Validated(value = Validation.AdvertisementCreation.class) FamilyAdvertisement familyAdvertisement,
            BindingResult bindingResult, Model model) {
        FamilyAdvertisement advertisementForUpdate = advertisementService.findFamilyAdvertisement(familyAdvertisement.getId());
        if (bindingResult.hasErrors()) {
            populateModel(model, familyAdvertisement);
            return "family/advertisement/edit";
        }
        advertisementForUpdate.setNeeds(familyAdvertisement.getNeeds());
        advertisementForUpdate.setChildcareTypes(familyAdvertisement.getChildcareTypes());
        advertisementForUpdate.setDayToTimeSlots(familyAdvertisement.getDayToTimeSlots());
        ...
        advertisementService.editFamilyAdvertisement(advertisementForUpdate);
        return "redirect:/some/url";
    }

目前的应用程序存在两个问题:

  • 首先,聪明的黑客可以轻松篡改 ID 并更新他人的广告。
  • 其次,我必须使用 spring mvc 模型属性中的字段手动更新附加实体的每个字段:这既乏味又丑陋。

有人可以建议更好的模式或解决方案吗?

编辑 1:我遵循了提供的建议。

这是我修改后的 Controller 方法:

@RequestMapping(value = "/family/advertisement/edit", method = RequestMethod.POST, produces = "text/html")
    public String editFamilyAdvertisement(@ModelAttribute @Validated(value = Validation.AdvertisementCreation.class) FamilyAdvertisementInfo familyAdvertisementInfo,
            BindingResult bindingResult, Model model) {
        Member member = memberService.retrieveCurrentMember();
        FamilyAdvertisement advertisementForCheck = advertisementService.findFamilyAdvertisement(familyAdvertisementInfo.getFamilyAdvertisement().getId());
        if (!member.getAdvertisements().contains(advertisementForCheck)) {
            throw new IllegalStateException("advertisement does not belong to member");
        }
        if (bindingResult.hasErrors()) {
            populateModel(model, familyAdvertisementInfo);
            return "family/advertisement/edit";
        }
        advertisementService.editFamilyAdvertisement(familyAdvertisementInfo.getFamilyAdvertisement());
        return "redirect:/family/advertisement/edit/" + familyAdvertisementInfo.getFamilyAdvertisement().getId();
    }

您看到我必须从数据库中获取家庭广告实体,以检查它是否属于 session 中的当前成员。然后,当我尝试按照建议保存实体时,我收到 StaleObjectStateException ,如下所示:

SEVERE: Servlet.service() for servlet [bignibou] in context with path [/bignibou] threw exception [Request processing failed; nested exception is org.springframework.orm.jpa.JpaOptimisticLockingFailureException: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bignibou.domain.FamilyAdvertisement#1]; nested exception is javax.persistence.OptimisticLockException: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bignibou.domain.FamilyAdvertisement#1]] with root cause
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bignibou.domain.FamilyAdvertisement#1]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:303)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:76)
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:903)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:887)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:891)
    at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:879)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:366)
    at com.sun.proxy.$Proxy120.merge(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:241)
    at com.sun.proxy.$Proxy119.merge(Unknown Source)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:345)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:334)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:319)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocatio

编辑2:如果我不从数据库中获取实体,我遇到的问题是上面对contains的调用总是会计算为false 因为它在内部使用 equals 方法,并且实体可能已更改(毕竟这就是该方法的目的)。

if (!member.getAdvertisements().contains(familyAdvertisementInfo.getFamilyAdvertisement())) {
            throw new IllegalStateException("advertisement does not belong to member");
        }

编辑3:

我对 StaleObjectStateException 仍然有同样的问题,因为我的 Controller 方法似乎执行了两次保存/事务。

@RequestMapping(value = "/family/advertisement/edit", method = RequestMethod.POST, produces = "text/html")
    public String editFamilyAdvertisement(@ModelAttribute @Validated(value = Validation.AdvertisementCreation.class) FamilyAdvertisementInfo familyAdvertisementInfo,
            BindingResult bindingResult, Model model) {
        Member member = memberService.retrieveCurrentMember();//ONE
        if (!advertisementService.advertisementBelongsToMember(familyAdvertisementInfo.getFamilyAdvertisement(), member)) {
            throw new IllegalStateException("advertisement does not belong to member");
        }
        if (bindingResult.hasErrors()) {
            populateModel(model, familyAdvertisementInfo);
            return "family/advertisement/edit";
        }
        familyAdvertisementInfo.getFamilyAdvertisement().setMember(member);
        advertisementService.editFamilyAdvertisement(familyAdvertisementInfo.getFamilyAdvertisement());//TWO
        return "redirect:/family/advertisement/edit/" + familyAdvertisementInfo.getFamilyAdvertisement().getId();
    }

查看异常:

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bignibou.domain.FamilyAdvertisement#1]
    org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:303)
    org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151)
    org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:76)
    org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:903)
    org.hibernate.internal.SessionImpl.merge(SessionImpl.java:887)
    org.hibernate.internal.SessionImpl.merge(SessionImpl.java:891)
    org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:879)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:366)
    com.sun.proxy.$Proxy123.merge(Unknown Source)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:241)
    com.sun.proxy.$Proxy122.merge(Unknown Source)
    org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:345)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:334)
    org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:319)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:155)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.data.jpa.repository.support.LockModeRepositoryPostProcessor$LockModePopulatingMethodIntercceptor.invoke(LockModeRepositoryPostProcessor.java:91)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:91)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
    com.sun.proxy.$Proxy129.save(Unknown Source)
    com.bignibou.service.AdvertisementServiceImpl_Roo_Service.ajc$interMethod$com_bignibou_service_AdvertisementServiceImpl_Roo_Service$com_bignibou_service_AdvertisementServiceImpl$updateFamilyAdvertisement(AdvertisementServiceImpl_Roo_Service.aj:58)
    com.bignibou.service.AdvertisementServiceImpl.updateFamilyAdvertisement(AdvertisementServiceImpl.java:1)
    com.bignibou.service.AdvertisementServiceImpl_Roo_Service.ajc$interMethodDispatch1$com_bignibou_service_AdvertisementServiceImpl_Roo_Service$com_bignibou_service_AdvertisementServiceImpl$updateFamilyAdvertisement(AdvertisementServiceImpl_Roo_Service.aj)
    com.bignibou.service.AdvertisementServiceImpl.editFamilyAdvertisement(AdvertisementServiceImpl.java:27)
    com.bignibou.controller.AdvertisementController.editFamilyAdvertisement(AdvertisementController.java:85)

最佳答案

第一个问题:当您从数据库检索实体时,请指定 ID 和用户。如果没有找到带有 id 和用户的实体,则意味着该用户不拥有该实体。

第二个问题:几种解决方案,具体取决于您的要求

  • 直接公开您的实体,而不是专用的表单对象
  • 将您的实体封装在表单中并使用委托(delegate)方法(您的 IDE 可以生成它们)
  • 使用Dozer

关于jpa - 从 Spring mvc Controller 方法更新 JPA 实体以及使用隐藏输入字段存储 ID 的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15789837/

相关文章:

java - JPA:使用字符串字段,但修改它们并保存为字节数组

java.lang.IllegalArgumentException : Named query not found: 异常

spring - 如何使用 Spring MVC 在 post 方法中传递 List<String>?

java - JPAQuery 与 Spring Pageable

java - 如何自定义 spring-data Repository 方法名称?

java - 使用数据表与单独的审计表进行审计

java - Java LocalDateTime 和 db 之间的正确映射

java - 在 Spring Framework 中使用 @PathVariable 时编码不正确

css - 为动态复选框放置适当的 CSS - Spring MVC

java - 更新实体并仅覆盖非空值(无需动态查询)