HATEOAS RESTFull Entity relation broken - spring

I have a very basic system using Spring with HATEOAS, and I found a problem. I have two very basic entities a car, and a person. Getters and setters avoided to make the question more readable.
#Entity
public class Car implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long carId;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="personId")
private Person owner;
private String color;
private String brand;
}
#Entity
public class Person implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long personId;
private String firstName;
private String lastName;
#OneToMany(mappedBy="owner")
private List<Car> cars;
}
This are my repositories:
#RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
List<Person> findByLastName(#Param("name") String name);
}
#RepositoryRestResource(collectionResourceRel = "cars", path = "cars")
public interface CarRepository extends PagingAndSortingRepository<Car, Long> {
List<Person> findByBrand(#Param("brand") String name);
}
I can create and query them, but some reference links are broken. For example, a couple of POSTs successfully creates two related entities:
http://localhost:8080/people
{ "firstName" : "Frodo", "lastName" : "Baggins"}
http://localhost:8080/cars
{ "color":"black","brand":"volvo", "owner":"http://localhost:8080/people/1"}
This are the GET reply on them:
http://localhost:8080/cars/2
{
color: "black2",
brand: "volvo2",
_links: {
self: {
href: "http://localhost:8080/cars/2"
},
owner: {
href: "http://localhost:8080/cars/2/owner"
}
}
}
http://localhost:8080/people/1
{
firstName: "Frodo",
lastName: "Baggins",
_links: {
self: {
href: "http://localhost:8080/people/1"
},
cars: {
href: "http://localhost:8080/people/1/cars"
}
}
}
But I don't know why the owner has this URL on the car:
http://localhost:8080/cars/2/owner
which actually doesn't work.
Any help on that?

It's there because that's what HATEOAS is all about, representing entity/resource relationships with links.
I'm not sure why it doesn't work. I'd guess it probably doesn't work because the owner isn't eagerly fetched when retrieving a car resource.
You can customize which links are generated by following the steps at https://stackoverflow.com/a/24660635/442773

Related

JPA Repository many to one

I have Student entity and Course entity. This is #ManyToOne relationship i.e. Student may attend only one course at a time, but courses may have multiple students.
#Entity
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String studentId;
private String firstName;
private String secondName;
#ManyToOne
#JoinColumn(name = "course_id")
//#JsonIgnore
private Course course;
#Entity
public class Course {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String courseName;
#OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "course", orphanRemoval = true, targetEntity = Student.class)
private List<Student> students = new ArrayList<>();
I post my data with the following json:
{
"id": 1,
"courseName": "course134",
"students" : [
{
"id" : 1,
"studentId": "123",
"firstName": "John1",
"secondName": "Name1"
},
{
"id" : 2,
"studentId": "1234567",
"firstName": "John2",
"secondName": "Name2"
}
then, as I get courses I receive:
{
"id": 1,
"courseName": "course134",
"students": []
}
How to list Students attending specific course?
I made up a Query in StudentRepository
#Query("SELECT s from Student s where s.id = :courseName")
Optional<Student> getStudentByCourseName(String courseName);
Still not working.
this is my Repository code:
#Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
Optional<Course> findCourseByCourseName(String courseName);
#Query("SELECT c.students FROM Course c WHERE c.courseName = :courseName")
Optional<Student> getStudentsByCourseName(String courseName);
}
this is my Service method
public Optional<Student> findStudentByCourse(String courseName){
return courseRepository.getStudentsByCourseName(courseName);
}
and finally my Controller:
#GetMapping("/student/course/{courseName}")
public ResponseEntity<Student> findCoursesWithStudentId(#PathVariable String courseName) {
Optional<Student> byCourseName = studentService.findStudentByCourse(courseName);
if (byCourseName.isPresent()) {
return ResponseEntity.ok(byCourseName.get());
} else {
return ResponseEntity.notFound().build();
}
}
You should query the Course table, not the Student table. Also, the query will return the list, not just one entity, so change your method's return type also...
#Query("SELECT c.students FROM Course c WHERE c.courseName = :courseName")
List<Student> getStudentsByCourseName(String courseName) {}
edit
You can always do it like so:
Excute the simple method:
Course findByCourseName(String courseName) {}
and then just get its Students by a simple:
course.getStudents();

Hibernate: How to display data from multiple table

I am new in spring/hibernate technologies, I have tried to find an information about it, but failed, so if you can help I will be so thankful!
I need to display a JSON response in browser of multiple tables, one of the table has primary key for another one.
My entities:
#Entity
#Table
#ToString
public class Book {
#Id
#GeneratedValue(strategy = AUTO)
#JsonView(Views.IdName.class)
private Long book_id;
#JsonView(Views.IdName.class)
private String name;
#Column(length = 1000000)
private String text;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="author_id")
#JsonView(Views.IdName.class)
private Author author;
// ....get/set methods
Another one:
#Entity
#Table
#ToString
public class Page {
#Id
#GeneratedValue(strategy = AUTO)
private Long id;
#Column(length = 1000000)
private String text;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "book_id")
private Book book;
// ...get/set methods
My controllers:
#RestController
#RequestMapping("books")
public class BookController {
private final BookRepo bookRepo;
#Autowired
public BookController(BookRepo bookRepo) {
this.bookRepo = bookRepo;
}
#GetMapping
#JsonView(Views.IdName.class)
public List<Book> getAll() {
return bookRepo.findAll();
}
#GetMapping("{id}")
public Book getOne(#PathVariable("id") Book book) {
return book;
}
}
Another one:
#RestController
#RequestMapping("authors")
public class AuthorController {
private final AuthorRepo authorRepo;
#Autowired
public AuthorController(AuthorRepo authorRepo) {
this.authorRepo = authorRepo;
}
#GetMapping
public List<Author> getAll() {
return authorRepo.findAll();
}
#GetMapping("{id}")
public Optional<Author> getOne(#PathVariable("id") Long id) {
return authorRepo.findById(id);
}
}
And also repo for interaction with DB (they are the similar):
public interface AuthorRepo extends JpaRepository<Author, Long> {
}
So when I make a request for get all books, I take the following JSON:
enter image description here
Bit I want different result, something like:
[
{
"book_id" : 1,
"name": "name 1 book",
"author" :
{
"author_id" : 1,
"name": "some_name"
}
}
]
Also, when I tried to make a request for /authors/1, I will get the following response (something like recursion) :
enter image description here
So any help how can I handle with it? Thank you!
You can use a #NoRepositoryBean
like in this example:
#NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
extends Repository<T, Long> {
#Query("select new com.example.YourObjectWithConstructor(e.attribute, sub.sub_attribute) from entity e inner join e.subtable sub where e.attribute = ?1")
List<YourObjectWithConstructor> findAllByAttribute(String attribute);
}
My example may not be 100% correct, I did not check the syntax. Feel free to explore it
Check this also:
JPQL Create new Object In Select Statement - avoid or embrace?

Spring Boot JPA returns List of List not List of Object

I am new to spring boot jpa. I would like to get return type as List but I am getting just List
My Entity Class
#Component
#Entity
#Table(name = "USERDB.USERS")
public class User() {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MY_SEQ")
#SequenceGenerator(sequenceName = "MY_SEQ_NAME", allocationSize = 1), name = "MY_SEQ")
#Column(name = "userId")
private long id;
#Column(name = "firstName")
private String fName;
#Column(name = "midName")
private String mName;
#Column(name = "lastName")
private String lName;
#Column(name = "email")
private String email;
#Column(name = "createdDate")
private Timestamp createdOn;
public User() {
this.createdOn = new Timestamp(System.currentTimeMillis()
}
//SETTERS & GETTERS
}
My Repository;
public interface UserRepository extends JpaRepository<User, String> {
#Query("SELECT id fName, lastName, email FROM User u WHERE u.fName=(:fName)")
public List<User> findByEmail(#Param("fName") String fName);
}
All I wanted is to get a json response as a User Array with key value pair like below
[
[
"id": 1001,
"fName": John",
"lName": "Doe",
"email": "johnd#example.com"
],
[
"id": 1002,
"fName": "John",
"lName": "Simmons",
"email": "johns#example.com"
],
]
but I am getting a list with just values as below.
[
[
1001,
"John",
"Doe",
"johnd#example.com"
],
[
1002,
"John",
"Simmons",
"johns#example.com"
],
]
I am not sure where I am doing wrong or is this how I am supposed to get ? This is a hypothetical example of my actual program. Please excuse me for any errors.
Here is my controller class
#Restcontroller
public class UserController {
#Autowired
UserRepository repo;
#GetMapping("/user/{fname}")
public List<User> getUserByName(
#PathVariable("fname") String fname) {
return repo.findByEmail(fname);
}
}
A few points on your code:
public interface UserRepository extends JpaRepository<User, String> {
Query("SELECT id fName, lastName, email FROM User u WHERE u.fName=(:fName)")
public List<User> findEmails(#Param("fName") String fName);
}
You are creating the repository with JpaRepository<User, String> but in your class User id is not of type String. Consider making the ID field a String.
The method is findEmails but it is returning a List<User>? I would expect such a function to return List<String> - each string of course being an email. You may consider renaming this function to avoid future confusion. Not a big deal though.
In your query:
#Query("SELECT id fName, lastName, email FROM User u WHERE u.fName=(:fName)")
You should change this to:
#Query("SELECT u.id u.fName, u.lastName, u.email FROM User u WHERE u.fName=(:fName)")
That should fix the serialization issue you are having.
I very much agree with the suggestion of Fermi-4. Using a better naming convention is key to have a better easy manageable code. You should follow them. To solve your problem, just do the following and it will solve your problem.
public interface UserRepository extends JpaRepository<User, long> {
public List<User> findByFname(String fName);
}
Also, consider changing USER Entity definition to add serializable implementation as below
#Component
#Entity
#Table(name = "USERDB.USERS")
public class User implements Serializable{
/**
*
*/
private static final long serialVersionUID = 3L;
Create additional User constructor with only few arguments that you wish to fetch from table
public User(long id, String fName, String lastName, String email) {
this.id = id;
this.fName = fName;
this.lastName = lastName;
this.email = email;
}
Adjust JP query like following
#Query("SELECT new com.package.User(u.id, u.fName, u.lastName, u.email) FROM User u WHERE u.fName = :fName")
public List<User> findByEmail(#Param("fName") String fName);
replace com.package with your User's actual package

Cascade persist with Spring Data REST

I have two Entities with bidirectional relation OtM <-> MtO. I also use cascade PERSIST because I would like to persist the data at once.
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String title;
#ManyToOne(cascade = CascadeType.PERSIST)
private Author author;
}
#Entity
public class Author {
#Id
#GeneratedValue
private Long id;
private String title; //mr, mrs
private String name;
#OneToMany(mappedBy = "author")
private List<Book> books;
}
I have created BookRepository at first and exposed it with Spring Data REST.
#RepositoryRestResource
public interface BookRepository extends JpaRepository<Book, Long>{
}
When I sent POST request with JSON:
{
"title": "Some title",
"author": {
"title": "Mr",
"name": "John Doe"
}
}
everything works and both book and author entities are persisted. Now I wanted to expose data about authors so I've added another Repository:
#RepositoryRestResource
public interface AuthorRepository extends JpaRepository<Author, Long> {
}
Now when I send the same JSON, the book entity is persisted, but author entity is not. What is more, the book title is now "Mr".
I do not understand this weird behaviour. Why with single repository everything works fine, but after adding another one, Spring is not only not persisting related author entity, but it is taking the wrong "title" field from JSON that I've sent?
Is there any way to persist the data with single request or I always have to persist author first and then persist the book with HAL format like "author": "http://.../createdAuthorId"?
See the office guide. Here an example:
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String title;
#ManyToOne(cascade = CascadeType.PERSIST)
#RestResource(exported = false)
private Author author;
}
I add #RestResource(exported = false) to the author property.
The Author entity should not be modified.
You can cascade-persist author like this:
{
"title": "Some title",
"author": {
"title": "Mr",
"name": "John Doe"
}
}

Spring Data Rest #EmbeddedId cannot be constructed from Post Request

I have a JPA entity Person and an entity Team. Both are joined by an entity PersonToTeam. This joining entity holds a many-to-one relation to Person and one to Team. It has a multi-column key consisting of the ids of the Person and the Team, which is represented by an #EmbeddedId. To convert the embedded id back and forth to the request id I have a converter. All this follows the suggestion on Spring Data REST #Idclass not recognized
The code looks like this:
#Entity
public class PersonToTeam {
#EmbeddedId
#Getter
#Setter
private PersonToTeamId id = new PersonToTeamId();
#ManyToOne
#Getter
#Setter
#JoinColumn(name = "person_id", insertable=false, updatable=false)
private Person person;
#ManyToOne
#Getter
#Setter
#JoinColumn(name = "team_id", insertable=false, updatable=false)
private Team team;
#Getter
#Setter
#Enumerated(EnumType.STRING)
private RoleInTeam role;
public enum RoleInTeam {
ADMIN, MEMBER
}
}
#EqualsAndHashCode
#Embeddable
public class PersonToTeamId implements Serializable {
private static final long serialVersionUID = -8450195271351341722L;
#Getter
#Setter
#Column(name = "person_id")
private String personId;
#Getter
#Setter
#Column(name = "team_id")
private String teamId;
}
#Component
public class PersonToTeamIdConverter implements BackendIdConverter {
#Override
public boolean supports(Class<?> delimiter) {
return delimiter.equals(PersonToTeam.class);
}
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
if (id != null) {
PersonToTeamId ptid = new PersonToTeamId();
String[] idParts = id.split("-");
ptid.setPersonId(idParts[0]);
ptid.setTeamId(idParts[1]);
return ptid;
}
return BackendIdConverter.DefaultIdConverter.INSTANCE.fromRequestId(id, entityType);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if (id instanceof PersonToTeamId) {
PersonToTeamId ptid = (PersonToTeamId) id;
return String.format("%s-%s", ptid.getPersonId(), ptid.getTeamId());
}
return BackendIdConverter.DefaultIdConverter.INSTANCE.toRequestId(id, entityType);
}
}
The problem with this converter is, that the fromRequestId method gets a null as id parameter, when a post request tries to create a new personToTeam association. But there is no other information about the payload of the post. So how should an id with foreign keys to the person and the team be created then? And as a more general question: What is the right approach for dealing many-to-many associations in spring data rest?
After running into the same issue I found a solution. Your code should be fine, except I return new PersonToTeamId() instead of the DefaultIdConverter if id is null in fromRequestId().
Assuming you are using JSON in your post request you have to wrap personId and teamId in an id object:
{
"id": {
"personId": "foo",
"teamId": "bar"
},
...
}
And in cases where a part of the #EmbeddedId is not a simple data type but a foreign key:
{
"id": {
"stringId": "foo",
"foreignKeyId": "http://localhost:8080/path/to/other/resource/1"
},
...
}

Resources