Spring-hateoas: How to extend the EntityModel now that all constructors are deprecated?

Created on 26 May 2020  Â·  7Comments  Â·  Source: spring-projects/spring-hateoas

Hi,

I moved to Spring HATEOAS 1.1.0.
I want to inherit from EntityModel but all constructors are deprecated.

Are they expect to become private? If not, what is their future? I suppose they will become protected.

We use Sonar to forbid new usage of deprecated elements. This is problematic for us.

As an alternative, I wanted to extend RepresentationModel instead but my model is very similar to EntityModel and does not behave like it. I guess it has something to do with EntityModel registered mixins. I'd like to avoid registering all mixins myself.

Could all accesses to content (in EntityModel) be done through getContent or a new protected method?

question waiting for feedback core mediatypes

All 7 comments

The constructors are deprecated as we'd like users to rather use the factory methods when using EntityModel directly. We're still gonna keep them around in protected form just as we do for the default constructor. That said, we usually encourage to extend RepresentationModel rather than EntityModel.

Indeed, EntityModel is designed as hypermedia-based container around a plain domain object. It's not really built as an extension point and thus none of our serializers and deserializers are built for that.

Like Ollie said, if you need something custom, extend RepresentationModel and go from there.

Hello,

I have the same problem right now as @reda-alaoui.

I am currently implementing a custom mediatype (Siren). In my case it would help a lot if a user of the mediatype could simply extend CollectionModel (or EntityModel). CollectionModel already represents the Siren Entity Model to a large extent, the only thing missing is the definition of properties. Extending the CollectionModel and adding some members to the subclass would make it very easy to fully implement the Siren Entity Model.

Extending the RepresentationModel is of course possible, but makes the implementation of the custom mediatype unnecessarily difficult in some parts. Especially the deserialization of an extended RepresentationModel is difficult to implement, because it must be explained to the deserializer where to deserialize which parts. In my case it would be easier if the CollectionModel (or EntityModel) could be extended and the user could simply follow a number of conventions.

Extending the models would of course only be relevant in this custom mediatype and a user of the custom mediatype cannot assume that the additional information is respected in every available mediatype.

Currently I'm a bit 'shy' about supporting to extend the models as I don't know how Spring Hateoas will behave or what it will support in the future. So I don't want to ignore the recommendations, but ask for further feedback so that I can implement a robust and durable custom mediatype. :)

Indeed, EntityModel is designed as hypermedia-based container around a plain domain object. It's not really built as an extension point and thus none of our serializers and deserializers are built for that.

(Sorry for being late to the party, just stumbled upon this)

@gregturn The case for extending EntityModel is not only customizing the representation model. It makes also sense for wrapping plain domain objects without modification like

public class TodoEntityModel extends EntityModel<Todo> {

    public TodoEntityModel(Todo content, Iterable<Link> links) {
        super(content, links);
    }

    public TodoEntityModel(Todo content) {
        super(content);
    }

}

Use case: writing a representation assembler. Here you won't get along with EntityModel<Todo> due to type erasure. Or am I missing something?

@Component
public class TodoModelAssembler extends RepresentationModelAssemblerSupport<Todo, TodoEntityModel>{

    private final RepositoryEntityLinks entityLinks;

    public TodoModelAssembler(RepositoryEntityLinks entityLinks) {
        super(Todo.class, TodoEntityModel.class); // no way to use EntityModel<Todo> her, right?
        this.entityLinks = entityLinks;
    }

    @Override
    public TodoEntityModel toModel(Todo entity) {
        TodoEntityModel todoEntityModel = new TodoEntityModel(entity);
        todoEntityModel.add(entityLinks.linkToItemResource(Todo.class, entity.getId()).withSelfRel());
        return todoEntityModel;
    }
}

@rgielen – The resource type is only used to create an instance of it reflectively. That in turn is only the case if you leverage ….createModelWithId(…) that automatically creates a link to the controller class handed in as the first parameter of the super constructor (you hand in Todo there which I think is wrong but doesn't cause any problem as you essentially bypass all logic the superclass is built for). The assembler you show here would just work fine if it implemented RepresentationModelAssembler directly and doesn't need a dedicated EntityModel subtype (unless you have other reasons for that one to exist).

@ingogriebsch – REST (haha!) assured, the constructors will stay accessible to media type implementations, as they of course need them due to the reasons you correctly laid out here.

Let me clarify my comment here: when I wrote "users", I meant folks like René. Application developers that implement controllers using those model APIs. We'd rather see folks implement RepresentationModel, wrap their entity and explicitly expose getters that might delegate to the target entity but also hide some of the properties maybe. With EntityModel you expose everything by default and then have to tediously @JsonIgnore everything you want to hide _on a domain object_. It's a matter of taste to some degree, but that was the original thought behind my short "do this, not that" recommendation.

Does that make sense? Inclined to resolve as "works as designed".

@odrotbohm thanks for the clarifications.

The resource type is only used to create an instance of it reflectively. That in turn is only the case if you leverage ….createModelWithId(…) that automatically creates a link to the controller class handed in as the first parameter of the super constructor (you hand in Todo there which I think is wrong but doesn't cause any problem as you essentially bypass all logic the superclass is built for).

Good catch - My fingers mindelessly expanded the needed class reference to type T of RepresentationModelAssemblerSupport rather than a controller reference 🙂. However, I'd have a hard time referencing a controller class as described below.

The assembler you show here would just work fine if it implemented RepresentationModelAssembler directly and doesn't need a dedicated EntityModel subtype (unless you have other reasons for that one to exist).

My concrete use case is enhancing a REST API, for which 80% of functionality comes out of Spring Data REST, with
@RepositoryRestController mixins. That said, using RepresentationModelAssemblerSupport makes perfect sense IMO:

@RepositoryRestController
class TodoController {

    final TodoRepository repository;
    final TodoModelAssembler assembler;

    TodoController(TodoRepository repository, TodoModelAssembler assembler) {
        this.repository = repository;
        this.assembler = assembler;
    }

    @GetMapping("/todos/{id}/children")
    public @ResponseBody
    CollectionModel<TodoEntityModel> findChildren(@PathVariable Integer id) {
        List<Todo> children = repository.findChildren(id);
        return assembler.toCollectionModel(children)
                .add(
                        linkTo(
                                methodOn(TodoController.class)
                                        .findChildren(id))
                                .withSelfRel()
                );
    }

}

So I'm keen on having the pre-implemented RepresentationModelAssemblerSupport.toCollectionModel() method at hand, to render an item list with proper self references pointing to item resources handled by Spring Data REST.

Your catch above raises the question what a valid controller reference would look like - since the TodoController is not the 'real' controller for the todos resource.

Side note: my actual use case is more complicated than this example, where a bi-directional relation with a passive end would be enough to get children in a parent-child relationship. This is just an exploratory example

Side note 2: I'm not trying to mis-use this discussion for support, I'd simply like to expand on a IMO valid use case for EntityModel's constructors in at least protected visibility, for regular users as pointed out by Oliver.

EDIT:

Does that make sense? Inclined to resolve as "works as designed".

I'd second that. However, clarifying the deprecation note to state that visibility might be changed to protected in future releases would be helpful to end users, IMO

@odrotbohm Rollback, I see now what I was missing - the toCollectionModeldefault method on RepresentationModelAssembler. Sorry for the noise, you are right - no need to extend EntityModel

public class TodoModelAssembler implements RepresentationModelAssembler<Todo, EntityModel<Todo>> {

    private final RepositoryEntityLinks entityLinks;

    public TodoModelAssembler(RepositoryEntityLinks entityLinks) {
        this.entityLinks = entityLinks;
    }

    @Override
    public EntityModel<Todo> toModel(Todo entity) {
        return EntityModel.of(entity)
                .add(entityLinks.linkToItemResource(Todo.class, entity.getId())
                        .withSelfRel()
                );
    }
}

The reference docs expand on RepresentationModelAssemblerSupport and not so much on RepresentationModelAssembler, this was a bit misguiding I guess.

Thanks anyway. Obviously I still second closing the issue, clarifying the deprecation notice would be helpful though.

Was this page helpful?
0 / 5 - 0 ratings