I am having a conceptual problem on which I hope someone might have an answer.
What do I already have?
I have a RESTful service which offers CRUD-like operations for a single type of entity. The entity (lets call it order) is represented as JSON using Jackson. The Jackson annotations are directly attached to the entity classes, there is no distinct data transport layer. The entity class contains quite a number of attributes, one-to-one and one-to-many relations.
Technologically, it is based on Wildfly 10, JAX-RS 2.0 and JPA 2.1 (using Hibernate 5).
Supported operations right now are:
- GET
orders/123
reads the entity by usingEntityManager.find
- PUT
orders/123
creates or replaces the entity by usingEntityManager.persist
orEntityManager.merge
Code sample
A simplified version of the entity looks like this:
@Entity
@Table(name = "order")
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(Include.NON_EMPTY)
public class Order {
@Id
@SequenceGenerator(name = "order_id", sequenceName = "order_id_seq", allocationSize=1)
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="order_id")
@Column(name = "id")
private long id;
@Version
private long version;
@Column(name = "correlation_id", unique = true)
private long correlationId;
@OneToOne(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JsonManagedReference
private SubOrder subOrder;
}
@Entity
@Table(name = "suborder")
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(Include.NON_EMPTY)
public class SubOrder {
@Id
@SequenceGenerator(name = "suborder_id", sequenceName = "suborder_id_seq", allocationSize=1)
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="suborder_id")
@Column(name = "id")
private long id;
@Version
private long version;
@OneToOne(cascade = { CascadeType.ALL }, fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", foreignKey = @ForeignKey(name = "suborder_order_FK1"))
@JsonBackReference
private Order order;
@Column(name = "field1", length = CommonConstants.LENGTH_4, nullable = true)
private String field1;
}
A JSON representation would look like this:
{
"id": 6000000197444,
"version": 358,
"correlationId": 4711,
"suborder": {
"id": 1000000005025,
"version": 40,
"field1": "value"
}
}
What do I want to do?
Now, I want to add a field field2 to the entity Order
. The service has multiple clients - Client1 and Client2.
Client1 performs an update of the entity and gives the new field the value newValue:
{
"id": 6000000197444,
"version": 358,
"correlationId": 4711,
"field2": "newValue",
"suborder": {
"id": 1000000005025,
"version": 40,
"field1": "value"
}
}
Client2 is not interested in the new field. Therefore, no new version of Client2 is developed. Now, it wants to change the value of field1
to the value anotherNewValue. As it does not know of the new field field2
, it PUTs the following JSON to the service:
{
"id": 6000000197444,
"version": 358,
"correlationId": 4711,
"suborder": {
"id": 1000000005025,
"version": 40,
"field1": "newValue"
}
}
Alternatively, Client1 decides to remove the value from field2
once again. Because the value is now empty and, according to the Jackson annotations, empty values are not included, the following JSON is being PUT:
{
"id": 6000000197444,
"version": 358,
"correlationId": 4711,
"suborder": {
"id": 1000000005025,
"version": 40,
"field1": "newValue"
}
}
What is the problem?
This solution provides a serious problem: A client using an older representation of the entity will accidentially remove fields that are unknown to it.
So basically it will break forward compatibility which is a huge issue for me!
I have some possible solutions, but each of it has serious shortcomings:
- Deploy a new version of the service for each evolution of the entity: Easy and will work for sure, but results in a large number of versions that have to be maintained at the same time.
- Use a specific media type for each version: This would allow the client to specifiy which version of the entity it refers to. However, I would need to manually merge the differences between the received entity (taking into account which fields are present in which version) and the one in the database.
- PATCH with partial updates: I could split the problem in two problems:
- Create the patch (client): I would have to manually compare the entity before and after the modification.
- Apply the patch (server): I would have to select the entity from the database and manually apply the patch.
Where I would like your help
I think that is a pretty common problem so I hope some of you has already done something like this. I have to specific questions: 1. Does anyone know of a Java library that allows to create and apply patches on arbirtary object trees (as required in solutions 2 und 3)? 2. Does anyone have a simpler solution?
Sorry that the text became so long, but I wanted to included all relevant details. Thank you very much for your time!