Is it possible to use a projection and in some related object use it's own projection?
For example, a have Exam, that has List<Question>. I'd like to request a list of exams (which I have a #projection), but I'd like to define the attributes to be retrieved for each related Question
If I understand correctly you want to use Projection as children of Projection. If it is the case, yes, you can. You can create a QuestionProjection and use inside the ExamProjection.
Example:
#Projection(name = "questionProjection", types = { Question.class })
public interface QuestionProjection {
// Getters
}
#Projection(name = "examProjection", types = { Exam.class })
public interface ExamProjection {
List<QuestionProjection> getQuestionList();
// Other Getters
}
You can do it something like this:
Assuming that your Exam entity might be:
#Entity
#Table(name = "EXAM")
public class Exam implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "DESCR")
private String descr;
#OneToMany(mappedBy = "exam")
private List<Question> questions;
// Getters and Setters
and your Question entity
#Entity
#Table(name = "QUESTION")
public class Question implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "DESCR")
private String descr;
#Column(name = "CONT")
private String cont;
#ManyToOne
#JoinColumn(name = "EXAM_ID")
#JsonIgnoreProperties("questions")
private Exam exam;
So, create projections
public interface ExamProjection {
Long getId();
String getDescr();
List<QuestionProjection> getQuestions();
}
and
public interface QuestionProjection {
Long getId();
String getDescr();
}
Your repository
#Repository
public interface ExamRepository extends JpaRepository<Exam, Long> {
List<Exam> findAll();
#Query("SELECT e FROM Exam e")
List<ExamProjection> findAllProjection();
}
Observe that using the findAll() method and passing the list type as ExamProjection, for some reason, causes a incompatible return type error. To avoid that, create a custom method, in this case, findAllProjection().
The service
#Service
public class ExamService {
#Autowired
ExamRepository examRepository;
public List<ExamProjection> findAllProjection() {
return examRepository.findAllProjection();
}
}
and finally, the resource
#RestController
#RequestMapping(value = "/exam")
public class ExamResource {
#Autowired
ExamService examService;
#GetMapping
public ResponseEntity<List<ExamProjection>> findAll() {
return ResponseEntity.ok().body(examService.findAllProjection());
}
}
Using the above, the json returned don't contains the field cont, because de QuestionProjection don't have the method getCont().
[
{
"id": 1,
"descr": "First Exam",
"questions": [
{
"id": 1,
"descr": "First Question"
},
{
"id": 2,
"descr": "Second Question"
}
]
}
]
If the QuestionProjection changes to
public interface QuestionProjection {
Long getId();
String getCont();
}
the json returned changes to
[
{
"id": 1,
"descr": "First Exam",
"questions": [
{
"id": 1,
"cont": "First Question Content"
},
{
"id": 2,
"cont": "Second Question Content"
}
]
}
]
Related
I'm creating a Spring JPA project with the following structure:
public class Pipeline {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private SourceConfig sourceConfig;
private SinkConfig sinkConfig;
...
...
}
public abstract class SourceConfig {
private long id;
private String name;
}
public abstract class SinkConfig {
private long id;
private String name;
}
public KafkaSourceConfig extends SourceConfig {
private String topic;
private String messageSchema;
}
public MysqlSourceConfig extends SourceConfig {
private String databaseName;
private String tableName;
}
Now when the client passes the following JSON, how would the program know which SourceConfig subclass to add to the Pipeline object?
{
"name": "mysql_to_bq_1",
"sourceConfig": {
"source": "MYSQL",
},
"sinkConfig": {
},
"createdBy": "paul"
}
You need to specify what class type you want to use like this
So instead of
private SourceConfig sourceConfig;
use
private KafkaSourceConfig kafkaSourceConfig;
Or you can keep it like you have but in case you want use some property of child class you have to cast it to that class type.
e.g.
KafkaSourceConfig kafkaSourceConfig = (KafkaSourceConfig)sourceConfig;
it can look like this
if(sourceConfig instanceOf KafkaSourceConfig)
{
KafkaSourceConfig ksc = (KafkaSourceConfig)sourceConfig;
}
if(sourceConfig instanceOf MysqlSourceConfig)
{
MysqlSourceConfig mysc= (MysqlSourceConfig)sourceConfig;
}
I looked around a bit and came to know about Polymorphic Deserialization, that's supported by Jackson.
Deserialize JSON with Jackson into Polymorphic Types - A Complete Example is giving me a compile error
Essentially, we have to infer from some field which subclass to refer to from the JSON that is provided.
For example,
"sourceConfig": {
"source": "MYSQL"
}
This should call MysqlSourceConfig's constructor. So the below code does the work.
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Data
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "source",
defaultImpl = MysqlSourceConfig.class,
visible = true)
#JsonSubTypes({
#JsonSubTypes.Type(value = MysqlSourceConfig.class, name = "MYSQL"),
#JsonSubTypes.Type(value = KafkaSourceConfig.class, name = "KAFKA"),
})
public abstract class SourceConfig {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Enumerated(value = EnumType.STRING)
#NotNull
private Source source;
}
I have two entities, which are in a many to many relationship.
#Entity
public class Room {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany(mappedBy = "rooms")
private Set<Team> teams;
}
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
private Set<Room> rooms;
}
To yield data, i have a repository for "Room" and "Team":
public interface RoomRepository extends CrudRepository<Room, Long> {
}
public interface TeamRepository extends CrudRepository<Team, Long> {
}
My goal is to request all rooms of a team, but prevent JPA from looping infinitely.
#RestController
#RequestMapping("....")
public class RoomController {
#Autowired
private RoomRepository roomRepository;
#GetMapping
public Iterable<Room> getAllRoomsOfTeam() {
final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.
final var team = teamRepository.findById(exampleId);
return ResponseEntity.ok(team);
}
}
This is the result:
{
"id": 1,
"name": "Team1",
"rooms": [
{
"id": 1,
"name": "Room 1",
"teams": [
{
"id": 1,
"name": "Team 1",
"rooms": [
{
"id": 1,
"name": "Room 1",
"teams": [
Jackson will loop forever, until an exception occurs (Since the back reference also references the parent element, which will create a loop).
I already tried #JsonManagedReference and #JsonBackReference, but they are used for many to one relationships.
How do i stop Jackson from looping infinitely? I want to affect other repositories and queries as little as possible.
Your controller shoud not return entities ( classes with the annotation #Entity). As a best practice is to create another separate class with same attributes. This code has a little dupplication but it keeps all the layers clean. I also suggest to use #Service.
public class RoomDTO {
private String name;
private List<TeamDTO> teams = new ArrayList<>();
public RoomDTO() {
}
public RoomDTO(Room room) {
this.name = room.name;
for(Team team : room.getTeams()) {
TeamDTO teamDTO = new TeamDTO();
teamDTO.setName(team.getName);
teams.add(teamDTO);
}
}
}
public class TeamDTO {
List<RoomDTO> rooms = new ArrayList();
public TeamDTO() {
}
public TeamDTO(Team team) {
this.name = team.name;
for(Room room : team.getRooms()) {
RoomDTO roomDTO = new RoomDTO();
roomDTO.setName(team.getName);
rooms.add(roomDTO);
}
}
}
The controller should return this
#GetMapping
public Iterable<TeamDTO> getAllRoomsOfTeam() {
final long exampleId = 1;
final var team = teamRepository.findById(exampleId);
TeamDTO teamDTO = new TeamDTO(team);
return ResponseEntity.ok(teamDTO);
}
How to use DTOs in the Controller, Service and Repository pattern
Currently, there is cyclic dependency in your classes which is causing issues while converting objects to JSON. Please add #JsonIgnore annotation on rooms variable in your Team class as shown in below example:
import com.fasterxml.jackson.annotation.JsonIgnore;
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
#JsonIgnore
private Set<Room> rooms;
}
If you need a solution for bidirectional conversion then you can use JsonView annotation.
First of all you need to create JSON view profiles for Team and Room as shown in below example:
public class JsonViewProfiles
{
/**
* This profile will be used while converting Team object to JSON
*/
public static class Team {}
/**
* This profile will be used while converting Room object to JSON
*/
public static class Room {}
}
Use above created JSON view profiles in your entities as shown in below example:
public class Room {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView({ JsonViewProfiles.Team.class, JsonViewProfiles.Room.class })
private long id;
#JsonView(JsonViewProfiles.Room.class)
#ManyToMany(mappedBy = "rooms")
private Set<Team> teams;
}
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView({JsonViewProfiles.Team.class, JsonViewProfiles.Room.class})
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
#JsonView(JsonViewProfiles.Team.class)
private Set<Room> rooms;
}
While converting your object to JSON please use these profiles as shown in below example:
#GetMapping
public String getAllRoomsOfTeam() {
final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.
final Team team = teamRepository.findById(exampleId);
String result = new ObjectMapper().writerWithView(JsonViewProfiles.Team.class)
.writeValueAsString(team);
return result;
}
well I have a doubt, i have a class like this:
#Entity(name = "movie_details")
#Getter #Setter
public class MovieDetailEntity implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
private long id;
#ManyToOne
#JoinColumn(name = "movie_id", nullable = false)
#JsonIgnoreProperties(value = {"movieDetails", "handler","hibernateLazyInitializer"}, allowSetters = true)
private MovieEntity movie;
#ManyToOne
#JoinColumn(name = "character_id", nullable = false)
#JsonIgnoreProperties(value = {"movies", "handler","hibernateLazyInitializer"}, allowSetters = true)
private CharacterEntity character;
}
And I just want to show the characters from a movie, but I don't want to show the id and the MovieEntity I just want to show CharacterEntity...
#Repository
public interface MovieDetailRepository extends JpaRepository<MovieDetailEntity, Long> {
Page<MovieDetailEntity> findByMovie(Pageable pageable, MovieEntity movie);
}
And this query return a json like this
"content": [
{
"id": ...,
"movie": {
...
},
"character": {
...
}
}
],
I was reading about making an interface dto with getters but didn't work for me
It sounds like you want to turn your entity into a DTO object that you can return. You'd need to create a new object like
public class MovieDetailDto {
private CharacterDto;
}
You would then probably want to implement a setter and getter method for CharacterDto and create the CharacterDto class similarly to the MovieDetailDto class with the fields from CharacterEntity you want to return with setters and getters for them.
When you get back a MovieDetailEntity from your repository you can then just create a new CharacterDto for the CharacterEntity and set it in a new MovieDetailDto.
I have could solve it, i create a interface projection, like this...
public interface MovieCharactersDto {
CharacterEntity getCharacter();
interface CharacterEntity {
Long getId();
String getName();
String getImage();
BigDecimal getWeight();
Integer getAge();
String getStory();
}
}
And the query was as follows:
#Repository
public interface MovieDetailRepository extends JpaRepository<MovieDetailEntity, Long> {
Page<MovieCharactersDto> findByMovie(Pageable pageable, MovieEntity movie);
}
I have two model class, two repository, two service and two controller which are as below.
public class ABC {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#Column(name = "abcField1")
String abcField1;
#NotNull
#Email
#Column(name = "abcField2")
String abcField2;
//getter and setters
}
public class XYZ {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#NotNull
#Column(name = "xyzField1")
String xyzField1;
#NotNull
#Column(name = "xyzField2")
String xyzField2;
#ManyToOne(cascade=CascadeType.PERSIST)
#JoinColumn(name = "abc_id", referencedColumnName = "id")
ABC abc;
//getter and setters
}
#RestResource(exported = false)
public interface ABCRepository extends CrudRepository<ABC, Long> {
}
#RestResource(exported = false)
public interface XYZRepository extends CrudRepository<XYZ, Long> {
}
#Service
public class XYZService {
#Autowired
XYZRepository xyzRepository;
public XYZ save(XYZ x) {
return xyzRepository.save(x);
}
}
#Service
public class ABCService {
#Autowired
ABCRepository abcRepository;
public ABC save(ABC a) {
return abcRepository.save(a);
}
}
public class ABCController {
#Autowired
ABCService abcService;
#PostMapping(path = "/abc/save")
public ResponseEntity<ABC> register(#RequestBody ABC a) {
return ResponseEntity.ok(abcService.save(a));
}
}
public class XYZController {
#Autowired
XYZService xyzService;
#PostMapping(path = "/xyz/save")
public ResponseEntity<XYZ> register(#RequestBody XYZ x) {
return ResponseEntity.ok(xyzService.save(x));
}
}
Problem is that when I try to save below JSON it is going insert row in ABC table every time.
{
"xyzField1": "XYZ1",
"xyzField2": "XYZ2",
"abc":{
"abcField1": "ABC1",
"abcField2": "ABC2",
.
.
},
.
.
}
And I want to update row of ABC table instead of adding every time so when I pass id field in below json as shown below then I face error "org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist"
{
"xyzField1": "XYZ1",
"xyzField2": "XYZ2",
"abc":{
"id" : "1",
"abcField1": "ABC1",
"abcField2": "ABC2",
.
.
},
.
.
}
I have tried to find solution for this and they are suggesting that save method of CrudRepositary will automatically update when we pass id in JSON but it is not working in my case.
The issue here seems to be with the cascade type you are providing. Since it is PERSIST, so whenever you try to persist your parent entity (ie XYZ entity), hibernate also tries to persist your child entity(ie ABC entity). Since there is a row already available in ABC with id 1, you are facing this issue.
Therefore, you should change the cascade type to MERGE in order to satisfy both the cases.
My entity class looks like this :
#Entity
#Table(name = "tbl_programstrm_projstream")
public class ProgramStm_Projstrm_Model {
#Id
#GeneratedValue
private int id;
#Lob
#Column(columnDefinition="TEXT")
private String programstrm;
private int programstrmId;
#ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH })
#JoinColumn(name = "projstrmId")//
private ProjectStreamModel projstrmId;
//getter and setter
}
Based on this entity I am getting result json like this
{
"programstrm": "D-BSS Implementation",
"programstrmId": 3,
"projstrmId": {
"name": "Program Leadership"
}
}
But I want ProjectStreamModel response as a string not as an object so that final result json looks like this"
{
"programstrm": "D-BSS Implementation",
"programstrmId": 3,
"projstrmId": "Program Leadership"
}
If I undestood right, using #JsonSerialize will probably help you (for example link)