I'm using spring-data-mongodb 1.8.2 (spring-boot-starter 1.3.1) and I have a fairly easy case at hand (in which I added fetch eager in desperation):
#Document(collection = "class_room")
public class ClassRoom implements Serializable {
#Id
private String id;
#NotNull
#Field("name")
private String name;
#ManyToOne**(fetch = FetchType.EAGER)**
#JoinColumn(name = "school_id")
private School school;
[...]
}
#Document(collection = "school")
public class School implements Serializable {
#Id
private String id;
#NotNull
#Field("name")
private String name;
#OneToMany(mappedBy = "school"**, fetch = FetchType.EAGER**)
private Set<Article> articles = new HashSet<>();
[...]
}
The repositories:
public interface SchoolRepository extends MongoRepository {
}
public interface ClassRoomRepository extends MongoRepository<ClassRoom,String> {
}
And the resources:
#RequestMapping(value = "/schools",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<School> getAllSchools() {
return schoolRepository.findAll();
}
#RequestMapping(value = "/classRooms",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<ClassRoom> getAllClassRooms() {
return classRoomRepository.findAll();
}
Now, can someone explain to me why the articles are correctly loaded when I perform the 'schoolRepository.findAll()'
but not when I perform the 'classRoomRepository.findAll()'?
And how can I make it happen?
TL;DR
A School has set of Articles
A classRoom has a School.
When I access a school directly: I see the set of Article
When access a school through a classRoom, the set of Article is empty.
Your approach to using object associations is a bit off. In Spring Data with Mongo the concepts of defining annotations to describe how associations happen is not the standard approach.
If you see the documentation here http://docs.spring.io/spring-data/data-mongo/docs/1.4.2.RELEASE/reference/html/mapping-chapter.html it helps to provide more clarity.
But to highlight, Mongo uses the concept of embedded objects and so ideally your data structure can be something like:
#Document(collection = "class_room")
public class ClassRoom implements Serializable {
#Id
private String id;
private String name;
private School school;
// Where School has the following fields and structure:
// private String id;
// private String name;
// private Set<Article> articles = new HashSet<>()
}
If you want school to be embedded in ClassRoom you leave it as above, else you can have School as a separate collection of its own. So:
#Document(collection = "school")
public class School implements Serializable {
#Id
private String id;
private String name;
private Set<Article> articles = new HashSet<>();
[...]
}
In the above School, it is a collection of its own and is not embedded in ClassRoom.
Typically, you have to just think differently from your traditional ORM approach when dealing with Mongo, or a NoSQL/Graph database.
Related
I'm working on a database for adding bands, musicians, instruments, etc.
I have a table 'band' and a table 'musician'. They have a ManyToMany relationship (one band can have many musicians, a musician can be in many bands), with an extra table BandMusician that has an embeddedId BandMusicianId. I did it like this because I want the relationship between bands and musicians to have also other information, like the year the musician joined the band.
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Band {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String genre;
private int year;
#OneToOne(mappedBy = "band")
private Website website;
#OneToMany(mappedBy = "band")
private List<Album> albuns;
#OneToMany(mappedBy = "band")
private List<BandMusician> musicians;
}
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#JsonDeserialize(using = MusicianJsonDeserializer.class)
public class Musician {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#JsonFormat(pattern = "dd-MM-yyyy")
#JsonProperty("DoB")
#Column(name = "date_of_birth")
private LocalDate DoB;
#ManyToMany
#JoinTable(
name = "musician_instruments",
joinColumns = #JoinColumn(name = "musician_id"),
inverseJoinColumns = #JoinColumn(name = "instrument_id")
)
private List<Instrument> instruments = new ArrayList<>();
#OneToMany(mappedBy = "musician")
private List<BandMusician> bands;
public void addInstrument(Instrument instrument) {
this.instruments.add(instrument);
}
}
#Embeddable
#Data
#AllArgsConstructor
#NoArgsConstructor
public class BandMusiciansId implements Serializable{
private static final long serialVersionUID = 1L;
#Column(name = "band_id")
private Long bandId;
#Column(name = "musician_id")
private Long musicianId;
}
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class BandMusician {
#EmbeddedId
private BandMusiciansId id = new BandMusiciansId();
#ManyToOne
#MapsId("bandId")
#JoinColumn(name = "band_id")
private Band band;
#ManyToOne
#MapsId("musicianId")
#JoinColumn(name = "musician_id")
private Musician musician;
private String role;
private int joined;
}
When I receive a POST request to "/musician" I can save a musician. I'm using Jackson to deserialize a request like this:
{
"name": "John the Ripper",
"DoB": "03-12-1965",
"instruments": "voice, guitar",
"bands": "Band1, Band2"
}
With Jackson I can get each band, search with the BandRepository and create a BandMusician.
THE PROBLEM: When I receive the request, in order to create a BandMusician I have to create a BandMusiciansId, and to do that I need the bandId and the MusicianId. But I'm creating the musician right now, so I don't have the musicianId. It is created automatically when I save the musician.
MusicianJsonDeserializer class
public class MusicianJsonDeserializer extends JsonDeserializer<Musician>{
private final InstrumentRepository instrumentRepository;
private final BandRepository bandRepository;
#Autowired
public MusicianJsonDeserializer(
InstrumentRepository instrumentRepository,
BandRepository bandRepository
) {
this.instrumentRepository = instrumentRepository;
this.bandRepository = bandRepository;
}
#Override
public Musician deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JacksonException {
ObjectCodec codec = p.getCodec();
JsonNode root = codec.readTree(p);
Musician musician = new Musician();
musician.setName(root.get("name").asText());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
musician.setDoB(LocalDate.parse(root.get("DoB").asText(), formatter));
if (root.get("instruments") != null) {
String instrumentList = root.get("instruments").asText();
String[] instrumentArray = instrumentList.split(", ");
List<Instrument> musicianInstrumentList = new ArrayList<>();
for (String instrument : instrumentArray) {
Instrument instrumentFound =
instrumentRepository.findByName(instrument)
.orElseThrow(RuntimeException::new);
// TODO custom exception
musicianInstrumentList.add(instrumentFound);
}
musician.setInstruments(musicianInstrumentList);
}
if (root.get("bands") != null) {
// TODO Stuck here!
What I thought of doing: In my MusicianService, after saving the musician, I can create the BandMusician and the relationship. I think doing this in the Service layer would be a bad choice though.
EDIT: To make it easier to understand, I created a project only with the relevant parts of this one and pushed to github (https://github.com/ricardorosa-dev/gettinghelp).
Again, what I want is to be able to send a POST to "/musician", that will be caught by the MusicianJsonDeserializer, and somehow create a BandMusicianId and BandMusician for each band sent in the request body.
I have the entities Band and Musician and a ManyToMany relationship between them with an association table BandMusician.
What I wanted was to create the entity Musician and the relationship (BandMusician) in the same request.
As far as I can gather it is not possible, because in order to create a record in the association table (BandMusician), I would have to have the musician (I'm creating in this request) already created.
I tried everything just to see if it was POSSIBLE and wasn't able to do it. But even if it was possible, it would be a very bad practice, since it would make the class too tightly coupled.
The clear solution was to create only the Musician with this request, and then send another request to create the connection between Band and Musician.
I also tried to create many entries in the BandMusician table with one request, which was also impossible, because the JsonDeserializer table doesn't seem to accept List<> as a return type. I was trying to avoid making a lot of requests to create the relationship entries (for a musician that is in five bands, for example), but it seems it is better to keep things clear and simple.
I now save one musician-band relationship per request:
{
"musician": "Awesome musician",
"band": "Awesome band",
"role": "guitar",
"joined": 2003
}
I'm using Spring MVC with Spring data.
Simple example of my problem:
My dao Service class:
#Service
#AllArgsConstructor
#Transactional
public class FooService{
private FooRepository fooRepo;
public Foo save(Foo foo){
return fooRepo.save(foo);
}
}
and controller:
#Controller
#AllArgsConstructor
#Transactional //if I remove this, method add does not save a foo.
//But I don't understand why, because FooService already has #Transactional annotation
public class FooController{
private FooService fooService;
#PostMapping("/add")
public String add(#RequestParam("programName") String programName, #RequestParam("id") long id){
Foo foo = fooService.findById(id).get();
foo.setProgramName(programName);
fooService.save(foo);
return "somePage";
}
}
If I remove #Transaction annotation from controller class, method save will not update foo object.
And I don't understand why I should mark controller by #Transactional annotation if I already mark service class by this annotation?
############ UPDATE ####################
Simple detailed description:
I have Program and Education entities. One Program has many Education, Education entity has foreign key program_id.
There is a page with Program form, there are fields: program id, program theme,..., and field with a list of education id separated by commas.
I'm trying to update the education list at the program, so I add a new education id at the page form and click save. Through debugger I see, that new education has appeared in the program, but changes do not appear in the database.
#Controller
#RequestMapping("/admin/program")
#AllArgsConstructor //this is lombok, all services autowired by lombok with through constructor parameters
#Transactional//if I remove this, method add does not save a foo.
//But I don't understand why, because FooService already has #Transactional annotation
public class AdminProgramController {
private final ProgramService programService;
private final EducationService educationService;
#PostMapping("/add")
public String add(#RequestParam("themeName") String themeName, #RequestParam("orderIndex") int orderIndex,
#RequestParam(value = "educationList", defaultValue = "") String educationList,
#RequestParam(value = "practicalTestId") long practicalTestId){
saveProgram(themeName, orderIndex, educationList, practicalTestId);
return "adminProgramAdd";
private Program saveProgram(long programId, String themeName, int orderIndex, String educationList, long practicalTestId){
List<Long> longEducationList = Util.longParseEducationList(parsedEducationList); //this is list of Education id separeted by commas that I load from page form
//creating new program and set data from page form
Program program = new Program();
program.setId(programId);
program.setThemeName(themeName);
program.setOrderIndex(orderIndex);
//starting loop by education id list
longEducationList.stream()
.map(educationRepo::findById)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(edu->{
//linking program and education
program.getEducationList().add(edu);
edu.setProgram(program);
});
//saving new program or updating by service if there is one already
Program savedProgram = programService.save(program);
//saving education with updated program
for(Education edu : savedProgram.getEducationList())
{
educationService.save(edu);
}
return savedProgram;
}
}
ProgramService:
#Service
#AllArgsConstructor //this is lombok, all services autowired by lombok with throught constructor parameters
#Transactional
public class ProgramService {
private ProgramRepo programRepo;
//other code here.....
public Program save(Program program) {
Optional<Program> programOpt = programRepo.findById(program.getId());
//checking if the program is already exist, then update it paramateres
if(programOpt.isPresent()){
Program prgm = programOpt.get();
prgm.setThemeName(program.getThemeName());
prgm.setOrderIndex(program.getOrderIndex());
prgm.setPracticalTest(program.getPracticalTest());
prgm.setEducationList(program.getEducationList());
return programRepo.save(prgm);
}
//if not exist then just save new program
else{
return programRepo.save(program);
}
}
}
Education service
#Service
#AllArgsConstructor //this is lombok, all services autowired by lombok with throught constructor parameters
#Transactional
public class EducationService {
private EducationRepo educationRepo;
//other code here....
public Education save(Education education){
return educationRepo.save(education);
}
}
Program entity:
#Entity
#ToString(exclude = {"myUserList", "educationList", "practicalTest"})
#Getter
#Setter
#NoArgsConstructor
public class Program implements Comparable<Program>{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name = "theme_name")
private String themeName;
#Column(name = "order_index")
private int orderIndex; //from 1 to infinity
#OneToMany(mappedBy = "program", fetch = FetchType.LAZY)
#OrderBy("orderIndex asc")
private List<Education> educationList = new ArrayList<>();
#OneToMany(mappedBy = "program", fetch = FetchType.LAZY)
private List<MyUser> myUserList = new ArrayList<>();
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "test_id")
private PracticalTest practicalTest;
public Program(int orderIndex, String themeName) {
this.orderIndex = orderIndex;
this.themeName = themeName;
}
public Program(long id) {
this.id = id;
}
//other code here....
}
Education entity:
#Entity
#ToString(exclude = {"program", "myUserList"})
#Getter
#Setter
#NoArgsConstructor
public class Education implements Comparable<Education>{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String link;
#Column(name = "order_index")
private int orderIndex;
private String type;
private String task;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "program_id")
private Program program;
#OneToMany(mappedBy = "education", fetch = FetchType.LAZY)
private List<MyUser> myUserList = new ArrayList<>();
public Education(String link, int orderIndex, String task, Program program) {
this.link = link;
this.orderIndex = orderIndex;
this.task = task;
this.program = program;
}
//other code here....
}
Program repo:
#Repository
public interface ProgramRepo extends CrudRepository<Program, Long> {
Optional<Program> findByPracticalTest(PracticalTest practicalTest);
Optional<Program> findByOrderIndex(int orderIndex);
List<Program> findByIdBetween(long start, long end);
}
Education repo:
#Repository
public interface EducationRepo extends CrudRepository<Education, Long> {
Optional<Education> findByProgramAndOrderIndex(Program program, int orderIndex);
#Query("select MAX(e.orderIndex) from Education e where e.program.id = ?1")
int findLastEducationIndexByProgramId(long programId);
}
I think the problem is program object created in one transaction and saved in another. That's why if I put Transactional on controller it works. There are two ways to solve the problem:
Without transactional on the controller: then I must save education object at first, because it has program id field and then save the program object.
With transactional on controller: then saving order has no matter, because saving object occurs in one transaction
I want to model a OneToMany Relation with Spring Data JDBC. I´ve read on this very useful blog https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates that you should use references when you want to model ToMany Reference:
Therefore any Many-to-One and Many-to-Many relationship must be modeled by just referencing the id.
So I have this scenario:
One Student can have multiple Registration. And one Registration can have exactly one Student. If you delete Registration the assigned Student should not get deleted cascading.
I ended up with this modelling:
#Data
#AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = #__(#PersistenceConstructor))
public class Registration {
private final #Id
#Wither
long registrationId;
#NotNull
private String electiveType;
#NotNull
private LocalDateTime created = LocalDateTime.now();
#NotNull
private StudentRegistrationReference studentRegistrationReference;
}
#Data
#AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = #__(#PersistenceConstructor))
public class StudentRegistrationReference {
private long student;
private long registration;
}
#Data
#AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = #__(#PersistenceConstructor))
public class Student {
private final #Id
#Wither
long studentId;
#NotNull
#Size(min = 4, max = 20)
private String userId;
#NotNull
#Min(0)
private int matriculationNumber;
#NotNull
#Email
private String eMail;
private Set<StudentRegistrationReference> studentRegistrationReferences = new HashSet<>();
}
My question is whether my modeling is correctly implemented?
You are quoting the article talking about "Many-To-X" but you talk yourself about "X-To-Many". You can model a One-To-One or a One-To-Many relationship with a direct reference, or a List/Set/Map of entities.
What you should avoid are bidirectional relationships. While you probably can make them work with the approach you are using, you really shouldn't.
Which brings us to the question: How should this model look like?
The central decision to make is how many aggregates are involved?
A Student certainly is an aggregate and the Student class is its aggregate root. It can exist on its own.
But what about Registration? I'd argue, it is probably part of the same aggregate. The delete test is a good one. If you delete a Student from the system, do the registrations of that Student still have value? Or should the disappear together with the Student?
As an exercise let's do both variants. I start with: Just one aggregate:
class Registration {
#Id private long Id;
String electiveType;
LocalDateTime created = LocalDateTime.now();
}
class Student {
#Id private long Id;
String userId;
int matriculationNumber;
String eMail;
Set<Registration> registrations = new HashSet<>();
}
With this, you would have a single repository:
interface StudentRepository extends CrudRepository<Student, Long>{}
I removed all the Lombok annotations since they aren't really relevant to the problem. Spring Data JDBC can operate on simple attributes.
If Registration and Student both are aggregates it gets a little more involved:
You need to decide which side owns the reference.
First case: The Registration owns the reference.
class Registration {
#Id private long Id;
String electiveType;
LocalDateTime created = LocalDateTime.now();
Long studentId;
}
public class Student {
#Id private long Id;
String userId;
int matriculationNumber;
String eMail;
}
Second case: The Student owns the reference
class Registration {
#Id private long Id;
String electiveType;
LocalDateTime created = LocalDateTime.now();
}
class Student {
#Id private long Id;
String userId;
int matriculationNumber;
String eMail;
Set<RegistrationRef> registrations = new HashSet<>();
}
class RegistrationRef {
Long registrationId;
}
Note that the RegistrationRef doesn't have a studentId or similar. The table assumed for the registrations property will have a student_id column.
I have a problem using JPA and RelationsShips One to Many with Jackson and Spring Rest ... I try to find multiples solutions but anything is working for me , and I don't kno where is the problem.
For example I have a table Team that has One to Many/Many To One relationship
I have two repository one for Team and another for Player
Team >>> has Many >> Player
Player >>> many to one >> Team
My entity Team has the following content
#Entity
#Table(name = "teams")
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Team {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private long teamId;
private String abbreviation;
private String team;
private String simpleName;
private String logo;
#OneToMany(cascade = {CascadeType.ALL,CascadeType.PERSIST,CascadeType.MERGE}, mappedBy = "team")
#Column(nullable = false)
private List<Player> players;
Theirs getters/setter , hashcodes and string similars.
On the other hand the entity Player
#Entity
#Table(name = "player")
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Player {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "teams_id", nullable=true)
private Team team;
private String name;
So , I have the typical get call in a controller in a repository.
#RestController
#RequestMapping("/api/public/team")
public class TeamController {
#Autowired
private TeamRepository teamRepository;
#Autowired
private GenericMethods genericMethods;
#GetMapping(value = "/{id}")
public Team getPersona(#PathVariable("id") int id) {
return teamRepository.findOne(genericMethods.toLong(id));
}
And repository
#Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
}
Now , when I call this endpoint I receive the following answer and I think that is incorrect , I only need a List With Players
{
"id":2,
"teamId":0,
"abbreviation":null,
"team":null,
"simpleName":"Betis",
"logo":null,
"players":[
{
"id":1,
"team":2,
"category":{
"id":1,
"nombre":"juvenil a",
"language":null,
"description":null,
"league":[
],
"players":[
1,
{
"id":2,
"team":2,
"category":1,
"name":"hulio"
}
]
},
"name":"pepe"
},
2
]
}
I need to acces at information with Player and Team so I can't use #JsonIgnoreProperties
Could anyone help to solve this problem ?
Depending on what you really want to achieve you may try different options. I'm not sure if you're using (or intending to use) spring-data-rest or not.
1. Dedicated repository
Spring data rest will embed the related entities if they don't have their own repository. Try creating public interface PlayersRepository extends JpaRepository...
2. Lazy loading
Why are you using FetchType.EAGER ? Try without it.
3. Projections
Projections are only applicable to lists, not to individual entities (i.e. not explicitly what you're asking for). You can hide players from the Teams collection even if it was returned by default like so:
#Projection(name = "noPlayers", types = { Team.class })
public interface TeamWithoutPlayers {
Long getId();
long getTeamId();
String getAbbreviation();
String getTeam();
String getSimpleName();
String getLogo();
}
More info - Spring Data Rest Projections
4. Ignore during serialization in Team Entity using #JsonIgnore
#JsonIgnore
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "teams_id", nullable=true)
private Team team;
Final thought
With spring-data-rest you can extend a CrudRepository instead of JpaRepository and access the item directly through the repository. That way you don't need to write a controller.
I have these Objects:
#Data
#Entity
#Table
#EqualsAndHashCode(callSuper = true)
public class User extends AbstractEntity implements Serializable {
private static final long serialVersionUID = -55089179131569489L;
private String username;
private String email;
private boolean admin;
private String name;
private String surname;
#OneToMany(mappedBy = "owner")
private List<Ad> ads;
}
and
#Entity
#Table
#Data
#EqualsAndHashCode(callSuper = true)
public class Ad extends AbstractEntity implements Serializable {
private static final long serialVersionUID = -4590938091334150254L;
private String name;
private String description;
private double price;
#Enumerated(EnumType.STRING)
private Category category;
#ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinColumn(name = "OWNER_ID")
private User owner;
}
When I try to execute a POST with an object of type Ad.class with inside an existing object of type User.class (already in the Database) the service saves only the Ad object and the join column "OWNER_ID" remains empty.
I think that the mapping is correct. Could you help me to figure out the problem?
This is my Repository:
#Repository
#Transactional(readOnly = true)
public interface AdRepository extends PagingAndSortingRepository<Ad, String>
{}
and this is my RestRepository
#RepositoryRestResource(collectionResourceRel = "ad", path = "ad")
public interface AdRestRepository extends PagingAndSortingRepository<Ad, String> {}
If I step back a little and generalize your problem,
You are trying to POST a sub resource and expect both actions of
making a new resource (Ad)
making association with the owner (User)
to be happened with a single call.
But unfortunately spring-data-rest does not support such a behavior. You need 2 calls to do this.
One to make the resource (Ad) => POST to /ads with actual payload
Second to make the association => POST to users/{ownerId} with the hateoas link of the resource created by the first call.
Take a look at this section of official documentation.