【问题标题】:Where to implement changing fields leading to copy of entity by business rule在哪里实施更改字段以根据业务规则复制实体
【发布时间】:2020-05-07 07:57:08
【问题描述】:

我有 spring-data-rest 项目,其中有一些实体,例如名为Aaa。这是简化的定义:

@Entity
@Data // some lombok-project magic for getters/setters/...
public class Aaa {
    // many different fields

    /**
     * bi-directional many-to-one association to Bbb
     */
    @ManyToOne(optional = true)
    @JoinColumn(name="bbb_fk")
    @RestResource(
        description = @Description("Optional relation of Aaa to Bbb. " +
            "If not empty, it means that this Aaa belongs to the given Bbb. " +
            "Otherwise given Aaa is just something like a template."
        ))
    private Bbb bbb;
    // also some other references, like:
    private List<Ccc> cccs;
}

我需要(根据业务规则)确保设置 Bbb 引用将导致数据库中给定实体的副本,并且只有副本才会设置给定引用。写时复制语义。将引用从某个 Bbb 实例更改为其他实例,不会触发副本。

请注意,Aaa 实体和 Bbb 实体确实有它们的 interface AaaRepository extends PagingAndSortingRepository&lt;Aaa, Long&gt;BbbRepository。这意味着当使用 HAL 表示时,Aaa 实例在其主体中确实只有与 Bbb 的关联链接。

目标/目标: 我已经在表中存储了 Aaa 实例的“模板”(例如 Aaa 实例,具有 Aaa.bbb == null)以及 Aaa 的“真实”实例(例如 Aaa.bbb 不为空)。在创建 Aaa 的“真实”实例时,总是使用一些 Aaa 模板来完成。从空值设置 Aaa.bbb 时,我想复制给定的 Aaa 并将 Aaa_copy.bbb 设置为所需的值。同样返回的休息资源必须是新创建的副本(即为 ID 为 /api/aaa/123 的休息资源设置关联将返回具有不同 ID 的实例!)。

我想到的可能的解决方案。我没有实现它们中的任何一个,我只是想选择正确的实现方法

  1. 为关联链接实现自定义控制器(即 /api/aaa/{id}/bbb 用于 POST 和 PUT。可能的问题 with "hiding" 可能很容易解决。
  2. 覆盖存储库中的 S save(S s) 和 saveAll 方法并在那里执行“如果需要则克隆”魔法
  3. 在 Aaa 类中实现方法并使用 @PrePersist 注释对其进行注释。

我应该在哪里(以及为什么在那里)实施这种行为?

【问题讨论】:

  • 在这段摘录中private Release release 应该是private Bbb bbb 对吗?如果您错过了这一点,请在您的问题中更正这一点。此外,您的目标对我来说还不是很清楚。是否正确,在更改 Bbb 引用时,您想要创建 Aaa 对象(当前正在更改)的副本,然后保存该引用?那么Aaa 对象的某种版本控制?

标签: spring-data-rest spring-hateoas


【解决方案1】:

用户想要编辑Aaa 对象的Bbb 关联,即将不同的Bbb 对象与相关Aaa 对象相关联。您想实现某种版本控制,并将Aaa 对象的副本存储在它应用更改之前的状态。

我会提出以下解决方案来解决这个问题:

Spring Data REST 事件处理

使用Spring Data REST's event functionality 和...

扩展AbstractRepositoryEventListener

...实现一个扩展AbstractRepositoryEventListener 的类,其中包含一个覆盖onBeforeLinkSave(...) 方法的方法。

@Component
public class AaaRepositoryListener extends AbstractRepositoryEventListener<Aaa> {
    @Override
    protected void onBeforeLinkSave(Aaa parent, Object linked) {
        // Handle event, remember to detach the entity using the entity manager if necessary and checking the type of the linked object.
    }
}

@RepositoryEventHandler注解

...实现一个用@RepositoryEventHandler 注释的类,其中包含一个处理BeforeLinkSaveEvent 的方法。

@Component
@RepositoryEventHandler 
public class AaaEventHandler {
  @PersistenceContext
  private EntityManager entityManager;

  @HandleBeforeLinkSave
  public void handleAaaToBbbSave(Aaa aaa, Bbb bbb) {
    // Mind that this only handles changes on Aaa objects
    // that affect Bbb links and only takes a single argument.
    // As soon as Aaa contains links to other classes, this method
    // no longer works.
    // 
    // Copy Aaa object and store it in the repository.
  }
}

有关上述方法的说明

请记住,您在handleAaaToBbbSave(...) 方法中收到的对象可能已附加,您可能需要在重置标识符并再次保存之前将其分离 (EntityManager.detach(...))。

此外,由于a bug in Spring Data REST,您需要将此组件添加到您的应用程序中,以便实际处理事件。

@Configuration
public class BugFixForSpringDATAREST524 implements InitializingBean {

    private ValidatingRepositoryEventListener eventListener;
    private Map<String, Validator>            validators;

    @Autowired
    public BugFixForSpringDATAREST524(ValidatingRepositoryEventListener eventListener,
                                      Map<String, Validator> validators) {
        this.eventListener = eventListener;
        this.validators    = validators;
    }

    @Override
    public void afterPropertiesSet() {
        List<String> events = Arrays.asList("beforeCreate",
                                            "afterCreate",
                                            "beforeSave",
                                            "afterSave",
                                            "beforeLinkSave",
                                            "afterLinkSave",
                                            "beforeDelete",
                                            "afterDelete");

        for (Map.Entry<String, Validator> entry : validators.entrySet()) {
            events.stream()
                    .filter(p -> entry.getKey().startsWith(p))
                    .findFirst()
                    .ifPresent(p -> eventListener.addValidator(p, entry.getValue()));
        }
    }
}

请注意,只有在实际使用 Spring Data REST 时才会触发事件。如果您在存储库上使用save(...) 方法,则不会触发事件并且不会保存受影响的Aaa 对象的副本。

Spring AOP(面向方面​​编程)

如果你确实想支持repository的save(...)方法,我推荐使用Spring AOP创建@Before@Around通知(取决于你的需要)来拦截对repository方法的调用.这是此类组件的基本脚手架:

@Aspect
@Component
public class AaaRepositoryAspect {

    @Pointcut(value = "execution(* com.example.backend.repository.aaa.AaaRepository.save()) && args(aaa)")
    private void repositorySave(Aaa aaa) {
    }


    @Before(value = "repositorySave(aaa)")
    private void beforeSave(Aaa aaa) throws Throwable {
        // Save a copy of the object.
    }
}

推理

为什么我推荐上述方法而不是您的方法之一:

  1. 您需要创建一个控制器来覆盖 Spring Data REST 引入的方法。此外,您需要处理返回值并(例如,如果您使用 Spring HATEOAS)自己组装资源。此外,这仅适用于对端点的调用,不适用于存储库的 save(...) 方法的内部调用。

  2. 同样,您需要编写很多实际上不需要的代码。

  3. 这会在您的模型和存储库之间创建依赖关系,因为您随后需要模型类中的存储库实例。

使用 Spring Data REST 提供的事件处理程序可以使您用于执行版本控制的代码靠近存储库和 Spring Data REST。使用切面类似,只是事件处理程序的一个更抽象的版本(不考虑实际实现)。

【讨论】:

  • 我喜欢你所有的回答。我将尝试在我的项目中实现它,而不是标记您的答案。谢谢。
  • @Lubo 对不起,我还没有处理@HandleBeforeLinkSave 事件,并认为它们会起作用。处理这些链接更改时有点不同。我已经更新了上面的示例代码 - 您需要将 @Component 添加到您的处理程序,并将 Bbb 参数添加到您的处理程序方法。请阅读我写的评论 - 我还不确定如何处理多个参数。但在您的情况下,上述更改应该可以正常工作。
  • 您的建议似乎奏效了,但是...使用 POST 方法创建给定的休息资源到/api/aaa 端点时会引发异常。未发送对某些/api/bbb/123 的引用,因为我想创建不引用 Bbb 的 Aaa 实例。抛出的异常来自IlegalArgumentException,见pastebin.com/Q47ptq60 PS:好像是在设置Aaa资源的其他关联时抛出的。它可能搜索自定义handleAaaToXxxSave(..) 方法?
  • @Lubo 这就是我的预期。一旦您与不同类型有多个关联,它就会崩溃,因为类型不兼容。我将在 Spring Data REST Jira 中打开一个问题并创建一个拉取请求来解决此问题,以便支持使用 @HandleBeforeLinkSave 注释的多种方法。如果您自己处理类型没问题,那么您可以查看我更新的答案以了解第三种方法。
  • 给定的解决方案(制作扩展 AbstractRepositoryEventListener 的处理程序)有效。在linked 实例不是Bbb 的情况下,处理代码可能“什么都不做”。这种方式关联其他类型的工作。出现了一个问题。如果我与 Bbb 有更多(和不同)的关联怎么办?例如private Bbb authorprivate Bbb lastUpdateBy。我怎样才能获得有关“onBeforeLinkSave”关系的信息? PS:这似乎事情变得越来越复杂了:/
【解决方案2】:

在 Spring Moore 发布火车中,有新的可能性称为 BeforeSaveCallback (documentation page) 和 BeforeConvertCallback (documentation page)。可以这样使用:

@Bean
BeforeSonvertCallback<Aaa> beforeSave() {
  return (aaa, convertedAaa) -> {
    // aaa.modifyBeforeSave...
    // perhaps do something like this:
    // aaa = new Aaa(aaa.Bbb, null);
    return aaa;
  }
}

如需了解更多信息,请观看 23 分钟的 https://www.infoq.com/presentations/spring-data-enhancements/ 视频演示。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-24
    • 1970-01-01
    相关资源
    最近更新 更多