I have an Account object containing a OneToMany relation with Beneficiary object and this relationship is bi-directional so I have a ManyToOne relation in the Beneficiary Object with Account Object
public class Account {
#Id
#GeneratedValue
private Long id;
private String name;
private String number;
//Other fields
#OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
#JsonManagedReference
private List<Beneficiary> beneficiaries = new ArrayList<>();
}
public class Beneficiary {
#Id
#GeneratedValue
private Long id;
//Other fields
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "account_id")
#OnDelete(action = OnDeleteAction.CASCADE)
private Account account;
}
In the JSON response, I need the Account information containing the list of Beneficiaries and for each Beneficiary I just need the Account name and Account number. Is it possible to serialize it somehow so that I get response in this fashion? Or do I need to modify my entity structures?
Sample Account Response -
{
"id": 123,
"name": "Name1",
"number": "111111",
"beneficiaries": [
{
"id": 1,
"account": {
"name": "Name2",
"number": "222222"
}
},
{
"id": 2,
"account": {
"name": "Name3",
"number": "333333"
}
}
]
}
You are not supposed to serialize your JPA objects. Instead, you need to define domain objects. These are objects are the ones to be serialize and exposed to the business. This abstraction decouples your REST or SOAP or whatever interface with your JPA layer.
I would create a domain class for your account class. Call it AccountDTO or something like that. Any object being returned from your JPA repositories need to be mapped to this DTO objects and bubbled up to the services layer. Then your DTO is the class which models your business needs. In there you can just put the accounts and the beneficiaries names.
DTO stands for Data Transfer Objects. These are the ones supposed to be serialized and sent between systems.
One idea would be to use a custom serializer.
You would have to write a custom serializer, similar to this:
public class NestedAccountSerializer extends StdSerializer<Account> {
public NestedAccountSerializer() {
this(null);
}
public NestedAccountSerializer(Class<Account> t) {
super(t);
}
#Override
public void serialize(Account account, JsonGenerator generator, SerializerProvider provider) throws IOException {
generator.writeObject(new AccountView(account.getName(), account.getNumber()));
}
private static class AccountView {
#JsonProperty
private final String name;
#JsonProperty
private final String number;
AccountView(String name, String number) {
this.name = name;
this.number = number;
}
}
}
And then use it like this in your Beneficiary class:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "account_id")
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonSerialize(using = NestedAccountSerializer.class)
private Account account;
Please, let me know if it helped.
Related
Say I have a oneToMany relationship between Person and Job. Each person has only one job, while a job has many persons.
I have a controller which calls the service which calls the repository that will execute the queries.
here they are:
#RestController
#CrossOrigin()
#RequestMapping(path = "api/person")
public class PersonController {
private final PersonService personService;
#Autowired
public PersonController(PersonService personService) {
this.personService = personService;
}
#PostMapping
public Person storePerson(#RequestBody Person person) {
return this.personService.storePerson(person);
}
//...more code is also here
}
#Service
public class PersonService {
private final PersonRepository personRepository;
#Autowired
public PersonService(PersonRepository personRepository, CountryRepository countryRepository,
JobRepository jobRepository, RoleRepository roleRepository, HairColorRepository hairColorRepository) {
this.personRepository = personRepository;
}
public Person storePerson(Person person) {
return this.personRepository.save(person);
}
//...more code is also here
}
#Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
}
Now the models and the way I define the relationship between them. I can code this in two ways.
Senario 1:
#Entity
#Table(name = "people")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = Job.class)
#JoinColumn(name = "job_id")
private Job job;
// ...getters and setters, constructors, toString(), etc are here
}
#Entity
#Table(name = "jobs")
public class Job {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String name;
#OneToMany(mappedBy = "job", orphanRemoval = true, cascade = CascadeType.ALL)
private Set<Person> persons;
// ...getters and setters, constructors, toString(), etc are here
}
I use postman to insert records into this database.
I send a POST request and this is the body:
First Json
{
"name": "James",
"job": {
"id": null,
"name": "Doctor"
}
}
This works perfectly, because it creates the person, it also creates a new job that DID NOT EXIST in the database, and also creates the relationship between the two.
But on second request, I want to reuse the Job. So I make this request:
Second Json
{
"name": "David",
"job": {
"id": 1,
"name": "Doctor"
}
}
Here I get an Exception:
{
"timestamp": "2022-08-05T11:20:41.037+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "detached entity passed to persist: ir.arm.archiver.job.Job; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: ir.arm.archiver.job.Job",
"path": "/api/person"
}
Senario2
If I change the Cascade values in the relationship annotations a bit, I get the exact opposite results. If in Person.java I change the annotations for the private Job job field to use Cascade.MERGE like this:
#ManyToOne(cascade = CascadeType.MERGE, fetch = FetchType.EAGER, targetEntity = Job.class)
#JoinColumn(name = "job_id")
private Job job;
Then, when I pass the First Json, this time, I get an exception:
{
"timestamp": "2022-08-05T11:36:17.854+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : ir.arm.archiver.person.Person.job -> ir.arm.archiver.job.Job; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : ir.arm.archiver.person.Person.job -> ir.arm.archiver.job.Job",
"path": "/api/person"
}
BUT, if I create the job record myself in the database, and then I execute the request with the Second Json, it will work, and create the person with the relationship to the existing job record.
Now my question is:
How Can I combine the two? I want the JPA to do both.
Is there any way, to be able to pass both jsons and the jpa automatically creates the job, if the Id is null, and fetch and reuse it if it has Id?
I found the fix. Remove cascade attribute for Job member variable of Person Entity. JPA is combining both and also reusing the existing job from database.
Update Person Entity :
#Entity
#Table(name = "people")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToOne(fetch = FetchType.EAGER, targetEntity = Job.class)
#JoinColumn(name = "job_id")
private Job job;
// ...getters and setters, constructors, toString(), etc are here
}
Output (In DB Person Table) :
Job Table :
I'm working on a small Spring REST API project. I have two classes that represent 2 tables in my database. I have a #OneToMany mapping in the object that I want to retrive data from. Right now I retrive ALL the nested objects, but what I want is to be able to limit the amount of nested objects by its int datestamp variable (which is an epoch declared as "utcs" in my class). I was naively thinking that the CrudRepoitory could help me with that but now I understand I was wrong. What I was hoping to be able to do in my repository was something like this:
#Repository
public interface TypeRepository extends CrudRepository<Type, Integer> {
List<Type> findByDataUtcsGreaterThan(int utcs);
}
This is the JSON structure I want and how it looks right now. But how do I limit the amount of Data objects?
[
{
"typeKey": "Queue",
"uomId": 1,
"data": [
{
"value": 11,
"utcs": 1605840300
},
{
"value": 15,
"utcs": 1605840360
},
{
"value": 22,
"utcs": 1605840420
}
]
},
{
"typeKey": "Unroutable",
"uomId": 1,
"data": [
{
"value": 196,
"utcs": 1605840300
},
{
"value": 196,
"utcs": 1605840360
},
{
"value": 196,
"utcs": 1605840420
}
]
}
]
The (Type) object class with the nested object #OneToMany
#Entity
#Table(name = "SYSTEMSTATSTYPE")
public class Type {
#Id
#Column(name = "ID")
private int id;
#Column(name = "TYPEKEY")
private String typeKey;
#Column(name = "UOMID")
private int uomId;
#OneToMany(mappedBy = "type", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<Data> data = new ArrayList<>();
public Type() {
super();
}
public Type(String typeKey, int uomId) {
this.typeKey = typeKey;
this.uomId = uomId;
}
// Getters and setters
}
The (Data) object class #ManyToOne
#Entity
#Table(name = "SYSTEMSTATSDATA")
public class Data {
#Id
#Column(name = "ID")
private int id;
#Column(name = "VALUE")
private int value;
#Column(name = "UTCS")
private int utcs;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "TYPEID")
private Type type;
public Data() {
super();
}
public Data(int value, int utcs, Type type) {
super();
this.value = value;
this.utcs = utcs;
this.type = type;
}
// Getters and setters
}
I don't think there is a possibility directly with the annotation, and using EAGER fetching is in most cases not a good idea anyway. I think you have to build the logic yourself with a custom query, either fetching only the 2 Data objects (something as described here https://www.baeldung.com/jpa-limit-query-results), or a join getting you Type and Data all together right away.
That's not directly possible. You could do dedicated select queries for each Type object or maybe use something like Blaze-Persistence Entity Views which has native support for limiting collection elements.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Type.class)
public interface TypeDto {
#IdMapping
Integer getId();
String getTypeKey();
int getUomId();
#Limit(limit = "5", order = "utcs DESC")
Set<DataDto> getData();
#EntityView(Data.class)
interface DataDto {
#IdMapping
Integer getId();
int getValue();
int getUtcs();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
TypeDto a = entityViewManager.find(entityManager, TypeDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
#Repository
public interface TypeRepository extends CrudRepository<Type, Integer> {
List<TypeDto> findByDataUtcsGreaterThan(int utcs);
}
I recently started working with Spring Boot and Hibernate. I just tried to implement my first mapping between two entities.
But I don't really understand how the different mapping annotation works. I'm getting null values when I try to persist an Entity into my database.
I've an Institution Entity that can contain many Department Entities (OneToMany), and a Department Entity that relates to one Institution Entity (ManyToOne).
This is my Institution Entity
#Entity
#NoArgsConstructor
#Setter
#Getter
#Table(name = "institution")
public class Institution {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "uuid2")
#Column(name = "id", columnDefinition = "BINARY(16)")
private UUID id;
#Column(name = "name")
private String name;
#Column(name = "acronym")
private String acronym;
#OneToMany( mappedBy = "institution")
List<Department> departments;
}
Department Entity
#Entity
#NoArgsConstructor
#Getter
#Setter
#Table(name = "department")
public class Department {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "uuid2")
#Column(name = "id", columnDefinition = "BINARY(16)")
private UUID id;
#Column(name = "name")
private String name;
#ManyToOne
#JoinColumn(name = "institution_id")
private Institution institution;
}
InstitutionService
#Slf4j
#Service
#AllArgsConstructor
public class InstitutionService {
private final Logger logger = LoggerFactory.getLogger(InstitutionService.class);
private final InfoMessages infoMessages = new InfoMessages();
private final InstitutionRepository institutionRepository;
public Institution addInstitution(Institution institution) {
return institutionRepository.save(institution);
}
public Optional<Institution> getInstitutionById(UUID id) {
return institutionRepository.findById(id);
}
public List<Institution> getInstitutions(String searchParam) {
if (searchParam != null) {
return institutionRepository.findInstitutionByAcronymContainingOrNameContainingIgnoreCase(searchParam, searchParam);
}
return institutionRepository.findAll();
}
public Institution getInstitutionByName(String name){
return institutionRepository.findInstitutionByName(name);
}
}
InstitutionController
#AllArgsConstructor
#Api(value = "/institution", tags = "Institution Management System")
#RequestMapping(
path = "/institution",
produces = MediaType.APPLICATION_JSON_VALUE)
#RestController
public class InstitutionController {
private final InstitutionService institutionService;
#ApiOperation("Add institution")
#PostMapping(consumes = ("application/json"))
public ResponseEntity<WrappedResponse<Institution>> createInstitution(
#ApiParam("JSON Object representing Institution")
#RequestBody Institution institution
) {
institutionService.addInstitution(institution);
WrappedResponse<Institution> wrappedResponse = new WrappedResponse<>();
wrappedResponse.setList(Collections.singletonList(institution));
return ResponseEntity
.status(HttpStatus.CREATED)
.location(URI.create("/institution/" + institution.getId()))
.body(
wrappedResponse
);
}
}
This is my input
{
"acronym": "uio",
"departments": [
{
"name": "Biology"
}
],
"name": "University of Oslo"
}
This is the JSON Payload returned when I try to post an Institution Entity
{
"list": [
{
"id": "2163ff6a-71f4-40f7-bec2-dfeadbcdfec8",
"name": "University of Oslo",
"acronym": "uio",
"departments": [
{
"id": null,
"name": "Biology",
"institution": null
}
]
}
]
}
Why am I getting null values for Department ID and Institution ID ?
Why am I getting null values for Department ID
Because you have not specified any cascade on the OneToMany. So persisting an Institution does just that: persisting the institution. If you also want to persist the depertments it contains, the PERSIST operation (at least) needs to be cascaded. Since the department is not persisted, no ID is being generated for it.
Why am I getting null values for Institution ID
Because the institution field of the department you're saving is null.
So it's also null in the JSON. And even with a proper cascade, you would still get a null department.institution_id in the database. You need to add code to initialize this side of the association, which doesn't exist in the input JSON.
Note however that, if it was not null, since you're trying to serialize as JSON a cyclic data structure, you would get a stack overflow error. I strongly advise not to accept and return JPA entities in the controllers. Those represent the persistent state of your app. Design your API be clearly specifying what JSON you want to receive and return, and design classes representing that JSON. A JSON document is a tree. It can't be a cyclic data structure.
I found a work around for my issue in here
apperently I had some issues with Jackson and how it mapped the JSON object to my entities.
I have two entities, Book and BookEvent. They are linked by a ManyToOne-Relationship, i.e. one Book can have many BookEvents.
I view Book as the aggregate root and have one Repository for the Book entity, but not for the BookEvent.
#Entity
#Table(name = "book")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
#OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
private List<BookEvent> events = new ArrayList<>();
public List<BookEvent> getEvents() {
return events;
}
public void setEvents(List<BookEvent> events) {
for (BookEvent event: events) {
event.setBook(this);
}
this.events = events;
}
public void addEvent(BookEvent event) {
events.add(event);
event.setBook(this);
}
public void removeEvent(BookEvent event) {
events.remove(event);
event.setBook(null);
}
}
(Other getters/setters are omitted here).
#Entity
public class BookEvent {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne
#JoinColumn(name="book_id")
private Book book;
private LocalDate date;
#PreRemove
private void removeEventFromBook(){
book.removeEvent(this);
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
if (!this.book.getEvents().contains(this)) {
this.book.getEvents().add(this);
}
}
}
I now want to add a new event to the book after the book has been created. I use Spring Data Rest.
Creating the book with a POST and one event works fine:
{
"title": "My example book",
"events": [
{
"type": "BOUGHT",
"date": "2017-05-09"
}
]
}
Gives the answer:
{
"title": "My example book",
"events": [
{
"id": 3,
"date": "2017-05-09",
"_links": {
"book": {
"href": "http://localhost:8080/api/books/2"
}
}
}
]
}
But if I then do a JSON Patch to append one new event, the event is included in the response to the PATCH request, but it is actually not saved in the database (a GET on the book afterwards does not return the event and when the database the column book_id is null).
[
{
"op": "add",
"path": "/events/-",
"value":
{
"date": "2017-05-09"
}
}
]
When using the debugger, the setEvents() method is called on the initial POST request, but during the PATCH request, only the getEvents() method is called - no setBook() or addEvent() method. I think the problem is there.
Do I have a problem with my entity setup?
The problem was my setup as a bidirectional OneToMany setup without a join table. The problem can be fixed in two ways:
Create a join table. This is done by adding a #JoinTable annotation to the events attribute the Book class. This needs one additional table in the database, therefore I did not chose this way.
Use a unidirectional OneToMany setup (see Java Persistence/OneToMany). This is only supported by JPA 2.x, but this was no problem in a Spring Boot 2.0 setup. The implementation looks really clean this way.
My code now looks as following:
#Entity
#Table(name = "book")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(cascade = ALL) //The cascade is important as otherwise a new event won't be saved.
#JoinColumn(name="book_id", referencedColumnName = "id")
private List<BookEvent> events = new ArrayList<>();
//Standard getter and setter for getEvents() and setEvents()
}
#Entity
public class BookEvent {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name="book_id")
private Long bookId;
//No getter/setter for bookId was necessary
}
No special getters/setters which update the reciprocal link were necessary. This will get a clean JSON response with SDR without a _links attribute on each event. Adding and deleting new entries work as well.
Sometimes it's confusing how I should link resources within a RESTful API, consider for example the entities:
Profile (Users can create business profiles with address, details, etc..)
Plan (Already persisted in app's DB, created by administrators)
The request to create a Profile looks like:
POST /profiles
{
"name": "Business name",
"address": "The address",
"phone": "0000000000"
}
Now it is required that a Profile belongs to a Pricing Plan. So is it a good idea to do POST request like this with JSON?
POST /profiles
{
"name": "Business name",
"address": "The address",
"phone": "0000000000"
"plan": {
"id": 1
}
}
and then load the plan by the provided id and associate it with the profile being created:
#POST
#Path("/profiles")
public Response createProfile(Profile profile) {
// load plan resource from DB
Plan plan = em.find(Plan.class, profile.getPlan().getId())
// associate
profile.setPlan(plan);
// persist profile
em.perist(profile);
}
The Profile entity:
#Entity
public class Profile implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "plan_id", nullable = false)
private Plan plan;
private String name
...
// getters and setters
}
The Plan entity:
#Entity
public class Plan implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Column(nullable = false)
private String name;
#NotNull
#Column(nullable = false, columnDefinition = "text")
private String description;
#NotNull
#Column(nullable = false, precision = 8, scale = 2)
private BigDecimal price;
#NotNull
#Column(nullable = false)
private Integer days;
#OneToMany(fetch = FetchType.LAZY, mappedBy="plan", cascade = CascadeType.ALL)
private List<Profile> profiles;
...
}
In other words i am asking what I should pass to the request body in order to link a reference entity.
I would like to believe that something like this is more reasonable:
POST /plans/1/profiles
but according to the REST and JSON semantics what would be the best option?
I can also think of other ways such as providing the Plan id as a query param:
POST /profiles?planId=1
I would say you need to do the following:
Create profile with
POST: /profile
Assign plan with
PUT: /profile/<profile_id>
{
"name": <optional>,
"plan_id": <optional>,
...
}
First thing is you separate create and update (POST/PUT). Another is you state profile ID for update in URL.
You can set parameters you need to update in PUT request body and update only parameters which are set. Think it's fine with REST concept.