Andrea Bevilacqua
Andrea Bevilacqua

Reputation: 1337

Refresh and fetch an entity after save (JPA/Spring Data/Hibernate)

I've these two simple entities Something and Property. The Something entity has a many-to-one relationship to Property, so when I create a new Something row, I assign an existing Property.

Something:

@Entity
@Table(name = "something")
public class Something implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "owner")
    private String owner;

    @ManyToOne
    private Property property;

    // getters and setters

    @Override
    public String toString() {
        return "Something{" +
            "id=" + getId() +
            ", name='" + getName() + "'" +
            ", owner='" + getOwner() + "'" +
            ", property=" + getProperty() +
            "}";
    }

Property:

@Entity
@Table(name = "property")
public class Property implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "shape")
    private String shape;

    @Column(name = "color")
    private String color;

    @Column(name = "dimension")
    private Integer dimension;

    // getters and setters

    @Override
    public String toString() {
        return "Property{" +
            "id=" + getId() +
            ", shape='" + getShape() + "'" +
            ", color='" + getColor() + "'" +
            ", dimension='" + getDimension() + "'" +
            "}";
    }
}

This is the SomethingRepository (Spring):

@SuppressWarnings("unused")
@Repository
public interface SomethingRepository extends JpaRepository<Something,Long> {
    
}

Through a REST controller and a JSON, I want to create a new Something:

@RestController
@RequestMapping("/api")
public class SomethingResource {

    private final SomethingRepository somethingRepository;

    public SomethingResource(SomethingRepository somethingRepository) {
        this.somethingRepository = somethingRepository;
    }

    @PostMapping("/somethings")
    public Something createSomething(@RequestBody Something something) throws URISyntaxException {
        Something result = somethingRepository.save(something);
        return result;
    }
}

This is the JSON in input (the property id 1 is an existing row in the database):

{
  "name": "MyName",
  "owner": "MySelf",
  "property": {
    "id": 1
  }

}

The problem is: after the method .save(something), the variable result contains the persisted entity, but without the fields of field property, validated (they are null):

Output JSON:

{
  "id": 1,
  "name": "MyName",
  "owner": "MySelf",
  "property": {
    "id": 1,
    "shape": null,
    "color": null,
    "dimension": null
  }
}

I expect that they are validated/returned after the save operation.

To workaround this, I have to inject/declare the EntityManager in the REST controller, and call the method EntityManager.refresh(something) (or I have to call a .findOne(something.getId()) method to have the complete persisted entity):

@RestController
@RequestMapping("/api")
@Transactional
public class SomethingResource {

    private final SomethingRepository somethingRepository;
    
    private final EntityManager em;

    public SomethingResource(SomethingRepository somethingRepository, EntityManager em) {
        this.somethingRepository = somethingRepository;
        this.em = em;
    }

    @PostMapping("/somethings")
    public Something createSomething(@RequestBody Something something) throws URISyntaxException {
        Something result = somethingRepository.save(something);
        em.refresh(result);
        return result;
    }
}

With this workaround, I've the expected saved entith (with a correct JSON):

{
  "id": 4,
  "name": "MyName",
  "owner": "MySelf",
  "property": {
    "id": 1,
    "shape": "Rectangle",
    "color": "Red",
    "dimension": 50
  }
}

Is there an automatic method/annotation, with JPA or Spring or Hibernate, in order to have the "complete" persisted entity?

I would like to avoid to declare the EntityManager in every REST or Service class, or I want avoid to call the .findOne(Long) method everytime I want the new refreshed persisted entity.

Upvotes: 76

Views: 131795

Answers (6)

Oliver Drotbohm
Oliver Drotbohm

Reputation: 83051

This is not a problem with Spring Data / JPA, but multiple suboptimal decisions you made in your code. Thus, the problem cannot be solved by tweaking some JPA mappings or tweaking your repository interface. At least not without creating additional negative side-effects that effectively worsen the situation. Let me try to go through the issues one by one:

1. You're not modelling aggregates

An entity managed by a Spring Data repository needs to follow aggregate semantics to work properly, which means that, in the case of JPA, @ManyToOne relationships do not have a place in such a model. As you didn't go into whether a repository for Property exists, there are two options:

a) No PropertyRepository exists

A @ManyToOne relationship is invalid, as aggregates (your Something) need to govern the consistency of the entities they contain. A many-to-one relationship allows two Somethings to exist, which subverts that requirement. A change to one of them could render the constraints in the other invalid.

b) A PropertyRepository exists (i.e. Property is an aggregate itself)

The relationship must not be modeled as a full object reference but as a by-id reference. If that's the case, the entire question disappears, as you'd just bind the identifier from the request anyway, resolve it for presentation purposes and create a JSON response through some DTO.

2. You're (ab)using your domain entity to bind partial request data

You've designed a Something to capture the state related to the Property instances, and now magically expect a persistence mechanism to detect, whether the fields of Property that are null are intentionally null (i.e. are supposed to be set to null). Or whether they're supposed to be filled up with the actual values available in the current state of the instance that you only provided the identifier for. That's impossible. The persistence mechanism cannot know that.

You have overloaded the aggregate type with two concerns. Maintaining aggregate state and acting as capture mechanism for incomplete data. The solution is to rather use a dedicated type to achieve the latter and introduce a dedicated mapping step from the bound request body to the entity model. That may include a resolution of the Property identifier (in case of 1b applying) or not (for 1a).

Summary

There's no need for a custom repository extension that basically leaks implicit JPA concepts into calling code, any kind of JPA EntityManager.merge(…)/(….refresh(…)) Kung-Fu or even interacting with the abstraction (Session) behind the abstraction (JPA) behind the abstraction (Spring Data). All you need is proper modelling and separation of the concerns “managing aggregate state” and “binding request data to an object”.

Upvotes: 21

Sahil Chhabra
Sahil Chhabra

Reputation: 11666

Instead of defining EntityManager in each of your resource, you can define it once by creating a Custom JpaRepository. Reference

Then use the refresh of your EntityManager in each of your repository directly.

Refer the below example:

CustomRepository Interface

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import java.io.Serializable;

@NoRepositoryBean
public interface CustomRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
  void refresh(T t);
}

CustomRepository Implementation

import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.io.Serializable;

public class CustomRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
    implements CustomRepository<T, ID> {

  private final EntityManager entityManager;

  public CustomRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
    super(entityInformation, entityManager);
    this.entityManager = entityManager;
  }

  @Override
  @Transactional
  public void refresh(T t) {
    entityManager.refresh(t);
  }
}

Enable Custom JPARepository in Spring Boot Application Class

@SpringBootApplication
@EnableJpaRepositories (repositoryBaseClass = CustomRepositoryImpl.class)
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

Your Something Repository

public interface SomethingRepository extends CustomRepository<Something, Long> {

}

Use Refresh directly in SomethingResource (Assuming Something is an Entity)

@RestController
@RequestMapping("/api")
@Transactional
public class SomethingResource {

    private final SomethingRepository somethingRepository;

    public SomethingResource(SomethingRepository somethingRepository) {
        this.somethingRepository = somethingRepository;
    }

    @PostMapping("/somethings")
    public Something createSomething(@RequestBody Something something) throws URISyntaxException {
        Something result = somethingRepository.save(something);
        somethingRepository.refresh(result);
        return result;
    }
}

Upvotes: 90

Dinakar
Dinakar

Reputation: 11

I would replace the property with a proxy generated using session.load

        something.setProperty(session.load(Property.class, something.getProperty().getId()))
        Something result = somethingRepository.save(something);
        return result;

Now the result will have the entire property object loaded from the db

Upvotes: 0

Vlad Mihalcea
Vlad Mihalcea

Reputation: 153700

That's not enough:

Something result = somethingRepository.save(something);

You need to manually merge the incoming entity:

Something dbSomething = somethingRepository.findOne(
    Something.class, something.getId()
);
dbSomething.setName(something.getName());
dbSomething.setOwner(something.getOwner());

somethingRepository.save(dbSomething);

Since the property attribute is using the default FetchType.EAGER, the entity should have the property attribute initialized.

But, that's strange to call the Repository twice from the REST controller. You should have a Service layer that does all that in a @Transactional service method. That way, you don't need to resave the entity since it's already managed.

@Transactional
public Something mergeSomething(Something something) {
    Something dbSomething = somethingRepository.findOne(
        Something.class, something.getId()
    );
    dbSomething.setName(something.getName());
    dbSomething.setOwner(something.getOwner());

    return dbSomething;
}

Now, you need to carefully merge every property you sent. In your case, if you send null for property you should decide whether you should nullify the @ManyToOne reference or not. So, it depends on your current application business logic requirements.

Update

If you make sure you always send back the same entity you previously fetched, you could just use merge.

em.merge(result);

But your property attribute is just an id, and not an actual child entity, so you have to resolve that yourself in the Service layer.

Upvotes: 11

Narasimha A
Narasimha A

Reputation: 445

In Spring Boot JpaRepository:

If our modifying query changes entities contained in the persistence context, then this context becomes outdated.

In order to fetch the entities from the database with latest record.

Use @Modifying(clearAutomatically = true)

@Modifying annotation has clearAutomatically attribute which defines whether it should clear the underlying persistence context after executing the modifying query.

Example:

@Modifying(clearAutomatically = true)
@Query("UPDATE NetworkEntity n SET n.network_status = :network_status WHERE n.network_id = :network_id")
        int expireNetwork(@Param("network_id") Integer network_id,  @Param("network_status") String network_status);

Upvotes: 7

Harish Barma
Harish Barma

Reputation: 634

By the time you persist the entity it will be in managed state so if you just call something.getProperty(); it loads from the database and fills the property value of the something entity

public Something save(Something something) {
    em.persist(something);
    something.getProperty();
    return something;
}

so normally when you have many-to-one relationship that should be fetched automatically. If not calling the getters of the objects in entity will fill them too by firing a new DB Find request.

Upvotes: -1

Related Questions