在哪里根据业务规则实施导致实体副本的更改字段

Where to implement changing fields leading to copy of entity by business rule

我有 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<Aaa, Long>BbbRepository。这意味着当使用 HAL 表示时,Aaa 实例在其主体中确实仅关联 link 到 Bbb。

Objective/目标: 我在 table 中存储了 "templates" 个 Aaa 实例(这样的 Aaa 实例,有 Aaa.bbb == null),还有 "real" 个 Aaa 实例(这样会有 Aaa.bbb 不为空)。创建 Aaa 的 "real" 实例时,总是使用一些 Aaa 模板来完成。从空值设置 Aaa.bbb 时,我想复制给定的 Aaa 并将 Aaa_copy.bbb 设置为所需的值。此外 returned 剩余资源必须是新创建的副本(即设置 ID 为 /api/aaa/123 的剩余资源关联将 return 具有不同 ID 的实例!)。

我想到的可能的解决方案。我还没有实施任何一个,我只是 想选择正确的实施方法:

  1. 为关联 link 实施自定义控制器(即 /api/aaa/{id}/bbb POST 和 PUT。可能的问题 或许可以轻松解决。
  2. 重写存储库中的 S save(S s) 和 saveAll 方法并在那里执行 "clone if needed" 魔法
  3. 在 Aaa class 中实现方法并用 @PrePersist 注释对其进行注释。

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

用户想要编辑 Aaa 对象的 Bbb 关联,即将另一个 Bbb 对象关联到相关 Aaa 对象。您想实施某种版本控制,并存储 Aaa 对象的 copy 更改之前的状态已应用。

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

Spring 数据 REST 事件处理

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

正在扩展 AbstractRepositoryEventListener

...实现 class 扩展 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 注释的 class 包含一个处理 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 数据 REST 时才会触发事件。如果您在存储库上使用 save(...) 方法,则不会触发事件并且不会保存受影响的 Aaa 对象的副本。

Spring AOP(面向切面编程)

如果你确实想支持存储库的 save(...) 方法,我建议使用 Spring AOP 创建一个 @Before@Around 建议(取决于你的needs) 拦截对存储库方法的调用。这是此类组件的基本脚手架:

@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 引入的方法。此外,您需要自己处理 return 值和(例如,如果您使用 Spring HATEOAS)assemble 资源。此外,这仅适用于对端点的调用,不适用于存储库 save(...) 方法的内部调用。

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

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

使用 Spring Data REST 提供的事件处理程序将您用于进行版本控制的代码保持在存储库附近并在 Spring Data REST 中。使用方面类似,它只是事件处理程序的更抽象版本(忽略实际实现)。

在 spring Moore 发布序列中有一个名为 BeforeSaveCallback (documentation page) and 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/ 视频演示。