Spring Data Rest testing with JPA Relationship Mapping : Odd behaviour with update - jpa

I followed this tutorial (https://spring.io/guides/tutorials/react-and-spring-data-rest/#react-and-spring-data-rest-part-5) to experiment Spring Data REST and I wanted to test the CRUD with TestRestTemplate.
Add (postForEntity) is ok.
Delete (delete) is ok.
Read (getForEntity) is ok.
Update (template.exchange(URL, HttpMethod.PUT, entity, String.class, ID)) only works when I don't have any relation with other entities... and I don't understand why.
Here's an example :
#Data
#Entity
public class Dojo {
private #Id #GeneratedValue Long id;
private String name;
private String location;
private Date created;
#OneToMany(mappedBy = "dojo")
#JsonIgnore
private List<Workshop> workshops;
private Dojo() {}
public Dojo(String name, String location) {
this.name = name;
this.location = location;
this.created = new Date();
this.workshops = new ArrayList<>();
}
//getters and setters ...
}
#Data
#Entity
public class Workshop {
private #Id #GeneratedValue Long id;
private String name;
#ManyToOne
private Dojo dojo;
private Workshop() {}
public Workshop(String name, Dojo dojo) {
this.name = name;
this.dojo = dojo;
}
}
So, I have a bidirectionnal 1:n relation between Dojo & Workshop. The #JsonIgnore annotation is here to avoid an infinite loop with the JSON Marshaller.
The repositories are standard
public interface WorkshopRepository extends CrudRepository<Workshop, Long> {}
Now my test : I want to update a workshop. Sounds good, doesn't work.
#Test
public void testUpdateWorkshop() throws Exception {
final String DOJO_NAME="My Dojo";
final String DOJO_LOCATION="Liege";
final String WORKSHOP_NAME="Stuff";
final String HOST_PORT="http://localhost:8080";
//creation of a dojo
final Dojo DOJO = dojoRep.save(new Dojo(DOJO_NAME,DOJO_LOCATION));
//creation of a workshop
Workshop workshop = workshopRep.save(new Workshop(WORKSHOP_NAME,DOJO));
String newValue = "After Test";
System.out.println("before update");
System.out.println(workshop.getName()+" == "+WORKSHOP_NAME);
Long oldID = workshop.getId();
//As you can see I didn't modify the workshop object
HttpEntity<Workshop> entity = new HttpEntity<Workshop>(workshop);
ResponseEntity<String> response = template.exchange(HOST_PORT+"/api/workshops/"+oldID, HttpMethod.PUT, entity, String.class, oldID);
assert response.getStatusCodeValue() == 200;
//re-Get the updated workshop
workshop = workshopRep.findOne(oldID);
System.out.println("after update");
System.out.println(workshop.getName()+" == "+WORKSHOP_NAME);
// as I didn't set the newValue, it must fail and workshop.getName() must stay equal to "Stuff".
Assert.assertEquals("Update does not work",newValue,workshop.getName());
}
I run mvn clean test and
before update
Stuff == Stuff
after update
My Dojo == Stuff
Failed tests:
WorkshopTests.testUpdateWorkshop:218 Update not work expected:<[After Test]> but was:<[My Dojo]>
So basically, I didn't change anything into my object but
Result code is 200.
It changed a property of my object.
The name was modified to take the dojo.name value !
Just ... Why ?
More information :
When I create a new workshop object with a new name (using the newValue ;-) ) and a new Dojo and try to update the existing workshop, the result is still the same. workshop.dojo unchanged and name copied from dojo.name. So basically, my update doesn't work.
I also try with mockMvc instead of TestRestTemplate like this.
mockMvc.perform(put(HOST_PORT+"/api/workshops/"+oldID)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(convertObjectToJsonBytes(workshop))
);
with the function
private byte[] convertObjectToJsonBytes(Object object) throws IOException {
ObjectMapper mapper = new ObjectMapper();
System.out.println("log my face ");
System.out.println(mapper.writeValueAsString(object));
return mapper.writeValueAsBytes(object);
}
And the log seems to rightly parse my object before update...
{"id":1,"name":"Stuff","dojo":{"id":1,"name":"My Dojo","location":"Liege","created":1500799092330}}
but still doesn't work :(
When I run the app (mvn spring-boot:run), a GET on localhost:8080/api/workshops/1 returns
{
"name" : "Stuff",
"_links" : {
"self" : {
"href" : "http://localhost-core:8080/api/workshops/1"
},
"workshop" : {
"href" : "http://localhost-core:8080/api/workshops/1"
},
"dojo" : {
"href" : "http://localhost-core:8080/api/workshops/1/dojo"
}
}
}
If I change the property name of my Dojo class by nameD and I update with a new name and a new Dojo (previously saved into DB), the name is updated but not the dojo.
To summarize my questions are :
Just ... why ?
What is the correct way to update an object like Workshop with a HTTP request ?
What is the correct way to test this update ?
Thanks to all and have a nice day ! :-)

I think it's because you are using bidirectional one-to-many association. In this case you have to provide linking/unlinking of entities by yourself. For example in the collection setter, like this:
#Data
#ToString(exclude = "slaves")
#Entity
public class Master {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy = "master", cascade = {PERSIST, MERGE})
private List<Slave> slaves;
public void setSlaves(List<Slave> slaves) {
// link new slaves to this master
slaves.forEach(slave -> slave.setMaster(this));
// unlink prev slaves
if (this.slaves != null) this.slaves.forEach(slave -> slave.setMaster(null));
this.slaves = slaves;
}
}
#Data
#Entity
public class Slave {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToOne
private Master master;
}
Then you can store Slave:
POST http://localhost:8080/api/slaves
{
"name": "slave1"
}
// the same for salve2, slave3, slave4
Store Master:
POST http://localhost:8080/api/masters
{
"name": "master1",
"slaves": [
"http://localhost:8080/api/slaves/1",
"http://localhost:8080/api/slaves/2"
]
}
Update Master:
PUT http://localhost:8080/api/masters/1
{
"name": "master1u",
"slaves": [
"http://localhost:8080/api/slaves/3",
"http://localhost:8080/api/slaves/4"
]
}
PUT http://localhost:8080/api/masters/2
{
"name": "master2"
}
Or update Slave:
PUT http://localhost:8080/api/slaves/1
{
"name": "slave1u",
"master": "http://localhost:8080/api/masters/2"
}
PUT http://localhost:8080/api/slaves/2
{
"name": "slave2u",
"master": "http://localhost:8080/api/masters/2"
}
See working example.
Additional info

Related

Spring-boot + Mongodb manual reference not working, can't find my mistake, querying Mongodb works fine

I'm new to Spring-boot and MongoDB. In MongoDB a manual reference between two collections works fine. The mapping in Spring-boot seems not to work. I really don't know what else to check.Below all the relevant details, sorry for the long question.
The reason not to use DBref is because I might need the projections.
The "players" collection has this schema(any other not allowed)
{"_id":{"$oid":"5f56021d61738cc35de79438"},
"name":"Romeo",
"entryDate":{"$date":"2020-08-23T22:00:00.000Z"}}`
The "games" collection has the following schema
{
"_id":{"$oid":"5f5614a361738cc35de7943b"},
"dices":{
"value1":1,
"value2":6
},
"gameScore":1,
"player_id":{"$oid":"5f56021d61738cc35de79438"}
}
The aggregation in MongoDB Compass
[{
$match: {
_id: ObjectId('5f56021d61738cc35de79438')
}
}, {
$lookup: {
from: 'games',
localField: '_id',
foreignField: 'player_id',
as: 'games'
}
}]
yields
In Spring-boot the POJOs are:
#Document(collection = "players")
public class Player {
#Id
private String id;
private String name;
private LocalDate entryDate= LocalDate.now();
private List<Game> game;
public Player(){};
public Player(String name) {
this.name = name;
}
//getters and setters for all properties, including game
}
#Document(collection = "games")
public class Game {
#Id
private String id;
private Dices dices;
private Integer gameScore;
#Field(value = "player_id")
private String playerId;
public Game(){};
public Game(Dices dices) {
this.dices = dices;
}
//getters and setters for all properties
}
public class Dices {
private int value1;
private int value2;
public Dices(){}
public Dices(int value1, int value2) {
this.value1 = value1;
this.value2 = value2;
}
//getters and setters for both properties
In Postman
GET findAll players shows:
[{"id":"5f56021d61738cc35de79438","name":"Romeo","entryDate":[2020,8,24],"game":null},{"id":"5f5602e361738cc35de79439","name":"Julieta","entryDate":[2020,8,24],"game":null},
....]
game is shown because I added also getters and setters for this property, just trying to find the way to properly mapping the games as manual references to players
GET findAll games:
[{"id":"5f5614a361738cc35de7943b","dices":{"value1":1,"value2":6},"gameScore":1,"playerId":"5f56021d61738cc35de79438"},
{"id":"5f5619f561738cc35de7943c","dices":{"value1":2,"value2":5},"gameScore":1,"playerId":"5f5602e361738cc35de79439"},
{"id":"5f561a5461738cc35de7943d","dices":{"value1":3,"value2":3},"gameScore":0,"playerId":"5f56021d61738cc35de79438"},
...]
GET lh:8080/players/5f56021d61738cc35de79438/games
yields an empty array, this is why I assume that the mapping between the collections in Spring-boot fails.
The GamesRepository
#Repository
public interface GameRepository extends MongoRepository<Game, String> {
List<Game> findAll();
List<Game> findGamesByPlayerId(String playerId);
}
The method in the service
#Override
public List<Game> findAllGamesByPlayerId(String playerId) {
Optional<Player> playerDB= playerRepository.findById(playerId);
if(playerDB.isPresent()) {
return gameRepository.findGamesByPlayerId(playerId);
}
else throw new ResourceNotFoundException("Player with id: "+playerId+" does not exist");
}
and the GameController
#GetMapping("/{ID}/games")
public ResponseEntity<List<Game>> getAllGamesByPlayerId (#PathVariable("ID") String playerId){
return ResponseEntity.ok()
.body(gameService.findAllGamesByPlayerId(playerId));
}
Tips are welcome!
Aggregations don't work with MongoRepostory unless you use #DBRef. But using #DBRef is not recommended. What you did in aggregation can be converted into Aggregation pipeline of Spring data.
For that you need to autowired the MongoTemplate
#Autowired
MongoTemplate mongoTemplate;
Then you can convert the aggregation you have written. I haven't tested it, since your aggregation is working, this should work.
public List<Object> test(ObjectId id){
Aggregation aggregation = Aggregation.newAggregation(
match(Criteria.where("_id").is(id)),
lookup("games","_id","player_id","games")
).withOptions(AggregationOptions.builder().allowDiskUse(Boolean.TRUE).build());
return mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Players.class), Object.class).getMappedResults();
}

how to save parent object containing child object using spring boot in #ManyToOne unidirectional mapping?

I am new in spring boot. I've two model classes Party(parent) and PartyCategory(child). PartyCategory stores data id, labelAr and labelEn successfully.
Now I am passing child id in json request and getting null values for labelAr and labelEn in json response as pasted below. Can someone please help what and doing wrong here.
I've pasted my code as well.
Json Request:
{
"name": "Party A",
"description": "Description of Party A",
"category": {
"id": 1
}
}
Json response;
{
"id": 6,
"name": "Party A",
"description": "Description of Party A",
"category": {
"id": 1,
"labelAr": null,
"labelEn": null
}
}
Party.java:
public class Party {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
private String description;
#ManyToOne(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
#JoinColumn(name = "category_id")
private PartyCategory category;
....setters and getters
}
PartyCategory.java:-
public class PartyCategory {
#Id
#GeneratedValue
private Integer id;
private String labelAr;
private String labelEn;
...setters and getters..
Repositories:
public interface PartyCategoryRepository extends JpaRepository<PartyCategory, Integer> {
}
public interface PartyRepository extends JpaRepository<Party, Integer> {
}
Services:
public class PartyServiceImpl {
#Autowired
PartyRepository partyRepository;
public Party saveParty(Party party) {
return partyRepository.save(party);
}
Controller:
#RestController
public class PartyController {
#Autowired
PartyServiceImpl partyServiceIml;
#PostMapping(value = "/party/save")
public Party saveParty(#RequestBody Party party ) {
Party returnedParty = partyServiceIml.saveParty(party);
return returnedParty;
}
}
The problem is that the category you are posting is not recognised as being an existing category.
You can then do something like the below. Firstly, Create a Jackson converter class to customise the Json deserialisation. I was not sure if these were Spring managed but they are so you can then inject the necessary repository.
import com.fasterxml.jackson.databind.util.StdConverter;
#Component
public class CategoryConverter extends StdConverter<Integer, PartyCategory> {
#Autowired
private PartyCategoryRepository repo;
#Override
public PartyCategory convert(Integer value) {
return repo.findById(value).get();
}
}
Then update your entity as follows so that the category Json property will be handled via the converter created above. Note that in reality I would use a Jackson mix-in to apply this custom deserializer as this would avoid 'polluting' the entity class with Json processing instructions. You can look up how to do that.
#Entity
public class Party {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
private String description;
#ManyToOne
#JoinColumn(name = "category_id")
#JsonDeserialize(converter = CategoryConverter.class) //in real life use a 'mix-in'
private PartyCategory category;
}
Then you can post JSON as below where we simply specify the id of an existing category:
{
"name": "Party A",
"description": "Description of Party A",
"category": 1
}
By enhancing this solution to use mix-ins as suggested then it is possible then to cleanly separate the view model from the entity model without having to create a DTO layer which will typically largely duplicate the entity model and which will required tedious mapping code to handle the conversions.
First of all, it's not a good practice to use the same entity for the database and also for the rest services. They should be separate entities, normally the entities for the rest services are called DTO (Data Access Objects).
Now, regarding your problem, it's normal what it's happening in your code, because you overwrite the PartyCategory labelAr and labelEn associated to the ID 1 with null values when you save your new Party, because you didn't provide any value for those two labels.
return partyRepository.save(party);
If you want to avoid this problem you have to retrieve the PartyCategory data first from the database, set to the Party entity and after that to save it to the database. Something like this:
public class PartyServiceImpl {
#Autowired
PartyRepository partyRepository;
#Autowired
PartyCategoryRepository partyCategoryRepository;
public Party saveParty(Party party) {
PartyCategory partyCategory = partyCategoryRepository.findById(party.getPartyCategory().getId());
party.setPartyCategory(partyCategory);
return partyRepository.save(party);
}

MongoDB with Spring Boot and document containing similar fields

I'm current trying to access an object in my MongoDB database.
My object is stored this format in db:
{
"_id" : ObjectId("some object id"), // mongodb gives this id
"my_id" : "Given id by myself",
"url" : "Some string data"
}
Myobj class:
#Document(collection = "MYOBJ")
public class Myobj {
#Id
private ObjectId _id;
private String my_id;
private String url;
// getters and setters and other methods
}
I want to fetch this object using my_id field. In my repository I have these:
public interface MyobjRepository extends MongoRepository<Myobj, String> {
Myobj findBy_id(ObjectId _id);
Myobj findByMy_id(String my_id);
}
But it fails to build, it gives me this error:
No property my found for type Myobj! Did you mean 'url'?
I suppose it cannot differantiate between my_id and _id. How can I solve this issue without changing my object in the database?
Without findByMy_id method it was working. That method causes compile error.
Problem is that you inside interface MyobjRepository set String as Id of that template MongoRepository<Myobj, String>, and you need to set ObjectId like MongoRepository<Myobj, ObjectId> because you said in your Myobj class that you will be use ObjectId _id for #Id
Am I right ?
#Document(collection = "MYOBJ")
public class Myobj {
private ObjectId _id;
#Id
private String my_id;
private String url;
}
public interface MyobjRepository extends MongoRepository<Myobj, String> {
Myobj findBy_id(ObjectId _id);
Myobj findByMy_id(String my_id);
}

REST Service - JSON mapping for dynamic parameters

Let us take the following JSON response which I want to return from my REST service,
{
"id" : 123,
"name" : "ABC",
}
For the above JSON response, I can create a POJO class like,
public class Student{
private long id;
private String name;
//getters and setters
}
So, I can write a GET service to return the Student object which will be then transformed as JSON.
#GET
#Produces(MediaType.APPLICATION_JSON)
public Response get(){
Student student = new Student();
student.setId(123);
student.setName("ABC");
return Response.ok(student).build();
}
It works fine. Now I want to introduce optional parameters to my JSON response as follows,
{
"id" : 123,
"name" : "ABC",
"params" : {"param1":"xxx","param2":342}
}
Here the params in the JSON response is an Object type and the attributes of that object are not fixed. It will vary for every request like sometime it can have 3 attributes and sometime it will have none. I don't know how to create my POJO class for this requirement. Can anybody suggest me a way how to do it?
Unless you don't need anything special, you should design it as like:
public class Student{
private long id;
private String name;
//getters and setters
private Map<String, String> parameters = new HashMap<>();
public void add(String key, String value) {
parameters.put(key, value);
}
public void addAll(Map<String, String> map) {
parameters.putAll(map);
}
}
If you need type safety then the design is little bit complicated a consider using something like:
class StudentParameters {
long param1;
String param2;
}
and Student:
public class Student{
private long id;
private String name;
//getters and setters
private StudentParameters studentParameters;
public setStudentParameters(final StudentParameters studentParameters) {
this.studentParameters = studentParameters;
}
}
Do not create complex hierarchies e.g Map<List<List>, List<List>> it will complicate whole structure.

How to use hidden field to store data model in wicket

I have a entity, name Product.It have two property is unit (byte) and unitName(String). unit property is mapped on database. Ex: 0:Kg ; 1:g;.... I want when input a valid unit, unit property is stored; unless, it save to unitName
Product
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "product_id")
private int productId;
#Column(name = "product_name")
private String productName;
#Column(name = "unit")
private Byte unit;
#Transient
private String unitName;
}
In unit text field, I use a UnitConvert
UnitConvert
public class UnitConverter implements IConverter<Byte> {
private static final long serialVersionUID = 4798262219257031818L;
public UnitConverter() {
}
#Override
public Byte convertToObject(String value, Locale locale) {
return Text.isEmpty(value) ? 0 : UtilCommon.getTaniCode(value);
}
#Override
public String convertToString(Byte value, Locale locale) {
return (value == null || value==0 ) ? "" : UtilCommon.getTaniName(value);
}
}
I only think about HiddenField to do that, but I don't know how to do that.
Someone know how to use or anything can help me. Thank you very much
So from what I understood you want to save the input of a Model to a different database property depending on certain checks before hand. You can do that in your Form.onSubmit() method.
A very simple implementation could look like this:
public ProductPanel(String id, final IModel<Object> productModel) {
super(id, productModel);
// we're putting the productModel into the constructor.
// Therefore it's guaranteed to be detached
// -> it's okay to have it with final modifier.
IModel<String> formModel = Model.of("");
Form<String> form = new Form<String>("form", formModel) {
#Override
protected void onSubmit() {
super.onSubmit();
String productName = getModelObject();
Object realProduct = productModel.getObject();
if (isAcceptableUnit(productName)) {
realProduct.setUnit(parseUnit(productName));
} else {
realProduct.setUnitName(productName);
}
layer.saveProduct();
}
};
add(form);
TextField<String> productName = new TextField<String>("textField", formModel);
form.add(productName);
}
private boolean isAcceptableUnit(String productName) {
// your logic to determine if it's okay to cast to byte here...
return true;
}
private byte parseUnit(String productName) {
// your logic to parse the String to byte here...
return 0;
}
Some additional comments since I'm uncertain if the code snippets you provided are just for simplicity or actually code pieces:
You should try to avoid declaring your db object Serializable. Should you use normal Model objects to save your DTOs wicket will actually serialize them and you won't be able to do anything with them (well with hibernate at least).
Database object should use LoadableDetachableModel and save the primary key to load the entity in the load() method of it.
This would enable you now to work directly on those objects by using CompoundPropertyModel etc (which has it's pros and cons which I will not explain in detail here).
Still in your case I would add an Model<String> to the form and let the server decide how the input should be handled and mapped to the actual domain object.