I wish to know how to delete a many-to-many association via a REST call. I am able to create records, and associated them, but do not understand how to delete.
I have a Spring Boot project where i'm using REST and HATEOAS to by pass Services and Controllers and expose my Repository directly.
I have a User Model/Domain class
#Entity
#Table(name = "usr")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Version
private long version = 0;
#Id
#GeneratedValue(generator="optimized-sequence")
private Long id;
#Column(nullable = false, unique = true, length = 500)
#Size(max = 500)
private String userName;
#Column(nullable = false, length = 500)
#Size(max = 500)
private String firstName;
#Column(nullable = false, length = 500)
#Size(max = 500)
private String lastName;
#ManyToMany( fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable( name="user_role",
joinColumns={ #JoinColumn( name = "user_id",
nullable = false
)
},
inverseJoinColumns={ #JoinColumn( name="role_id",
nullable=false
)
}
)
private Set<Role> roles = new HashSet<Role>(0);
...Getters/Setters Below...
As as you can see, I have a roles member that is Many-To-Many association with Role class, of which the code is such:
#Entity
public class Role {
#Id
#GeneratedValue(generator="optimized-sequence")
private Long id;
#Column(nullable = false)
private String name;
#Column(nullable = false)
private String description;
...Getters/Setters Below...
My repositories look like so:
UserRepository
public interface UserRepository extends
JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
List<User> findByUserName(String username);
}
RoleRepository
public interface RoleRepository
extends JpaRepository<Role, Long> {
}
Now, all is well. When I access the project root from a browser, I get the repository index/directory in JSON+HAL format. Wonderful.
(Note I'm remove the http:// part from the test below because StackOverflow is counting it towards my links quota)
I, using WizTools REST Client, HTTP.POST to the Role ( localhost:8080/resttest/roles ) repository and create a new Role. Success, Role ID #4 created.
Then I POST to the User repository to create a User ( localhost:8080/resttest/users ). Success, User ID #7 created.
Then I PUT to the User repository to create an association with the role:
PUT localhost:8080/resttest/users/7/roles
Content-type: uri-list
Body: localhost:8080/resttest/roles/4
Great! Association made. User 9 is now associated with Role 4.
Now I can't for the life of me figure out how to DELETE this association.
I'm sending an HTTP DELETE instead of PUT with the same command as above.
DELETE localhost:8080/resttest/users/7/roles
Content-type: uri-list
Body: localhost:8080/resttest/roles/4
I get back: HTTP/1.1 405 Method Not Allowed
{
"timestamp":1424827169981,
"status":405,
"error":"Method Not Allowed",
"exception": "org.springframework.web.HttpRequestMethodNotSupportedException",
"message":"Request method 'POST' not supported"
}
Although creating a PUT request with remaining elements does the trick, DELETE is an accepted command to delete an association resource and is in most cases easier to use.
As for your example, this should work:
DELETE localhost:8080/resttest/users/7/roles/4
On a separate note, when creating the association, it is expected to have URIs in the payload. You shouldn't need to write the whole URL in the body, this should be enough:
PUT localhost:8080/resttest/users/7/roles
Content-type: uri-list
Body: /roles/4
Hope this helps.
From the docs:
DELETE
...
405 Method Not Allowed - if the association is non-optional
PUT means that you replace the whole roles Set. So to remove a single link you PUT all remaining links. If you only have a single link and want to remove it you would PUT an empty collection:
PUT localhost:8080/resttest/users/7/roles
Content-type: uri-list
Body:
BTW: You won't send a body with a DELETE request. It doesn't make sense.
EDIT
See also this answer from the developer of Spring HATEOAS.
Related
My User class looks like this :
#Data
#Entity
public class User {
#Id
Long userID;
#ManyToMany(mappedBy = "admins")
private List<ClassRoom> classRooms = new ArrayList<>();
}
And my ClassRoom class like this :
#Data
#Entity
public class ClassRoom {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long classRoomID;
#ManyToMany
#JoinTable(name ="classroom_user",
joinColumns = #JoinColumn(name = "classroom_id"),
inverseJoinColumns = #JoinColumn(name = "user_id"))
private List<User> admins = new ArrayList<>();
}
And in my UserController class, I have :
#PostMapping("user/{id}/c")
User addClassRoom(#PathVariable Long id,#RequestBody ClassRoom newClassRoom)
{
logger.debug(repository.findById(id));
return repository.findById(id)
.map(user -> {
user.getClassRooms().add(newClassRoom);
user.setClassRooms(user.getClassRooms());
return repository.save(user);
})
.orElseGet(() -> {
return null;
});
}
And I POST and empty JSON ({}) and I see no change in my users. The Classroom or an empty Classroom doesn't get added in the User.
What is the problem here? How can I resolve this ?
user.getClassRooms().add(newClassRoom); is suffice, user.setClassRooms(user.getClassRooms()); not required.
You will have to perform cascade save operation.List all cascade types explicitly and don't use mappedBy, instead use joincolumns annotation.
Can you paste the logs, please? Is Hibernate doing any insert into your table? Has the database schema been created in the DB correctly? One thing I recommend you to do is to add a custom table name on the top of your User class, using annotations like so: #Table(name = "users"). In most SQL dialects user is a reserved keyword, hence it is recommended to always annotate User class a bit differently, so that Hibernate won't have any problems to create a table for that entity.
IMO you must find classRoom by its id from repository, if it's new, you must create a new entity and save it first. Then assign it to user and save it.
The object you receive from the post method was not created by the entity manager.
After using user.getClassRooms().add(newClassRoom);
We must use userRepository.save(user);
I'd like to find all Offer documents by Offer.ProductProperties.brand:
#Document(collection = "offers")
public class Offer {
#Id
private String id;
#NotNull
#DBRef
private ProductProperties properties;
ProductProperties:
#Document(collection = "product_properties")
public class ProductProperties {
#Id
private String id;
#NotNull
#NotEmpty
private String brand;
Service:
Flux<ProductProperties> all = productPropertiesRepository.findAllByBrand(brand);
List<String> productPropIds = all.toStream()
.map(ProductProperties::getId)
.collect(Collectors.toList());
Flux<Offer> byProperties = offerRepository.findAllByProperties_Id(productPropIds);
But unfortunately byProperties is empty. Why?
My repository:
public interface OfferRepository extends ReactiveMongoRepository<Offer, String> {
Flux<Offer> findAllByProperties_Id(List<String> productPropertiesIds);
}
How to find all Offers by ProductProperties.brand?
Thanks!
After reading some documentation found out that You cannot query with #DBRef. Hence the message
Invalid path reference properties.brand! Associations can only be
pointed to directly or via their id property
If you remove DBRef from the field, you should be able to query by findAllByProperties_BrandAndProperties_Capacity.
So the only ways is how you are doing. i.e. Fetch id's and query by id.
As I said in the comment, the reason it is not working is because return type of findAllByProperties_Id is a Flux. So unless u execute a terminal operation, you wont have any result. Try
byProperties.collectList().block()
I am new to spring boot, I have used it to implement a rest API. I have a self referencing table where each item has a parent -tree structure- . I have used ManyToOne to implement this and i get a json object which holds the parent. If i get all the items the speed is very slow since there are a huge network latency and processing because my tree can hold up to 10000 item.
How can I represent this using links, i.e the the json object contains a link to parent and array of links to children. I have read that DTO can be used to implement this but I did not find a full details.
part of my code
#Entity
#Table(name = "Item", schema = "dbo")
#Getter
#Setter
public class Item {
#Id
#GeneratedValue
#Column()
private Integer ItemID;
#Column()
private String Project;
#Column()
private String Name;
#Column()
private Integer Version;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ParentItemID", insertable = false, updatable = false)
private Item parentItem;
My controller is strait forward
#Autowired
ItemService ItemService;
#RequestMapping ("/items")
public Iterable<Item> items(#RequestParam(value = "page", defaultValue = "0") Integer page,
#RequestParam(value = "size", defaultValue = "20") Integer size, Authentication auth) {
return ItemService.findPaginated(page, size);
}
I would be glad if i get more explanation on how to use DTOs , or another design to get links instead of full objects.
Easiest thing you can do is, return parent and children ids instead of Item objects.
#Entity
#Table(name = "Item", schema = "dbo")
public class Item {
.
.
private Integer ItemID;
private Integer parentItem;
.
.
Have another endpoint in your controller like /items/{itemId}
#GetMapping("/items/{itemId}")
public Item item(#PathVariable itemId, Authentication auth) {
return ItemService.findById(itemId);
Let your consumer grab the parent/children Ids and make separate REST calls
Alternatively, you can use Spring-HATEOAS
The Spring HATEOAS project is a library of APIs that we can use to
easily create REST representations that follow the principle of
HATEOAS (Hypertext as the Engine of Application State).
Generally speaking, the principle implies that the API should guide
the client through the through the application by returning relevant
information about the next potential steps, along with each response.
More Information Here
I get an Post request that would give me a List<PersonApi> Objects
class PersonApi {
private String name;
private String age;
private String pincode ;
}
And I have an Entity Object named Person
#Entity
#Table(name = "person_master")
public class Person{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#Column(name = "name")
String name;
#Column(name = "age")
String age;
#Column(name = "pincode ")
String pincode ;
}
My record from Post request would look something like this (pseudocode representation of the data below)
[
"Arun","33","09876gh"
"James","34","8765468"
]
I need to do a bulk-validation using Spring JPA.. Give the List<PersonApi> and get a True or False based on the condition that all the entries in the PersonApi objects list should be there in the database.
How to do this ?
The selected answer is not a right one. (not always right)
You are selecting the whole database to check for existence. Unless your use case is very special, i.e. table is very small, this will kill the performance.
The proper way may start from issuing repository.existsById(id) for each Person, if you never delete the persons, you can even apply some caching on top of it.
exists
Pseudo Code:
List<PersonApi> personsApiList = ...; //from request
List<Person> result = personRepository.findAll();
in your service class you can access your repository to fetch all database entities and check if your list of personapi's is completeley available.
boolean allEntriesExist = result.stream().allMatch(person -> personsApiList.contains(createPersonApiFromPerson(person)));
public PersonApi createPersonApiFromPerson(Person person){
return new PersonApi(person.getName(), person.getAge(), person.getPincode());
}
I am trying to have a list in a model using #DBRef but I can't get it to work.
This is my User model:
#Data
#Document
public class User {
#Id
#JsonSerialize(using = ToStringSerializer.class)
private ObjectId id;
#Indexed(unique = true)
#NotBlank
private String email;
#NotBlank
private String name;
#NotBlank
private String password;
#DBRef
private List<Server> servers;
}
Server model:
#Data
#Document
public class Server {
#Id
#JsonSerialize(using = ToStringSerializer.class)
private ObjectId id;
#NotBlank
private String name;
#NotBlank
private String host;
}
The structure is very simple, every user can have multiple servers. But when I add servers to the user the server is created, but the servers array contains one null entry("servers" : [ null ]). So the server isn't added to the user. This is how I create a server and add it to an user:
#PostMapping
public Mono create(#Valid #RequestBody Server server, Mono<Authentication> authentication) {
return this.serverRepository.save(server).then(authentication.flatMap(value -> {
User user = (User) value.getDetails();
user.getServers().add(server);
return userRepository.save(user);
})).map(value -> server);
}
So I simply create and save a server, add the server the user and then save the user. But it doesn't work. I keep having an array with one null entry.
I've seen this page: http://www.baeldung.com/cascading-with-dbref-and-lifecycle-events-in-spring-data-mongodb. But it is for saving the child document, not for linking it. Also it is for a single document, not for an array or list.
Why is my list not being saved correctly?
All my libraries are coming from spring boot version 2.0.0.M6.
UPDATE
When removing #DBRef from the user's servers property the servers are getting saved, but they of course get double created, in the server collection and in every user.servers. So the error has something to do with references.
After some googling I found the answer...
https://jira.spring.io/browse/DATAMONGO-1583
https://jira.spring.io/browse/DATAMONGO-1584
Reactive mongo doesn't support this.
Actually there is a way to resolve DbRefs without to using the blocking driver. Yes - the references are resolved in a blocking fashion, but does not require a second connection. In order to achieve this we have to write our own DbRefResolver: NbDbRefResolver.java. In the provided resolver there is a flag: RESOLVE_DB_REFS_BY_ID_ONLY. If is switched on will not going to resolve the DbRefs from the database, but instead will resolve them to fake objects with id only. It is up to implementation to fill the references later in non-blocking fashion.
If the flag RESOLVE_DB_REFS_BY_ID_ONLY is set to false it will eagerly resolve the references by using the non-blocking driver, but will block the execution until the references are resolved.
Here is how to register the DbRefResolver in the app: DbConfig.kt
Files attached are provided here: https://jira.spring.io/browse/DATAMONGO-1584
Me did it like that for roles :
#Unwrapped(onEmpty = Unwrapped.OnEmpty.USE_NULL)
private Collection<Role> roles;
you can check the doc (2021) here : https://spring.io/blog/2021/04/20/what-s-new-in-spring-data-2021-0