Spring Data Rest 2.1.0 Cannot POST or PUT Complex Resource - spring

EDIT: This appears to be happening with PUTs as well.
Using spring-data-rest-webmvc version 2.1.0.BUILD-SNAPSHOT I have found that I am unable to POST a resource with a relation pointing to an already existing resource. I have 2 such entities which require references to be instantiated and POSTing to either of their endpoints results in the behavior below.
POSTing a resource without required references works well.
I did a bit of digging and it appears that PersistentEntityResourceHandlerMethodArgumentResolver finds the MappingJackson2HttpMessageConverter just fine, but it ends up throwing an exception while checking whether the ObjectMapper can deserialize the type. The cause of the exception is a NullPointerException.
example POST w/ relations to /reservations:
{
"description" : "test_post",
"dateMade" : "2014-03-03T08:04:44.293-0600",
"timeLastChanged" : null,
"userLastChanged" : null,
"courseTypeId" : null,
"numCredits" : null,
"instructor" : null,
"numParticipants" : null,
"reservationType" : "MCU",
"status" : "REQUESTED",
"abstract" : null,
"requestor" : "http://localhost:8080/users/2",
"submitter" : "http://localhost:8080/users/2",
"conferences" : []
}
RESPONSE:
{
cause: null
message: "No suitable HttpMessageConverter found to read request body into object of type class domain.Reservation from request with content type of application/json!"
}
POST w/ no relations to /roomGroups:
{
"description" : "All Rooms",
"isOffNetwork" : false,
"roomGroupType" : "STANDARD"
}
RESPONSE:
201 Created
Is there something wrong about the JSON I am POSTing which is resulting in an NPE from the ObjectMapper? Is there a workaround of some kind? This was working for me in 2.0.0.RC1 using a slightly different scheme for including reference links in the JSON and since the version of the Jackson dependencies appears to have stayed the same I wonder what is causing this issue...
Thanks for any help!
UPDATE:
This issue now seems un-related to the associated entities...
I created a new #Entity ConnectionRequest as follows:
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "CONNECTION_REQUEST_ID")
private Long id;
#Column(name = "FROM_ENTITY_ID", nullable = false)
private Long fromId;
#Column(name = "TO_ENTITY_ID", nullable = false)
private Long toId;
#Convert(converter = EntityTypeConverter.class)
#Column(name = "FROM_ENTITY_TYPE_ID", nullable = false)
private EntityType fromType;
#Convert(converter = EntityTypeConverter.class)
#Column(name = "TO_ENTITY_TYPE_ID", nullable = false)
private EntityType toType;
#ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE})
#JoinColumn(name = "CONFERENCE_ID", nullable = false)
private Conference conference;
I can POST a new ConnectionRequest record with a Conference relation included in the json as such {"conference" : ".../conferences/1"}.
I am however still getting the same exception w/ this #Entity Reservation:
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "RESERVATION_ID")
private Long id;
#Column(name = "DESCRIPTION", length = 50, nullable = false)
private String description;
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "DATE_MADE", nullable = false)
private Date dateMade;
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "TIME_LAST_CHANGED")
private Date timeLastChanged;
#Column(name = "USER_LAST_CHANGED")
private Integer userLastChanged; // TODO why is this an int?
#Column(name = "ABSTRACT", length = 2000)
private String _abstract;
#Column(name = "COURSE_TYPE_ID")
private Integer courseTypeId;
#Column(name = "NUMBER_OF_CREDITS")
private Integer numCredits;
#Column(name = "INSTRUCTOR", length = 255)
private String instructor;
#Column(name = "NUMBER_OF_PARTICIPANTS")
private Integer numParticipants;
#Convert(converter = ReservationTypeConverter.class)
#Column(name = "RESERVATION_TYPE_ID", nullable = false)
private ReservationType reservationType;
#Convert(converter = StatusConverter.class)
#Column(name = "STATUS_ID")
private Status status;
#ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE})
#JoinColumn(name="REQUESTOR_USER_ID", nullable=false)
private User requestor;
#ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE})
#JoinColumn(name="SUBMITTER_USER_ID", nullable=false)
private User submitter;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "reservation", cascade = {CascadeType.REMOVE})
private Set<Conference> conferences = new HashSet<>();
I'm not sure what's special about this class that's causing things to go awry...

The issue was the following:
Both of the non-postable entities had a property called _abstract due to it being a reserved word in Java. I had named the getter and setter for this property getAbstract() and setAbstract() respectively.
Jackson appears to have been throwing a null pointer exception since the getter and setter did not match the property name as expected.
When I changed the property name to resvAbstract and updated the accessors to getResvAbstract() and setResvAbstract() everything came together and started working.
I'm still curious about the change that led to this issue showing up, but I'm glad it's working!

Related

#JoinColumn "occurs out of order" when upgrading to spring-boot-3 (Hibernate 6 )

I have the following usage in JoinColumns
#Entity
public class EntityOne{
private String action;
private String type;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumns({
#JoinColumn(name = "action", referencedColumnName = "action_name", updatable = false, insertable = false),
#JoinColumn(name = "type", referencedColumnName = "type_name", updatable = false, insertable = false)
})
private Entitytwo entitytwo;
}
And
#Entity
public class EntityTwo {
#Id
#Column(name = "type_name")
private String typeName;
#Id
#Column(name = "action_name")
private String actionName;
}
This setup causes hibernate error of
Referenced column '" + column.getName()
+ "' mapped by target property '" + property.getName()
+ "' occurs out of order in the list of '#JoinColumn's
If i change the order inside the #JoinColumns it seems to work, but can stop working at the next time the application starts.
The hibernate comments at the begining of the relevant code states:
// Now we need to line up the properties with the columns in the
// same order they were specified by the #JoinColumn annotations
// this is very tricky because a single property might span
// multiple columns.
// TODO: For now we only consider the first property that matched
// each column, but this means we will reject some mappings
// that could be made to work for a different choice of
// properties (it's also not very deterministic)
And on the relevant code itself:
// we have the first column of a new property
orderedProperties.add( property );
if ( property.getColumnSpan() > 1 ) {
if ( !property.getColumns().get(0).equals( column ) ) {
// the columns have to occur in the right order in the property
throw new AnnotationException("Referenced column '" + column.getName()
+ "' mapped by target property '" + property.getName()
+ "' occurs out of order in the list of '#JoinColumn's");
}
currentProperty = property;
lastPropertyColumnIndex = 1;
}
How should i set the #JoinColumn for it to consistently work?
If the action and type attributes of EntityOne are meant to refer to the corresponding attributes of EntityTwo, they are useless and misleading.
The attribute private Entitytwo entitytwo is enough to design the #ManytoOne relation.
Remove these two attributes and if you need to get the action and type value of the entityTwo linked to an entityOne, simply use entityOne.entitytwo.getAction() (or entityOne.entitytwo.getType()).
I just tried the code you posted in Hibernate 6.1, and I observed no error. Even after permuting various things, still no error. So then to make things harder, I added a third column to the FK and tried permuting things. Still no error.
I now have:
#Entity
public class EntityOne {
#Id #GeneratedValue
Long id;
String action;
String type;
int count;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumns({
#JoinColumn(name = "count", referencedColumnName = "count", updatable = false, insertable = false),
#JoinColumn(name = "action", referencedColumnName = "action_name", updatable = false, insertable = false),
#JoinColumn(name = "type", referencedColumnName = "type_name", updatable = false, insertable = false),
})
EntityTwo entitytwo;
}
#Entity
public class EntityTwo {
#Id
#Column(name = "type_name")
String typeName;
#Id
#Column(name = "count")
int count;
#Id
#Column(name = "action_name")
String actionName;
}
and the test code:
#DomainModel(annotatedClasses = {EntityOne.class, EntityTwo.class})
#SessionFactory
public class BugTest {
#Test
public void test(SessionFactoryScope scope) {
scope.inTransaction( session -> {
EntityOne entityOne = new EntityOne();
entityOne.action = "go";
entityOne.type = "thing";
EntityTwo entityTwo = new EntityTwo();
entityTwo.actionName = "go";
entityTwo.typeName = "thing";
entityOne.entitytwo = entityTwo;
session.persist( entityOne );
} );
}
}
Perhaps there's something you're not telling us? Like, for example, something to do with the #Id of EntityOne which is missing in your original posted code?
Just in case, also tried this variation:
#Entity
public class EntityOne {
#Id
String action;
#Id
String type;
#Id
int count;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumns({
#JoinColumn(name = "action", referencedColumnName = "action_name", updatable = false, insertable = false),
#JoinColumn(name = "count", referencedColumnName = "count", updatable = false, insertable = false),
#JoinColumn(name = "type", referencedColumnName = "type_name", updatable = false, insertable = false),
})
EntityTwo entitytwo;
}
But still no error.

JPA Hibernate Problem for One to One Relationship with Embedded ID

I am struggling with the following problem that I've been trying to solve. After checking solutions on StackOverflow and articles on Baeldung I still get different JPA errors when trying to map the following ONE-TO-ONE relationship between 2 Oracle SQL tables with composite PK in a SpringBoot application:
MASTER
ID
VERSION
1
2022.1
Constraint:
PK_MASTER PRIMARY KEY(ID, VERSION)
MASTER_DETAILS
MASTER_ID
VERSION
DETAILS
1
2022.1
details
Constraint:
PK_MASTER_DETAILS PRIMARY KEY(MASTER_ID, VERSION)
FK_MASTER_DETAILS FOREIGN KEY(MASTER_ID, VERSION) REFERENCES MASTER(ID, VERSION)
After some failures in trying to map it using the #OneToOne JPA annotation with both classes having #EmbeddedId set on the composite PK, I also installed JPA Buddy to check how it will be generated and that resulted in the following 4 classes:
Master.java
#Getter
#Setter
#Entity
#Table(name = "master")
public class Master {
#EmbeddedId
private MasterId id;
#OneToOne(mappedBy = "master")
private MasterDetails masterDetails;
}
MasterId.java
#Getter
#Setter
#Embeddable
public class MasterId implements Serializable {
private static final long serialVersionUID = 8254837075462858051L;
#Column(name = "id", nullable = false)
private BigDecimal id;
#Lob
#Column(name = "version", nullable = false)
private String version;
}
MasterDetails.java
#Getter
#Setter
#Entity
#Table(name = "master_details")
public class MasterDetails {
#EmbeddedId
private MasterDetailsId id;
#MapsId
#OneToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumns({
#JoinColumn(name = "master_id", referencedColumnName = "id", nullable = false),
#JoinColumn(name = "version", referencedColumnName = "version", nullable = false)
})
private Master master;
#Lob
#Column(name = "details", nullable = false)
private String details;
}
MasterDetailsId.java
#Getter
#Setter
#Embeddable
public class MasterDetailsId implements Serializable {
private static final long serialVersionUID = -8375336118866998644L;
#Column(name = "master_id", nullable = false)
private BigDecimal masterId;
#Lob
#Column(name = "version", nullable = false)
private String version;
}
When running the SpringBoot application with this JPA structure the run time error received is:
org.hibernate.PropertyNotFoundException: Could not locate field [id] on class [org.project.packages.MasterDetails]
After removing the #MapsId that cause this error the application starts but when trying to insert data in the tables I get the following error:
org.hibernate.id.IdentifierGenerationException: null id generated for:class org.project.packages.MasterDetails
Checking in the H2 test database I noticed that the FK on the Master_Details table was not present, but only the PK was set.
I would appreciate any help in pointing out how this problem can be solved: other annotations required (Cascade/FetchType) or in case there are any changes to be made to the database level (I also tried adding a separate identifier column in the Master_Details table defined as PK and only keep the FK to the Master table). Thanks in advance!
After many tries, I figured out to solve the issue.
I had to use a common key between the two entities and also FetchType.LAZY.
MasterDetails.class
public class MasterDetails {
#EmbeddedId
#AttributeOverrides({
#AttributeOverride(name="ID", column=#Column(name="MASTER_ID"))
})
private MasterId id;
#OneToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumns({
#JoinColumn(name = "master_id", referencedColumnName = "id", nullable = false),
#JoinColumn(name = "version", referencedColumnName = "version", nullable = false)
})
private Master master;
#Lob
#Column(name = "guidance", nullable = false)
private String guidance;
}
Master.class
public class MasterSheet {
#EmbeddedId
private MasterId id;
#OneToOne(mappedBy = "master", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private MasterDetails masterDetails;
}

Spring data rest ManyToMany mapping PUT/update operation is not replacing the nested object

I started to learn spring data rest. I'm doing PUT operation and it's not working for the nested objects for ManyToMany relationship, whereas it works fine for OneToMany relation.
Entities structures:
#Table(name="CONFIG_DTLS",schema = "app_txn")
#Entity
public class Config {
#Id
#GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
#GeneratedValue(generator = "UUIDGenerator")
#Column(name = "ID", updatable = false, nullable = false)
private UUID id;
#Column(name = "NAME", nullable = false, length = 75)
private String name;
/*Unable to replace the data in the MBR_CONFIG_MAPPING table in the put operation.
When the control comes to #HandleBeforeSave annotated method in PUT operation,
the request data contains the existing Member info instead of the one which i'm passing in the PUT request body */
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE},fetch = FetchType.EAGER)
#JoinTable(schema = "app_txn", name = "MBR_CONFIG_MAPPING",
joinColumns ={#JoinColumn(name="CONFIG_ID",referencedColumnName = "ID")},
inverseJoinColumns = {#JoinColumn(name="MBR_ID",referencedColumnName = "ID")}
)
private Set<Member> members;
//able to replace the notifications completely in PUT operation
#OneToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinColumn(name = "CONFIG_ID",referencedColumnName = "ID")
private Set<Notification> notifications;
}
Member.java
#Table(name="MBR_DTLS",schema = "app_txn")
#Entity
public class Member {
#Id
#GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
#GeneratedValue(generator = "UUIDGenerator")
#Column(name = "ID", updatable = false, nullable = false)
private UUID id;
#Column(name = "OTHER_MBR_DATA", updatable = false)
private String otherMbrData;
}
Notification.java
#Table(name="NOTIFICATIONS",schema = "app_txn")
#Entity
public class Notification {
#Id
#GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
#GeneratedValue(generator = "UUIDGenerator")
#Column(name = "ID", updatable = false, nullable = false)
private UUID id;
#Column(name="LEVEL")
private String level;
#Column(name="EMAIL")
private String email;
}
Interfaces:
#RepositoryRestResource(collectionResourceRel = "configs", path="configs")
public interface ConfigRepo extends PagingAndSortingRepository<Config,UUID> {
}
#RepositoryRestResource(exported=false) // don't want to users to manipulate it directly.
public interface MemberRepo extends PagingAndSortingRepository<Member,Object> {
}
Here I don't want to add or modify anything in the MBR_DTLS table as it is loaded by another backend process. I want to update only the mapping details MBR_CONFIG_MAPPING table whenever user does the PUT/update operation. POST/create operation is working fine. Please share your thoughts on how to fix this and if you have any questions add it in the comment section.
PS: I referred some links online but that does not help much - Spring Data REST - PUT request does not work properly since v.2.5.7

Spring Data JPA update a Row without getting the row ById

I want to update the table using spring-jpa
This is my Entity Class
public class RewardEntity {
#Id
#Column(name = "reward_id", columnDefinition = "bigserial")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rewardId;
#Column(name = "reward_title", nullable = false)
private String rewardTitle;
#Column(name = "reward_text")
private String rewardText;
#Column(name = "reward_type", nullable = false)
private String rewardType;
#Column(name = "reward_for_code", nullable = false)
private String rewardFor;
#Column(name = "reward_from_date", nullable = false)
private OffsetDateTime rewardFromDate;
#Column(name = "reward_to_date", nullable = false)
private OffsetDateTime rewardToDate;
#Column(name = "is_display_on", nullable = false)
private Boolean isDisplayOn;
#Column(name = "created_id", length = 50, nullable = false)
private String createdId;
#Column(name = "updated_id", length = 50)
private String updatedId;
#Column(name = "created_date", columnDefinition = "timestamptz", nullable = false)
private OffsetDateTime createdDate;
#Column(name = "last_modified_date", columnDefinition = "timestamptz")
private OffsetDateTime lastModifiedDate;
}
I have a PutMapping Spring boot API that gets below Json Input
{
"rewardId": 53,
"rewardTitle": "Reward is Allocated",
"rewardText": "Reward allocated for your recent purchase with our shop located at ABC-Mall",
"rewardType": "Informational",
"rewardFor": "Customer",
"rewardFromDate": "2019-04-12T00:00:00+05:30",
"rewardToDate": "2019-04-15T00:00:00+05:30",
"isDisplayOn": false
}
My Controller takes Principal object for both creation and updating the rewards table
#PutMapping
public ResponseEntity<RewardsResponse> updateRewards(Principal updatedPrincipal,
#RequestBody RewardUpdateRequest RewardUpdateRequest) {
But I won't send my createdId or updatedId from my Angular-UI.. So when i try to insert the updated-entity in to the table, using the below service-layer code
public RewardEntity updateReward(Principal principal, rewardEntity rewardEntity) {
String updatedId = null != principal ? principal.getName() : "defaultUpdatedId";
rewardEntity.setUpdatedCdsId(updatedId);
rewardEntity.setLastModifiedDate(OffsetDateTime.now());
return rewardRepository.save(rewardEntity);
}
I get the below error
could not execute statement; SQL [n/a]; constraint [created_id]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
My assumption is that RewardEntity gets updated in the same row by mapping the ID that we pass and update only the fields that i set and do not touch rest of the fields ...
Should i first get my RewardEntity object from the DB based on the ID and then update on top of it ?? This makes the code connect DB twice for every update.
Request your inputs please
I would first get reference object using updatedId
RewardEntity rewardEntity = rewardRepository.getOne(updatedId )
update this object as per your requirement
rewardEntity.setLastModifiedDate(OffsetDateTime.now());
and finally use save to update this.
return rewardRepository.save(rewardEntity);
getOne() returns a reference to the entity and internally invokes EntityManager.getReference() method. It will always return a proxy without hitting the database (lazily fetched).

Repository GET returns JSON which contains both a content object and content array

I have an entity with a ManyToOne relationship, when I use restTemplate.getForEntity(), the nested entity has its values wrapped in a content field at the same time there is a content array added overriding it
#ManyToOne(fetch = FetchType.LAZY)
#RestResource(exported = false)
#JoinColumn(name = "namespace", nullable = false)
private Namespace namespace;
A GET on the entity with this relation returns the following output body
{
"id" : "some_containing_id",
"alertDefinition" : null,
"namespace" : {
"content" : {
"id" : "some_namespace_id",
"emailSenderName" : "some sender",
"emailSenderId" : "foo#bar.com",
"createdAt" : "2018-07-19T05:24:04.473Z",
"updatedAt" : "2018-07-19T05:24:04.473Z"
},
"content" : [ ],
"links" : [ ]
},
...
So the namespace is being serialized containing 2 content fields with the array replacing the content object containing the values
SpringBoot 2.0.3.RELEASE
Namespace.java
#Entity
#Table(name = "namespace"
, schema = "alert_notification"
)
public class Namespace implements java.io.Serializable {
public transient static final String
EMAIL_SENDER_NAME_DEFAULT = "some sender";
public transient static final String
EMAIL_SENDER_ID_DEFAULT = "foo#vbar.com";
#Id
#Column(name = "id", unique = true, nullable = false)
private String id;
#Builder.Default
#Column(name = "email_sender_name")
private String emailSenderName = EMAIL_SENDER_NAME_DEFAULT;
#Builder.Default
#Column(name = "email_sender_id")
private String emailSenderId = EMAIL_SENDER_ID_DEFAULT;
#CreationTimestamp
#Column(name = "created_at", nullable = false, updatable = false)
private OffsetDateTime createdAt;
#UpdateTimestamp
#Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
}
For some reason the issue is caused by the Hibernate5 module included in com.fasterxml.jackson.datatype:jackson-datatype-hibernate5 which was recently introduced.
Interestingly enough the module doesn't even need to be enabled, having the dependency on the classpath alone will cause the problem.

Resources