Sebastian
Sebastian

Reputation: 988

Second transaction overrides changes from first transaction

I working in a project where we use Spring Data Rest with HATEOAS, Spring JPA and Hibernate and at the moment we are experiencing a weird problem we were not able to fix:

  1. The Angular frontend transmits two HTTP requests to the backend. Both requests want add a relation in different columns on the same row (because of HATEOAS this has to be done in two requests).
  2. Two transactions (A and B) are created and Hibernate performs a select in each transaction to get the row in question.
  3. Hibernate gets the entitiy that will be linked in each transaction.
  4. Hibernate updates the selected row once in transaction A. Transaction B updates the row shortly afterwards and resets the value set in transaction A back to the value from the initial select that was performed at the beginning of the transaction. So we are losing all the information of transaction A.

The problem is that this does not seem to be deterministic. It just happens sometimes and then works fine the following ten times.

We were not able to fix this by adding locking (optimistic locking just leads to an ObjectOptimisticLockingFailureException in transaction B) and pessimistic locking had no effect either. We are using DynamicUpdate for the entity. We tried setting propagation on the transactions but that did not change anything. Right now we are out of ideas and googling did not yield any results.

The entitiy that should be updated looks as follows:

@AllArgsConstructor
@RequiredArgsConstructor
@NoArgsConstructor
@Data
@JsonIgnoreProperties(value = { "history" }, allowGetters = true)
@DynamicUpdate
@SelectBeforeUpdate
@Entity
@Table(name = "parameters")
public class Parameter
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "parameter_id", unique = true)
    private Long id;

    @NonNull
    @Column(name = "name", nullable = false, unique = true)
    private String key;

    @ManyToOne(fetch = FetchType.EAGER,
            cascade = {
                    CascadeType.REFRESH,
                    CascadeType.MERGE,
                    CascadeType.PERSIST,
                    CascadeType.DETACH
            }
    )
    private Component component;

    @OneToOne(fetch = FetchType.EAGER,
            cascade = {
                    CascadeType.REFRESH,
                    CascadeType.MERGE,
                    CascadeType.PERSIST,
                    CascadeType.DETACH
            }
    )
    private ParameterTypes type;

    @OneToMany(
            mappedBy = "parameter",
            fetch = FetchType.LAZY,
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    @OrderBy("history_creation_timestamp DESC")
    private List<History> history = new LinkedList<>();

    @CreationTimestamp
    @Column(name = "parameter_creation_timestamp")
    private LocalDateTime creationTimestamp;

    @UpdateTimestamp
    @Column(name = "parameter_update_timestamp")
    private LocalDateTime updateTimestamp;
}

And the fields component and type should be updated.

It would be great if we could assure that both requests update the row correctly or transaction B waits for transaction A to finish and re-selects the row in its updated state.

We greatly appreciate any assistance since we were unable to find a working solution in the last three days. We will gladly provide any additional information. Thank you all in advance.

Frontend

We are using angular4-hal with Angular 7 to communicate with the Spring backend. The call is rather simple and looks as follows:

saveParameter(): void {
    this.editMode = false;
    const saveObservs: Observable<any>[] = [];
    if (this.componentControl.dirty) {
    saveObservs.push(this.parameter.addRelation('component', this.componentControl.value));
    }

    if (this.typeControl.dirty) {
    saveObservs.push(this.parameter.addRelation('type', this.typeControl.value));
    }

    forkJoin(
    saveObservs
    ).pipe(
    catchError(err => of(err))
    ).subscribe(val => console.log(val));
}

componentControl and typeControl are both FormControl instances.

The addRelation function looks like this:

Resource.prototype.addRelation = function (relation, resource) {
    if (this.existRelationLink(relation)) {
        var header = ResourceHelper.headers.append('Content-Type', 'text/uri-list');
        return ResourceHelper.getHttp().put(ResourceHelper.getProxy(this.getRelationLinkHref(relation)), resource._links.self.href, { headers: header });
    }
    else {
        return observableThrowError('no relation found');
    }
};

And the context of the function can be found here

Upvotes: 0

Views: 258

Answers (1)

Selindek
Selindek

Reputation: 3423

Unfortunately I don't know Angular 7, but I have a feeling that saveObservs.push works asynchronously. Which means that sometimes the 2nd request reaches the API before the 1st one was finished, load the unmodified record from the DB, and then write it back with only the 2nd reference modified.

So the 2nd push should wait for the result of the 1st one.

However it won't solve the case if you try to modify the same object from two client at the same time.

You should add version property to the entity and Spring Data Rest will automatically respond with an error if you try to modify an object with an obsolete version.

  @Version
  private Long version;

Upvotes: 1

Related Questions