spring-projects / spring-projects/spring-data-rest

Feat: Make it easier for clients to deal with entity relationships

Open
#2,006 0 comments 0 reactions 1 assignee View on GitHub

@odrotbohm is already working on this.

Since May 3, 2021.

status: waiting-for-triage
Dominant language
Java
Stars
958
Forks
568
PR merge metrics
No merged PRs in 30d

Description

Here is an example from my app...

My app has a list of Event records, where each Event is associated with one or more Jurisdictions:
Event <-- Many-to-Many --> Jurisdiction

I need the app to do the following:

  1. Display a list of Events, with each event displaying the associated Jurisdictions
  2. Get a list of all possible Jurisdictions
  3. Given these two pieces of information, the user should be able to edit the Jurisdictions associated with the Event

For example, the client might display an edit form with the list of possible Jurisdictions in an HTML SELECT control, with the Jurisdictions that are already on the event pre-selected.

This is a super common use case, and I think Spring Data REST should support it with little or no customization.

In the current implementation, the following requests are needed:

  1. GET /events
  2. (for each event) GET /events/jurisdictions // <-- this is no good!! N+1 queries
  3. GET /jurisdictions
  4. PUT /events

You might think that I could implement an excerpt projection that renders the expanded Jurisdictions list for each event:

@Projection(name = 'expanded', types = [Event])
interface EventExpandedProjection {
    String getEventName()
    Date getEventDate()
    String getDetails()
    List<Jurisdiction> getJurisdictions()
}

In theory, I can now perform the following queries:

  1. GET /events?projection=expanded
  2. GET /jurisdictions
  3. PUT /events

However, this presents two major problems:

  • I've duplicated every single property on the Event object in my projection interface, so it's not DRY
  • The expanded JSON for jurisdictions on the Event projection looks like this:
                "jurisdictions": [
                    {
                        "jurisdictionName": "Delaware"
                    },
                    {
                        "jurisdictionName": "Pennsylvania"
                    }
                ],

From the client's perspective, when I edit an existing Event record, I need to match up the list of jurisdictions on the Event with the list of possible Jurisdictions from the GET /jurisdictions. It's not clear how I should do this if there are no IDs for the jurisdictions and no HAL links!

In Grails, a simple "expand" flag in the view layer can be added, which looks like this (_event.gson):

model {
	Event event
}

json g.render(event, [resolveTemplate: false, expand: ['jurisdictions']])

This is similar to what's proposed in #626.
Grails, however, doesn't use HAL links by default... everything is handled by the ID, which is rendered in the response.

In Spring Data REST, there are no IDs by default, so I have nothing to match against. In order to fix this, the framework should render the HAL links for associated entities when expanding the association.

This way the client can match the Jurisdictions on the Event to the list of possible Jurisdictions, using the self link URLs as the key.

If the framework doesn't want to do this out of the box, it might make sense to define some kind of easy customization to implement this behavior... maybe something like this:

@Entity
class Event {
    String eventName
    Date eventDate
    String details

    @ManyToMany(fetch = FetchType.EAGER)
    @Expand(links=true) // <-- this annotation signals that the jurisdictions list should be expanded whenever the Event is rendered
    List<Jurisdiction> jurisdictions
}

Whether I use GET /events or GET /events/(id), my JSON for each event should now include the jurisdictions, where each jurisdiction has its own links collection:

{
    "eventName": "Event",
    ...
    "jurisdictions": [
        {
            "jurisdictionName": "Delaware"
            "_embedded": { ... }
        }
    ],
    "_embedded": { ... }
}

A really robust solution should also detect cycles or repeated entities in the rendered object graph, and simply render the self-link for objects that have already been rendered in the output, while omitting the rest of the properties. (This is pretty much what the Grails framework does OOTB.) This behavior should also be customizable.

A partial alternative solution, which requires more work in the client app, might be to support EntityModel in projections:

@Projection(name = 'expanded', types = [Event])
interface EventExpandedProjection {
    String getEventName()
    Date getEventDate()
    String getEventDetails()
    List<EntityModel<Jurisdiction>> getJurisdictions()
}

This doesn't solve the DRY issue, but at least it gives me a customization option where I can include the links.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.