1

I have spring-data-rest project, where I have some entity, for example called Aaa. It's simplified definition:

@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;
}

I need (by business rule) ensure that setting Bbb reference will lead to the copy of the given entity in database and only copy will have set given reference. Copy-on-write semantics. Change of reference from some Bbb instance to other, does not trigger a copy.

Note, that Aaa entities and also Bbb entities does have their interface AaaRepository extends PagingAndSortingRepository<Aaa, Long> and BbbRepository. It means that when using HAL representation, Aaa instance does have just association link to Bbb in its body.

Objective/ goal: I have stored "templates" of Aaa instances in table (such Aaa instance, that have Aaa.bbb == null) and also "real" instances of Aaa (such will have Aaa.bbb not null). When creating "real" instance of Aaa, it is ALWAYS done by using some Aaa template. When setting Aaa.bbb from null value, I would like to make copy of given Aaa and set Aaa_copy.bbb to required value. Also returned rest resource have to be a newly created copy (i.e. setting association for rest resource with ID /api/aaa/123 will return instance with different id!).

Possible solutions I have think of. I have not implemented any of them, I just want to choose right approach for implementation:

  1. Implement custom controller for associative link (i.e. /api/aaa/{id}/bbb for POST and PUT. Possible problem with "hiding" can be solved perhaps easily.
  2. override S save(S s) and saveAll method in repository and do "clone if needed" magic there
  3. implement method in Aaa class and annotate it with @PrePersist annotation.

Where (and why there), should I implement such behavior?

Lubo
  • 1,621
  • 14
  • 33
  • 1
    Is it correct, that `private Release release` was meant to be `private Bbb bbb` in this excerpt? Please correct this in your question if you missed that. Additionally your objective is not yet completely clear to me. Is it correct, that on change of the `Bbb` reference you want to create a copy of the `Aaa` object (that is currently being changed) that then holds this reference? So some sort of version control of the `Aaa` objects? – Daniel Jan 22 '20 at 16:01

2 Answers2

1

A user wants to edit the Bbb association of an Aaa object, i.e. associate a different Bbb object to the Aaa object in question. You would like to implement some sort of version control and store a copy of the Aaa object in the state it has before the change is applied.

I would propose the following solutions to solve this problem:

Spring Data REST event handling

Use Spring Data REST's event functionality and...

Extending AbstractRepositoryEventListener

...implement a class extending the AbstractRepositoryEventListener containing a method overriding the onBeforeLinkSave(...) method.

@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.
    }
}

Annotating with @RepositoryEventHandler

...implement a class annotated with @RepositoryEventHandler containing a method that handles a 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.
  }
}

Notes about above methods

Please remember, that the object you receive in the handleAaaToBbbSave(...) method may be attached and that you may need to detach it (EntityManager.detach(...)) before resetting the identifier and saving it again.

Additionally, due to a bug in Spring Data REST, you need to add this component to your application so events are actually processed.

@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()));
        }
    }
}

Note that the events are only triggered, if Spring Data REST is actually used. In case you use the save(...) method on the repository, the event is not triggered and no copy of the affected Aaa object is saved.

Spring AOP (Aspect Oriented Programming)

If you do want to support the save(...) method of the repository, I recommend to use Spring AOP to create a @Before or @Around advice (depends on your needs) to intercept the call to the repository method. Here is a basic scaffolding of such a component:

@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.
    }
}

Reasoning

To why I recommend the above methods instead of one of your methods:

  1. You would need to create a controller that override the method introduced by Spring Data REST. Additionally you need to handle the return values and (if you, for example, use Spring HATEOAS) assemble the resources yourself. Furthermore, this only applies to calls to the endpoint, not internal calls of the save(...) method of the repository.

  2. Again, you need to write a lot of code that you actually do not need.

  3. This creates a dependency between your model and your repositories, because you then need a repository instance in your model class.

Using the event handler provided by Spring Data REST keeps the code you use to do the version control close to the repository and within Spring Data REST. Using the aspect is similar, it is just a more abstract version of the event handler (disregarding actual implementation).

Daniel
  • 458
  • 5
  • 16
  • 1
    I like all of your answer. I will try to implement it within my project and than mark your answer. Thanks. – Lubo Jan 23 '20 at 13:21
  • Hi @Daniel, BugFixForSpringDATAREST524 configuration is started (checked using debug, that afterPropertiesSet method is being called), but HandleBeforeLinkSave method is not trigered when my test does PUT on `/api/aaa/1/bbb` with content type `RestMediaTypes.TEXT_URI_LIST` and body of PUT method containing href to some BBB resource. What should I check? (I use Spring Boot :: (v2.2.4.RELEASE)) – Lubo Jan 25 '20 at 09:14
  • 1
    @Lubo I'm sorry, I have not yet handled `@HandleBeforeLinkSave` events and thought they would work the same. It's a bit different when handling those link changes. I have updated my example code above - you need to add `@Component` to your handler and add a `Bbb` parameter to your handler method. Please read the comment I wrote - I'm not sure how to handle multiple arguments yet. But in your case the above changes should make it work. – Daniel Jan 25 '20 at 10:10
  • 1
    It seems that your suggestion is working, but... It throws an exception when creating given rest resource using POST method to `/api/aaa` endpoint. No reference to some `/api/bbb/123` is sent, because I want to create Aaa instance which does not have reference to Bbb. Thrown exception is from IlegalArgumentException, see https://pastebin.com/Q47ptq60 PS: It seems that it is thrown, when setting other association of Aaa resource. It perhaps searches for custom `handleAaaToXxxSave(..)` method? – Lubo Jan 30 '20 at 13:54
  • 1
    @Lubo That's what I expected. As soon as you have more than one association with different types it crashes because the types are incompatible. I will open an issue in the Spring Data REST Jira and create a pull request to fix this issue so multiple methods annotated with `@HandleBeforeLinkSave` are supported. If you are fine with handling types yourself, then you may check my updated answer for a third method. – Daniel Jan 31 '20 at 10:03
  • 1
    Given solution (making handler which extends AbstractRepositoryEventListener) works. Handling code could "do nothing" in case of `linked` instance is something other than `Bbb`. This way association for other types work. One question arises. What in case that I have more (and different) associations to Bbb? For example `private Bbb author` and `private Bbb lastUpdateBy`. How can I get info about relation which is going to be "onBeforeLinkSave"? PS: this seems that things are getting more and more complicated :/ – Lubo Jan 31 '20 at 15:55
  • 1
    @Lubo The current implementation seems to be unsuitable to deal with such detailed concerns. Thanks for mentioning this, I will include it in my pull request to the project. If you need those details right now, I guess your best bet is to do all of that manually using the AOP method. – Daniel Jan 31 '20 at 16:04
  • One more comment. I would like to return some data after association. By default association returns "no body data", just status code 204. I need somehow (in respect to hateoas/rest spirit) let user know that resource has been "cloned". I think that header with "Location" referring to new instance would do it. Or returning 201 status code with full Aaa resource, as it would be upon creating Aaa resource using POST method? Perhaps I would choose second approach. PS: Am I right, that now (as a workaround) I have to implement custom controller to solve my task? – Lubo Feb 01 '20 at 13:35
  • 1
    @Lubo [Check this out](https://docs.spring.io/spring-data/rest/docs/current/reference/html/#getting-started.changing-other-properties) and change your configuration accordingly, then see if it works. If everything you mentioned before in the comments is required, then you can choose whether to write your own controller that augments Spring Data REST or use the AOP approach. – Daniel Feb 01 '20 at 16:01
  • Using @HandleBeforeLinkSave seems does not respect modified bean. See code https://github.com/spring-projects/spring-data-rest/blob/master/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java#L306-L308 It does call handler method, but to subsequent save event, it sends original bean... – Lubo Feb 17 '20 at 07:45
1

There is new possibility called BeforeSaveCallback (documentation page) and BeforeConvertCallback (documentation page)in spring Moore release train. One can use something like this:

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

For more info, have a look at 23 minute of https://www.infoq.com/presentations/spring-data-enhancements/ video presentation.

Lubo
  • 1,621
  • 14
  • 33