I'm writing a sample app using the HR schema on an Oracle db 18c.
I'm using Spring boot 2, Spring Data Jpa and Spring Rest.
I'm working on Regions' table (that contains two fields: region_id and region_name) and countries table (that contains three fields: country_id, country_name and region_id).
I can manage all CRUD operation on Regions' table if its entity doesn't contain the relationship #OneToMany with Country's entity when I add it the application return me a 415 error (non supported method) that have no sense!
Here it is my code:
Region Entity:
package it.aesys.springhr.entities;
import javax.persistence.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import java.util.List;
/**
* The persistent class for the REGIONS database table.
*
*/
#Entity
#Table(name="REGIONS")
#NamedQuery(name="Region.findAll", query="SELECT r FROM Region r")
public class Region {
#Id
#GeneratedValue(generator="REGIONS_SEQ", strategy=GenerationType.SEQUENCE)
#SequenceGenerator(name="REGIONS_SEQ", sequenceName="REGIONS_SEQ", allocationSize=0)
#Column(name="REGION_ID", unique=true, nullable=false)
private int regionId;
#Column(name="REGION_NAME", nullable=false, length=50)
private String regionName;
//bi-directional many-to-one association to Country
#OneToMany(mappedBy="region", cascade = CascadeType.ALL)
// this annotation help me to not retrieve all countries when I find all Regions but I tried also without it and anything change
#JsonBackReference
private List<Country> countries;
// constructor, getters and setters as usual
}
Country Entity:
package it.aesys.springhr.entities;
import javax.persistence.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.util.List;
/**
* The persistent class for the COUNTRIES database table.
*
*/
#Entity
#Table(name="COUNTRIES")
#NamedQuery(name="Country.findAll", query="SELECT c FROM Country c")
public class Country {
#Id
#Column(name="COUNTRY_ID", unique=true, nullable=false, length=2)
private String countryId;
#Column(name="COUNTRY_NAME", nullable=false, length=40)
private String countryName;
//bi-directional many-to-one association to Region
#ManyToOne
#JoinColumn(name="REGION_ID")
#JsonManagedReference
private Region region;
// constructor, getters and setters as usual
}
The RegionRepository Interface is simply:
package it.aesys.springhr.dao;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import it.aesys.springhr.entities.Region;
public interface RegionRepository extends JpaRepository<Region, Integer> {
}
and the RegionService contains this method:
public void save(Region theRegion) {
regionRepository.save(theRegion);
}
and finally the RegionRestController contains this method:
#PostMapping( value= "/regions", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8")
public Region addRegion(#RequestBody Region theRegion) {
// also just in case they pass an id in JSON ... set id to 0
// this is to force a save of new item ... instead of update
theRegion.setRegionId(0);
regionService.save(theRegion);
return theRegion;
}
I'm using CURL to test this app and I tried to pass countries in different ways but no one works!
Can I resolve without using some external framework like Map Struct? Or, in this case I MUST create a new object that mapping what I receive with what I must persist?
[Edit]: I modify the last method with this code:
#PostMapping( value= "/regions", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8")
public Region addRegion(#RequestBody HashMap) {
Region theRegion= new Region();
theRegion.setRegionId(0);
theRegion.setRegionName(map.get("regionName"));
regionService.save(theRegion);
return theRegion;
}
and now it works but I'm not sure that this solution is secure because it seems so simply and so generic ...
As I edited, I modify the last method with this code:
#PostMapping( value= "/regions", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8")
public Region addRegion(#RequestBody HashMap<String, String>) {
Region theRegion= new Region();
theRegion.setRegionId(0);
theRegion.setRegionName(map.get("regionName"));
regionService.save(theRegion);
return theRegion;
}
and now it works but I'm not sure that this solution is secure because it seems so simply and so generic so if you think that it is not sure please answer and tell me another better solution!
You can use #Component in service class and controller class and #ComponentScan(basePackages = "<full package name>" in the main class. Problem may be solved.
Related
Good Day developers , i'm hardly striving with this problem on my App which use SpringBoot framework.Basically can't put two and two together about how deleting one of the items in the relation ship once its parent is delete. Here my explanation:
First both entities with its respective relation to each other:
Product(Children)
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO,generator = "native")
#GenericGenerator(name="native",strategy="native")
private Long id;
#OneToMany(mappedBy = "products",fetch= FetchType.EAGER,cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Category> categorySet= new HashSet<>();
CONSTRUCTOR FOR PRODUCTS ENTITY
-------------------------------------GETTERS AND SETTERS---------------------------------
Being this the Product entity under the premise of one product being able to clasify to several categories hence its relation OnetoMany.Then:
Categories(Parent)
#Entity
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO,generator = "native")
#GenericGenerator(name="native",strategy="native")
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name="product_id")
private Product products;
CONSTRUCTOR FOR CATEGORY ENTITY
---------------------------GETTERS AND SETTERS-----------------------------
Following the former concept but withan inverse logic applied Category reltion toward products, and works perect on my database.
on repositories lets say i set this
Category Repository
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.Collection;
#RepositoryRestResource
public interface CategoryRepository extends JpaRepository <Category,Long> {
}
Product Repository
package com.miniAmazon;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.*;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
#RepositoryRestResource
public interface ProductRepository extends CrudRepository<Product,Long> {
Product findByProductName (String productName);
}
Then trying to set the command to delete products or categories from my Jpa and Crud Reps, using Junit Test on the Category Entity, like this:
Category Entity
#Test
public static void whenDeletingCategories_thenProductsShouldAlsoBeDeleted() {
ProductRepository.deleteAll();
assert(CategoryRepository.count()).isEqualTo(0);
assert(ProductRepository.count()).isEqualTo(0);
}
#Test
public static void whenDeletingProducts_thenCategoriesShouldAlsoBeDeleted() {
CategoryRepository.deleteAll();
assert(CategoryRepository.count()).isEqualTo(0);
assert(ProductRepository.count()).isEqualTo(2);
}
Throws me an error saying that "Non-static method 'deleteAll()/count()' cannot be referenced from a static context".
Any idea about why this is happening .Any advice ?.Thanks in advance!!!!.Have a good day!!!
Try using instantiated beans CategoryRepository and ProductRepository instead of the interfaces.
You are try to use Non-static method of interface but deleteAll() or count() are not static method. Try to create a repository object then autowired it to call deleteAll() / count() method.
#Autowired
private CategoryRepository categoryRepository;
And use categoryRepository to call call deleteAll() / count() method
categoryRepository.deleteAll();
assert(categoryRepository.count()).isEqualTo(0);
I created two tables -student and subject.
Then I created a third table student_subject, which is a joint table to connect students with subjects. It has 5 fileds: row id, student id, subject id and the created_at and updated_at fields (which comes from the AuditModel class):
package com.rest.web.postgresapi.data.models;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
#Entity
#Table(uniqueConstraints = {
#UniqueConstraint(columnNames = {"student_id", "subject_id"})
})
public class StudentSubject extends AuditModel {
#Id
#GeneratedValue(generator = "enrollment_generator")
#SequenceGenerator(
name = "enrollment_generator",
sequenceName = "enrollment_sequence",
initialValue = 4420
)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "student_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonIgnore
private Student student_id; // If I put private Long student_id, it fails
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "subject_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonIgnore
private Subject subject_id; // If I put private Long subject_id, it fails
// Constructors
protected StudentSubject() {}
public StudentSubject(Student student_id, Subject subject_id) {
this.student_id = student_id;
this.subject_id = subject_id;
}
// Getters
public Long getId() {
return id;
}
public Student getStudent_id() {
return student_id;
}
public Subject getSubject_id() {
return subject_id;
}
// Setters
public void setId(Long id) {
this.id = id;
}
public void setStudent_id(Student student) {
this.student_id = student;
}
public void setSubject_id(Subject subject) {
this.subject_id = subject;
}
}
The application perfectly creates the tables in the database and I can get and post in the student and subject tables. No problem with that. The pain comes with the controller for the joint table.
This is the controller for the student_subject joint table table
package com.rest.web.postgresapi.controllers;
import com.rest.web.postgresapi.data.models.StudentSubject;
import com.rest.web.postgresapi.repository.StudentSubjectRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
#RestController
public class StudentSubjectController {
#Autowired
private StudentSubjectRepository studentSubjectRepository;
#GetMapping("/enrollments")
public List<StudentSubject> getAllStudentsSubjects(){
return studentSubjectRepository.findAll();
}
#PostMapping("/enrollments/student/subject")
public StudentSubject createStudentSubject(#Valid #RequestBody StudentSubject studentSubject) {
return studentSubjectRepository.save(studentSubject);
}
}
There are two problems:
1 .- when I do the get from the student_subject table, It only retrieves the id of the row and the created_at and updated_at fields. No student_id nor subject_id.
response from get
2 .- when I do the post (from postman) to insert a row, I got the following error:
Detail: Failing row contains (4671, 2018-11-20 11:04:34.176, 2018-11-20 11:04:34.176, null, null).
I provide both student_id and subject_id, as you can see at this screenshot from postman, but the error clearly states both fields are null:
postman post
It seems that my definition of the table is somehow wrong. What am I missing in my code?
Spring MVC uses Jackson to serialize/deserialize Java objects to/from JSON.
If you annotate an attribute with #JSONIgnore then Jackson will ignore it.
This is the reason why you don't see the student_id field or the subject_id field in your JSON response of the GET method. Because Jackson is ignoring them when converts from object to json.
And this is the reason why your POST fails. Because Jackson is ignoring the received attributes. Jackson is creating an empty entity and JPA is saving the entity without student_id and subject_id.
Solved by replacing
#JsonIgnore
with
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
as indicated in this answer
I have two models:
Class One:
import javax.persistence.*;
import java.util.Set;
#Entity
public class One {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
#OneToMany(mappedBy = "one")
private Set<Many> manySet;
//Constructor, Getter and Setter
}
Class Many:
import javax.persistence.*;
#Entity
public class Many {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
#ManyToOne
#JoinColumn(name = "one_id")
private One one;
//Constructor, Getter and Setter
}
Repository:
import com.hotel.model.Many;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ManyRepository extends JpaRepository<Many, Long> {
}
Controller Class:
#RestController
#RequestMapping(value = "many")
public class ManyController {
#Autowired
private ManyRepository manyRepository;
#GetMapping
#ResponseBody
public List<Many> getAllMany() {
return manyRepository.findAll();
}
#PostMapping
#ResponseBody
public ResponseEntity createMany(#RequestBody Many many) {
return new ResponseEntity(manyRepository.save(many), HttpStatus.CREATED);
}
}
I created One record with id=1.
But when I create a Many record with JSON data:
{
"name": "Foo",
"one_id": 1
}
I received Many record with one_id is null
Can I using only one request to create new Many record and assign to One record has id = 1?
Do I have to use 2 request: create Many and assign to One?
You have to update your method like so
#PostMapping
#ResponseBody
public ResponseEntity createMany(#RequestBody ManyDTO many) {
One one = oneRepository(many.getOne_id()); //Get the parent Object
Many newMany = new Many(); //Create a new Many object
newMany.setName(many.getName());
newMany.setOne(one); // Set the parent relationship
...
}
Note: The above answer only explains the way to set the relationships of the entity. Proper service layer should be invoked actually.
You can persist 1 record in One and multiple record in Many tables only using a single request while your are using the below repository
import com.hotel.model.One;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OneRepository extends JpaRepository<One, Long> {
}
On the other hand you can persist 1 record in One and 1 record in Many tables using the JpaRepository given in your example.
Your question was not clear, whether you want to persist One/Many or retrieve One/Many.
I have got two entities with Many-to-one (One-to-many) relationship.
Class ParserEntity:
import javax.persistence.*;
#Entity
#Table(name = "parser", schema = "newsmonitoringdb")
public class ParserEntity {
public ParserEntity() {
}
public ParserEntity(String name, SourceTypesEntity type) {
this.name = name;
this.type = type;
}
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Basic
#Column(name = "name")
private String name;
#ManyToOne(optional = false)
#JoinColumn(name="type",referencedColumnName="id")
private SourceTypesEntity type;
...//getters, setters and other fields are here
}
}
Class ParserTypesEntity:
import javax.persistence.*;
import java.util.Collection;
import java.util.List;
/**
* Created by User on 17.08.2016.
*/
#Entity
#Table(name = "source_types", schema = "newsmonitoringdb")
public class ParserTypesEntity {
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Basic
#Column(name = "name")
private String name;
#OneToMany(mappedBy="type", fetch=FetchType.EAGER, targetEntity = ParserEntity.class)
private Collection<ParserEntity> siteParser;
...//getters and setters are here
}
}
They have a relation by field and everything seems ok.
However I would like a ParserTypeEntity class to save only names from ParserEntity class in a separate Collection field instead of ParserEntity list.
I would like them to be filled automatically when retrieving types from DB the same way it is done with ParserEntity objects now.
Is there any way to do that, or I have to change the relation to the unidirectional one, get all of the types and then get names for each type by its id?
They have a relation by field and everything seems ok. However I would
like a ParserTypeEntity class to save only names from ParserEntity
class in a separate Collection field instead of ParserEntity list.
I would like them to be filled automatically when retrieving types
from DB the same way it is done with ParserEntity objects now.
I don't think that you can use converter for relation with collections.
Besides, even if you could, I would not use it since it could make the code less natural and less readable. For an enum, it's useful and overall cosmetic but for entity collection, it looks like a hack since it breaks the ORM relationship between entities.
If you use JPA/EntityManager, I can propose a workaround : using a lifecycle event of entities. Otherwise, if you use Hibernate/SessionFactory, you should look listeners for Hibernate. It's the same principle.
The idea is keeping your actual mapping and adding a transient list with data you wish which would be populated in an automatic way when a ParserTypesEntity is loaded.
So, in ParserTypesEntity, you can add a hook method to do a processing when the entity is loaded. In this method, you could collect names of parserEntity collection and add it in a transient List<String>.
public class ParserTypesEntity {
...
#Transient
private List<String> parserTypeNames;
#PostLoad
public void postLoad() {
parserTypeNames = new ArrayList<>();
for (ParserEntity parserEntity : siteParser){
parserTypeNames.add(parserEntity.getName());
}
...
}
This question already has an answer here:
Many-to-Many Relationship (with properties) in Google App Engine for Java
(1 answer)
Closed 7 years ago.
I'm trying to persist an entity that has a Map of Objects to Enum into a Google App Engine datastore. The entity classes are annotated using JPA.
Event class
import com.google.appengine.datanucleus.annotations.Unowned;
import com.google.appengine.api.datastore.Key;
import java.util.Map;
import javax.persistence.*;
import lombok.Builder;
import lombok.Data;
#Entity
#Builder
public #Data class Event {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
// I want a map belonging to event in order to query a particular user whether he confirmed his participation in the event
// All addressees are initially present in this map with response set to UNDEFINED
// If user has received and read notification, than the response is updated to YES, NO, or MAYBE
#Unowned
#ElementCollection
#CollectionTable(name = "user_response")
#MapKeyJoinColumn(name = "user_id")
#Enumerated
#Column(name = "response")
private Map<User, Response> addressees;
}
Response class
public enum Response {
UNDEFINED, YES, NO, MAYBE
}
I haven't defined any references in User class to this map. It's a unidirectional relationship.
User class
#Entity
public #Data class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
}
The Event.addressees column seems pretty tricky. So I ran my test to check if everything was working correctly. Well, it was not. I got an exception when I tried to save an Event entity to the datastore:
java.lang.IllegalArgumentException: addressees: Response is not a supported property type.
at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedSingleValue(DataTypeUtils.java:235)
at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedValue(DataTypeUtils.java:199)
at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedValue(DataTypeUtils.java:173)
at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedValue(DataTypeUtils.java:148)
at com.google.appengine.api.datastore.PropertyContainer.setProperty(PropertyContainer.java:101)
According to DataNucleus Enum is a persistable data type by default. So I don't understand why I get the error message saying "Response is not a supported property type".
I suspected that the problem was with the User class. Maybe the association from Event to Users was not enough, and User should also have an association to Events. So I've added the events field to User as follows:
#Unowned
#ElementCollection
#CollectionTable(name = "user_event_responses")
#ManyToMany(mappedBy="addressees", targetEntity = Event.class)
#MapKeyJoinColumn
#Enumerated
#Column(name = "response")
private Map<Event, Response> events;
It didn't work anyway. Then I've read similar questions, and found no quick answer.
Please, show me an example of many-to-many relationship with an extra column in DataNucleus / JPA!
Problem of creating two classes that have a Many-To-Many relationship, but the relational join table has additional data, is a frequent problem.
Good examples on this topic I've found at WikiBooks - Java Persistence / Many-To-Many and in the article Mapping a Many-To-Many Join Table with extra column using JPA by Giovanni Gargiulo. References in the official documentation I've found much, much later: Unowned Entity Relationships in JDO and Unsupported Features of JPA 2.0 in AppEngine.
In this case the best solution is to create a class that models the join table.
So an EventUserResponse class would be created. It would have a Many-To-One to Event and User, and an attribute for the additional data. Event and User would have a One-To-Many to the EventUserResponse. Unfortunately I didn't managed how to map a composite primary key for this class. And DataNucleus Enhancer refused to enhance an entity class without primary key. So I've used a simple auto-generated ID.
The result should be like
Here are the sources:
EventUserAssociation class
#Entity
#Table(name = "event_user_response")
#NoArgsConstructor
#AllArgsConstructor
#Getter #Setter
#EqualsAndHashCode(callSuper = true, exclude = {"attendee", "event"})
public class EventUserAssociation extends AbstractEntity {
#Unowned
#ManyToOne
#PrimaryKeyJoinColumn(name = "eventId", referencedColumnName = "_id")
private Event event;
#Unowned
#ManyToOne
#PrimaryKeyJoinColumn(name = "attendeeId", referencedColumnName = "_id")
private User attendee;
#Enumerated
private Response response;
}
If Lombok annotations (#NoArgsConstructor for example) seem unfamiliar to you, you may want to take a look at the ProjectLombok. It does a great job to save us from a boilerplate code.
Event class
#Entity
#Builder
#EqualsAndHashCode(callSuper = false)
public #Data class Event extends AbstractEntity {
/* attributes are omitted */
// all addressees are initially present in this map with response set to UNDEFINED
// if user has received and read notification, than the response is updated to YES, NO, or MAYBE
#Singular
#Setter(AccessLevel.PRIVATE)
#OneToMany(mappedBy="event", cascade = CascadeType.ALL)
private List<EventUserAssociation> addressees = new ArrayList<>();
/**
* Add an addressee to the event.
* Create an association object for the relationship and set its data.
*
* #param addressee a user to whom this event notification is addressed
* #param response his response.
*/
public boolean addAddressee(User addressee, Response response) {
EventUserAssociation association = new EventUserAssociation(this, addressee, response);
// Add the association object to this event
return this.addressees.add(association) &&
// Also add the association object to the addressee.
addressee.getEvents().add(association);
}
public List<User> getAddressees() {
List<User> result = new ArrayList<>();
for (EventUserAssociation association : addressees)
result.add(association.getAttendee());
return result;
}
}
User class
#Entity
#NoArgsConstructor
#RequiredArgsConstructor
#Getter #Setter
public class User extends AbstractEntity {
/* non-significant attributes are omitted */
#Setter(AccessLevel.PRIVATE)
#Unowned
#OneToMany(mappedBy="attendee", cascade = CascadeType.ALL)
private List<EventUserAssociation> events = new ArrayList<>();
public static User find(String attribute, EntityManager em) {
/* implementation omitted */
}
}
AbstractEntity class
#MappedSuperclass
#NoArgsConstructor
#EqualsAndHashCode
public abstract class AbstractEntity {
#Id
#Column(name = "_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Getter
protected Key id;
}
EMFService class
public abstract class EMFService {
#Getter
private static final EntityManagerFactory emfInstance = Persistence.createEntityManagerFactory("transactions-optional");
}
Example of usage:
EntityManager em = EMFService.getFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
User fromContact = User.find(fromId, em);
Event event = Event.builder()
/* attributes initialization */
.build();
em.persist(event);
User toUser = User.find(toId, em);
event.addAddressee(toUser, Response.UNDEFINED);
tx.commit();
} finally {
if (tx.isActive()) tx.rollback();
em.close();
}
Cross-group transactions should be allowed for this to work (what if they aren't?). Add the following property to persistence.xml:
<property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />
At last, regarding the code in the question, it is not allowed to have a primary key with the name key in AppEngine.