Reputation: 1337
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
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:
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:
PropertyRepository
existsA @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 Something
s to exist, which subverts that requirement. A change to one of them could render the constraints in the other invalid.
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.
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).
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
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
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
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.
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
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
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