I was given this assignment, just for practice, it became very long and challenging, but it has taught me a lot, on lambdas and JPA mainly.
It is a basic Rest API, which is used to create Hotels, Rooms, Guests, Reservations, types of guests, types of rooms, etc.
My initial problem was learning about JPA relations, OneToOne, OneToMany, etc., unidirectional, bidirectional, and what not.
I'm also using PostgreSQL, using "sping.jpa.hibernate.ddl-auto=create-drop(or update)", change as needed, when I want to recreate the DB for whatever reason.
So I'm very happy and excited using my new #Annotations to relate my Entities, and fetch back lists of whatever information I needed, came across multiple problems, read many many questions here, solved my problems, but now I have come across a new problem, but then, started questioning my approach, maybe I should not leave everything to JPA.
Let me show you what I mean. I'm going to keep my classes short to show only relevant information.
I have my reservation entity.
#Data
#Entity
#Table(name = "reservation")
public class Reservation {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "guest", referencedColumnName = "id")
#JsonManagedReference
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private Guest guest;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "room", referencedColumnName = "id")
private Room room;
#ManyToMany(fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
#JoinTable(name = "reservation_rooms",
joinColumns = { #JoinColumn(name = "reservation_id" )},
inverseJoinColumns = { #JoinColumn(name = "room_id") }
)
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private List<ReservationRoom> roomList = new ArrayList<>();
private LocalDate start_date;
private LocalDate end_date;
private Boolean check_in;
private Boolean check_out;
public void addRoom(Room room) {
this.roomList.add(room);
}
public void removeRoom(Long id) {
Room room = this.roomList.stream().filter(g -> g.getId() == id).findFirst().orElse(null);
if (room != null) {
this.roomList.remove(room);
}
}
}
This is my Room entity.
#Data
#Entity
#Table(name = "room")
public class Room {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String description;
private Integer floor;
#JsonProperty("max_guests")
private Integer maxGuests;
#ManyToOne(fetch = FetchType.LAZY)
#JsonBackReference
private Hotel hotel;
#ManyToOne(fetch = FetchType.LAZY)
#JsonProperty("type")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private RoomType roomType;
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Room)) {
return false;
}
return id != null && id.equals(((Room) o).getId());
}
#Override
public int hashCode() {
return getClass().hashCode();
}
}
And this is my Guest entity.
#Data
#Entity
#Table(name = "guest")
public class Guest {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String first_name;
private String last_name;
private String email;
#ManyToOne(fetch = FetchType.LAZY)
#JsonProperty("type")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private GuestType guest_type;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "guestList"
)
#JsonBackReference
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private List<Reservation> reservationList = new ArrayList<>();
public Guest(){}
public Guest(Long id) {
this.id = id;
}
public List<Reservation> getReservationList() {
return reservationList;
}
public void setReservationList(List<Reservation> reservationList) {
this.reservationList = reservationList;
}
}
At the beginning a reservation could only have 1 room, but the requirement changed and it can have multiple rooms now. So now, the guest list needs to be linked to the room linked to the reservation, and not directly to the reservation. (I know I have a Guest and a Room, and also the List of both, this is because I'm using the single Guest as the name for the reservation, and the single Room, as the "Main" room, but don't mind that please).
Letting JPA aside, because every challenge I have faced I would ask my self "how to do it JPAish?", and then research how to do it with JPA (that's how I learned about the #ManyToMany, etc. annotations).
What I would do is just create a new table, to relate the reservations to the room (which is already done in my entities with JPA), and then add also de guest id.
So, this new table, would have a PK with reservation_id, room_id and guest_id. Very easy, then create my Reservation model, which have a List of Room, and this Room model, would have a List of Guest. Easy.
But I don't want to add a List of Guest in my current Room entity, because I have an endpoint and maybe a couple of other functions, which retrieves my Room entity, and I don't want to add a List of Guest, because as the time passes, this list would grow bigger and bigger, and it is information you don't need to be passing around.
So I did some research and found that I can extend my entity with #Inheritance or #MappedSuperclass, and I could create maybe a Reservation_Room model, which includes a List of Guest and add a List of Reservation_Room instead of a List of Room in my Reservation Entity, which I really wouldn't know if it is even possible.
Having said that, and before I keep researching and start making modifications to my code, it got me wondering, if this would be the right approach? Or if I'm forcing JPA too much on this? What would be the best approach for this? Can a 3 id relation table be easily implemented/mapped on JPA?
The main goal would be to have my Room entity exposed as it is, but when a Room is added to a Reservation, this Room would also have a List of Guest. Can I do this JPAish? Or should I create a new model and fill with the information as needed? This wouldn't exempt me from creating my 3 ids table.
Based on what you wrote here, I think you might be at a point where you are realizing that the persistence model doesn't always match the presentation model, which you use in your HTTP endpoints. This is usually the point where people discover DTOs, which you also seem to have heard of.
DTOs should be adapted/created to the needs of the representation of an endpoint. If you don't want to expose certain state, then simply don't declare a getter/field for that data in a DTO. The persistence model should simply be designed in a way, so that you can persist and query data the way you need it. Translation between DTOs and entities is a separate thing, for which I can only recommend you to give Blaze-Persistence Entity Views a try.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Reservation.class)
public interface ReservationDto {
#IdMapping
Long getId();
GuestDto getGuest();
List<RoomDto> getRooms();
}
#EntityView(Guest.class)
public interface GuestDto {
#IdMapping
Long getId();
String getName();
}
#EntityView(Room.class)
public interface RoomDto {
#IdMapping
Long getId();
String getName();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ReservationDto a = entityViewManager.find(entityManager, ReservationDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<ReservationDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!
I would say that you need to add a layer between persistence and the endpoints. So, you will have Controllers/Services/Repositories (in the Spring world). You should use entities as return type from Repositories (so used them in Services as well), but return DTOs to Controllers. In this way, you will decouple any modification that you do between them (e.g. you may lose interest to return a field stored in an entity, or you may want to add more information to the dto from other sources).
In this particular case, I would create 4 tables: Reservations, Guests, Rooms, GuestsForReservation.
Guests will contain id + guests data (name, phone number, etc)
Rooms will contain id + room data
GuestsForReservation will contain id + reservationId + guestId (so you can get the list of guests for each reservation). FK for reservationId and guestId, PK for synthetic id mentioned.
Reservations will contain id (synthetic), room id, date from, date to, potentially main guest id (it could be the person paying the bill, if it makes sense for you). No link to the GuestForReservation table, or you can have a list of GuestForReservation if you need to.
When you want to reserve a room, you have a ReservationRequest object, which will go to the ReservationService, here you are going to query the ReservationRepository by roomId and dates. If nothing is returned, you create the various entities and persist them in ReservationRepository and GuestForReservation repository.
By using the service and the combination of various repositories, you should be able to get all the information that you need (list of guests per room, list of guests per date, etc). At the service level, you then map the data you need to a DTO and pass it to the controller (in the format that you need), or even to other services (depending on your needs).
For what concern the mapping between entities and DTOs, there are different options, you could simply create a Component called ReservationMapper (for example) and do it yourself (take an entity and build a DTO with what you need); implements Converter from the Springframework; use MapStruct (cumbersome in my opinion); etc.
If you want to represent in JPA an id made of multiple columns, usually #Embeddable classes are used (you should mark them as EmbeddedId when you use them), you can google them for more info.
Related
I would like to be able to create pagination for pulling all customers from the database (MYSQL), but I encountered a hibernate n+1 problem, which I then solved, but I encountered another problem: 2023-02-09 16:57:04.933 WARN 11660 --- [io-8080-exec-10] o.h.h.internal.ast.QueryTranslatorImpl : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
This problem I tried to solve with EntityGraph, but still nothing. Then I tried to use two Query, which collected the id and then used the IN clause, but this caused a huge sql query, which led to the generation of many "IN" which, with a huge dataset, can be problematic.
I am currently in a quandary and do not know how to solve this problem. I would like the figures to be fetched along with the customers, but I have no idea how to do it in such a way that the pagination works properly
I want to return CustomerDTO who have numberOfCreatedFigures attribute which is mapping from method in customer entity. This method is returning a size of customer figures.
I am using lombok for args/getters/setters. I've been trying to do everything, but nothing seems to fix the issue.
Config class with a mapper
#Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.createTypeMap(Customer.class, CustomerDTO.class)
.addMappings(mapper -> mapper
.map(Customer::numberOfCreatedFigures, CustomerDTO::setNumberOfFigures));
return modelMapper;
}
Customer class
public class Customer implements UserDetails, Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank(message = "Your name cannot be blank")
private String name;
#NotBlank(message = "Your name cannot be blank")
private String surname;
#NotBlank(message = "Your login cannot be blank")
private String login;
#NotBlank(message = "Your password cannot be blank")
private String password;
#Enumerated(EnumType.STRING)
private Role role;
private Boolean locked = false;
private Boolean enabled = true;
#OneToMany(mappedBy = "createdBy",
cascade = {CascadeType.MERGE, CascadeType.PERSIST},
fetch = FetchType.LAZY,
orphanRemoval = true)
#ToString.Exclude
private Set<Figure> figures = new HashSet<>() ...;
Figure class
public abstract class Figure implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(updatable = false, insertable = false)
private String figureType;
#Version
private Integer version;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "created_by_id")
#CreatedBy
#ToString.Exclude
private Customer createdBy;
#CreatedDate
private LocalDate createdAt;
#LastModifiedDate
private LocalDate lastModifiedAt;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "last_modified_by_id")
#LastModifiedBy
#ToString.Exclude
private Customer lastModifiedBy;
private Integer numberOfModification = 0 ...;
CustomerDTO class
public class CustomerDTO {
private Long id;
private String name;
private String surname;
private String login;
private Integer numberOfFigures;
private Role role;}
Method from Customer Controller
#GetMapping
public ResponseEntity<Page<CustomerDTO>> listAll(#PageableDefault Pageable pageable) {
return new ResponseEntity<>(customerService.listAll(pageable)
.map(customer -> modelMapper
.map(customer, CustomerDTO.class)), HttpStatus.OK);
}
I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Customer.class)
public interface CustomerDTO {
#IdMapping
Long getId();
String getName();
String getSurname();
String getLogin();
#Mapping("SIZE(figures)")
Integer getNumberOfFigures();
Role getRole();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
CustomerDTO a = entityViewManager.find(entityManager, CustomerDTO.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<CustomerDTO> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!
You could load the customers with the figures relationship eagerly initialized.
For this case, an entity graph would be suitable. You'd need to create a new repository method like this:
#Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
#EntityGraph(attributePaths = "figures")
List<Customer> findWithFiguresBy(Pageable pageable);
}
Then, you'd need call this repository method when searching instead of the one you are using now. With this approach, your figures relationship can remain lazily fetched (which is generally important as eager fetching is a code smell), but whenever you need to fetch customers with the figures eagerly loaded, you can use this method.
If you want to lear more about entity graphs, I recommend these articles:
JPA Entity Graph by Hibernate maintainer Vlad Mihalcea
JPA Entity Graph by Baeldung
Side note: if you had more than one association which needs to be loaded eagerly, you couldn't use an entity graph for that as it would result in a MultipleBagFetchException. Instead, you would load your parent entities as usual and then collect all ids into a list (say customerIds). Then, you'd need to load all child associations (say figures and otherFigures) by the customer id (JPQL example: select f from Figure f where f.customer.id in :customerIds) and place the figures in a Map<Long, List<Figure> (where the Long parameter is the customer id). Your mapper logic would then need to use the entities from the Maps for the DTOs instead of directly from the parent entity.
How can we get the count of OneToMany field of JPA entity as querying count for each parent entity while fetching as a list is costly and there is no way in JPA Repository.
I want to get the number of likes and comments for each PostEntity. The field is Lazy fetch type and if I call likes.size() or comments.size() then it will load all of the comments and likes from database and there can be thousands of comments and likes.
I know I can create a seperate repo for likes and comments to get the counts but while calling method from PostRepository how to get the counts for each and every entity? What is the best and efficient way?
Parent Entity
#Entity
#Table(name = "posts")
#Getter
#Setter
public class PostEntity extends MappedSuperClassEntity<UserEntity> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Nullable
private String title;
#Nullable
private String postText;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="user_id")
private UserEntity user;
#Nullable
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "community_id")
private CommunityEntity community;
#OneToMany(fetch = FetchType.LAZY)
private List<CommentEntity> comments;
#OneToMany(fetch = FetchType.LAZY)
private List<LikeEntity> likes;
#Transient
private int numberOfLikes;
#Transient
private int numberOfComments;
}
I would like to get the likes and comments count for each PostEntity while querying for the list of posts.
My Repo
public interface PostsRepository extends PagingAndSortingRepository<PostEntity, Integer> {
#Query(value = "SELECT P FROM PostEntity P WHERE P.user.id = :userId ORDER BY P.createdDate DESC")
Page<PostEntity> getUserPosts(int userId, Pageable pageable);
#Query(value = "select P from PostEntity P where p.community.id = :communityId order by P.createdDate desc")
Page<PostEntity> getCommunityPosts(int communityId, Pageable pageable);
}
I searched for a lot and someone suggested to use #Formula annotation for custom queries on the entity field but #Formula is hibernate specific and don't know if it works with #Transient field. Is there any JPA specific way to do that as it's a common problem.
You need "LazyCollection" annotation with EXTRA option.
#OneToMany(fetch = FetchType.LAZY)
#LazyCollection(LazyCollectionOption.EXTRA)
private List<CommentEntity> comments;
This annotation would allow to access "size()" without loading.
You can check this article.
https://www.baeldung.com/hibernate-lazycollection
Sometimes, we're only concerned with the properties of the collection, and we don't need the objects inside it right away. For example, going back to the Branch and the Employees example, we could just need the number of employees in the branch while not caring about the actual employees' entities. In this case, we consider using the EXTRA option. Let's update our example to handle this case. Similar to the case before, the Branch entity has an id, name, and an #OneToMany relation with the Employee entity. However, we set the option for #LazyCollection to be EXTRA:
I try to add comment but i have no writing comment access because of reputation so i send an answer.
Let's assume an application where there are leagues and teams inside of leagues, and teams can be in multiple leagues aswell. So we do have a many to many relationship.
League Entity
#Data
#Entity
public class League {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String countryCode;
private SportType sportType;
#ManyToMany(mappedBy = "leagues")
private List<Team> teams;
}
Team Entity
#Data
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String logoUrl;
private SportType sportType;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "team_league",
joinColumns = #JoinColumn(name = "team_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "league_id", referencedColumnName = "id"))
private List<League> leagues;
}
I am now on the point where i need for example to create a new team, and upon creation, it needs a league to put in, which has to already exist. That means i need an endpoint which takes a list of leagueIds instead of a List<League>. So i assume i need to build a DTO. But how should the DTO look like and how would i implement the method that maps the DTO to an entity and saves it to the database.
My idea of the TeamDTO
#Data
public class TeamDTO {
private Long id;
private String name;
private String logoUrl;
private SportType sportType;
private List<Integer> leagueIds;
}
So instead of a List<League> i do have a List<Integer> leagueIds so that the endpoint can accept proper JSON. Is that correct?
Now i want to create the team in the database, IF the leagues of List<Integer> leagueIds are present in the database. So my question now is, when do i map to the entity.
My idea of the implementation of the service
public class TeamServiceImpl implements TeamService {
#Autowired
private LeagueRepository leagueRepository;
#Autowired
private TeamRepository teamRepository;
#Override
public Team createTeam(TeamDTO teamDTO) {
List<Long> ids =
teamDTO.getLeagueIds().stream().filter(leagueId ->
leagueRepository.findById(leagueId).isPresent()).
collect(Collectors.toList();
if (!ids.isEmpty()) {
Team team = new Team();
team.setName(teamDTO.getName());
team.setLogoUrl(teamDTO.getLogoUrl());
team.setSportType(teamDTO.getSportType());
// do i actually need the League entities to set this?
team.setLeagues(...);
return team;
}
return null;
}
}
Most important question is: Is this the correct way?
Should i use a mapper for DTO to entity and vice versa?
Should i implement a mapper myself (i mean it only maps a few
fields)?
And on what place i would use the mapper, if i would implement one?
I don't know why you only want to save the team if it has leagues assigned that exist. It just sounds wrong to me i.e. some kind of bug is in your app if the league for an id does not exist. You should set a list of league references and rely on the FK-constraint to error if a wrong league id is used i.e. use something like this:
List<League> leagues =
teamDTO.getLeagueIds().stream().map(leagueId ->
leagueRepository.getOne(leagueId)).
collect(Collectors.toList());
team.setLeagues(leagues);
The DTO approach is fine and as long as it stays this simple, I guess using this custom implementation is good enough. If you have more complex requirements and want to make use of more efficient processing I would recommend you look into Blaze-Persistence Entity-Views which was made for exactly this purpose, efficient mapping between JPA entities and DTOs.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Team.class)
#UpdatableEntityView
public interface TeamDTO {
#IdMapping
Long getId();
String getName();
void setName(String name);
String getLogoUrl();
void setLogoUrl(String logoUrl);
SportType getSportType();
void setSportType(SportType sportType);
#UpdatableMapping
#JsonIgnore
List<LeagueDto> getLeagues();
default List<Long> getLeagueIds() {
return getLeagues().stream().map(LeagueDto::getId).collect(toList());
}
default void setLeagueIds(List<Long> ids) {
getLeagues().clear();
ids.stream().map(id -> evm().getReference(LeagueDto.class, id)).forEach(getLeagues()::add);
}
// This is a special context providing method
EntityViewManager evm();
#EntityView(League.class)
interface LeagueDto {
#IdMapping
Long getId();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
TeamDTO a = entityViewManager.find(entityManager, TeamDTO.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<TeamDTO> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!
The saving part will then be as simple as this:
public class TeamServiceImpl implements TeamService {
#Autowired
private TeamRepository teamRepository;
#Override
public Team createTeam(TeamDTO teamDTO) {
teamRepository.save(teamDTO);
return teamRepository.getOne(teamDTO.getId());
}
}
Due to the change-tracking implementation of Entity-Views, at any point in time it is clear what is dirty and will by default only flush these changes and avoid unnecessary select statements during flushing.
We are trying to save many child in a short amount of time and hibernate keep giving OptimisticLockException.
Here a simple exemple of that case:
University
id
name
audit_version
Student
id
name
university_id
audit_version
Where university_id can be null.
The java object look like:
#Entity
#Table(name = "university")
#DynamicUpdate
#Data
#Accessors(chain = true)
#EqualsAndHashCode(callSuper = true)
public class University {
#Id
#SequenceGenerator(name = "university_id_sequence_generator", sequenceName = "university_id_sequence", allocationSize = 1)
#GeneratedValue(strategy = SEQUENCE, generator = "university_id_sequence_generator")
#EqualsAndHashCode.Exclude
private Long id;
#Column(name = "name")
private String name;
#Version
#Column(name = "audit_version")
#EqualsAndHashCode.Exclude
private Long auditVersion;
#OptimisticLock(excluded = true)
#OneToMany(mappedBy = "student")
#ToString.Exclude
private List<Student> student;
}
#Entity
#Table(name = "student")
#DynamicUpdate
#Data
#Accessors(chain = true)
#EqualsAndHashCode(callSuper = true)
public class Student {
#Id
#SequenceGenerator(name = "student_id_sequence_generator", sequenceName = "student_id_sequence", allocationSize = 1)
#GeneratedValue(strategy = SEQUENCE, generator = "student_id_sequence_generator")
#EqualsAndHashCode.Exclude
private Long id;
#Column(name = "name")
private String name;
#Version
#Column(name = "audit_version")
#EqualsAndHashCode.Exclude
private Long auditVersion;
#OptimisticLock(excluded = true)
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "university_id")
#ToString.Exclude
private University university;
}
It seem when we assign university and then save Student, if we do more than 4 in a short amount of time we will get the OptimisticLockException.
It seem hibernate is creating update version on the University table even though the University didn't change at the db level.
UPDATE: code that save the student
Optional<University> universityInDB = universidyRepository.findById(universtityId);
universityInDB.ifPresent(university -> student.setUniversity(university);
Optional<Student> optionalExistingStudent = studentRepository.findById(student);
if (optionalExistingStudent.isPresent()) {
Student existingStudent = optionalExistingStudent.get();
if (!student.equals(existingStudent)) {
copyContentProperties(student, existingStudent);
studentToReturn = studentRepository.save(existingStudent);
} else {
studentToReturn = existingStudent;
}
} else {
studentToReturn = studentRepository.save(student);
}
private static final String[] IGNORE_PROPERTIES = {"id", "createdOn", "updatedOn", "auditVersion"};
public void copyContentProperties(Object source, Object target) {
BeanUtils.copyProperties(source, target, Arrays.asList(IGNORE_PROPERTIES)));
}
We tried the following
#OptimisticLock(excluded = true)
Doesn't work, still give the optimistic lock exception.
#JoinColumn(name = "university_id", updatable=false)
only work on a update since we don't save on the update
#JoinColumn(name = "university_id", insertable=false)
work but don't save the relation and university_id is always null
Change the Cascade behaviour.
The only one value that seem to made sense was Cascade.DETACH, but give a org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing.
Other solution we though of but are not sure what to pick
Give the client a 409 (Conflict) error
After the 409 the client must retry his post.
for a object sent via the queue the queue will retry that entry
later.
We don't want our client to manage this error
Retry after a OptimisticLockException
It's not clean since when the entry come from the queue we already doing it but might be the best solution so far.
Make the parent owner of the relationship
This might be fine if there are not a big number of relation, but we have case that might go in the 100 even in the 1000, which
will
make the object to big to be sent on a queue or via a Rest call.
Pessimistic Lock
Our whole db is currently in optimisticLocking
and we managed to prevent these case of optimisticLocking so far, we
don't want to change our whole locking strategy just because of this
case. Maybe force pessimistic locking for that subset of the model
but I haven't look if it can be done.
It does NOT need it unless you need it.
Do this:
University universityProxy = universidyRepository.getOne(universityId);
student.setUniversity(universityProxy);
In order to assign a University you don't have to load a University entity into the context. Because technically, you just need to save a student record with a proper foreign key (university_id). So when you have a university_id, you can create a Hibernate proxy using the repository method getOne().
Explanation
Hibernate is pretty complex under the hood. **When you load an entity to the context, it creates a snapshot copy of its fields and keeps track if you change any of it**. It does much more... So I guess this solution is the simplest one and it should help (unless you change the `university` object somewhere else in the scope of the same session). It's hard to say when other parts are hidden.
Potential issues
wrong #OneToMany mapping
#OneToMany(mappedBy = "student") // should be (mappedBy = "university")
#ToString.Exclude
private List<Student> student;
the collection should be initialized. Hibernate uses it's own impls of collections, and you should not set fields manually. Only call methods like add() or remove(), or clear()
private List<Student> student; // should be ... = new ArrayList<>();
*overall some places are not clear, like studentRepository.findById(student);. So if you want to have a correct answer it's better to be clear in your question.
If you enable your query logs from Hibernate, it would be worthwhile to see the queries that your ORM is performing. You'll likely realize that your ORM is doing too much.
In your application properties or config file enable hibernate.show_sql=true
I wouldn't be surprised if your single update to a Student becomes an update to a University which becomes an update to all of its containing Students. Everything gets a version bump.
ORM and entity mappings are for strategically retrieving data. They should not be used to actually define object relationships.
You'll want to visit strategies and design your entities based on how they are used in their REST endpoints.
You specified in your question that you are trying to save a Student but you're noticing that the University also gets updated along with every Student update.
Likely there would never be a time when a Student should ever update a University
Keep your entities lean!
You can structure your entity in such a way that supports this unidirectional relationship. I removed some of the annotation just to demonstrate the structure. You will want to keep in mind that when creating entities, you are writing them for how they are retrieved...
public class University {
#Id
private Long id;
private String name;
private Long auditVersion;
#OneToMany
private List<Student> student;
}
public class Student {
#Id
private Long id;
private String name;
private Long auditVersion;
private Long universityId;
}
This will ensure that updates to the student remains targeted and clean. You are simply assigning a university id to the student therefore establishing that relationship.
You typically want to respect LockExceptions. Retrying upon a LockException is simply bullying your database into submission and will cause more headaches as your application scales.
You always have the option to work with lean entities and create custom response or message objects that would zip the results together.
ORMs are not to be used to create shortcuts
The performance consequence of a SELECT on an indexed/foreign key is roughly the same cost of grabbing them joined... you only introduce a little extra network latency. A second trip to the database is not always a bad idea. (Often times, this is exactly how Hibernate fetches your entities)
You won't have to write queries, but you will still need to understand the retrieval and update strategies.
You're sacrificing database performance and introducing complexity for a convenient .getChild() method. You'll find that you resolve more performance/locking issues by removing annotations, not adding them.
I am designing an e-commerce web application using Spring JPA. I have 2 classes Product and Category, where a Product can be assigned to many categories, but a Category does not concern about Product.
#Entity(name = "products")
class Product {
#Id
#Column(name = "product_id")
private Long productId;
#ManyToMany
#JoinTable(
name = "product_category_links",
joinColumns = #JoinColumn(name = "product_id", referencedColumnName = "product_id"),
inverseJoinColumns = #JoinColumn(name = "category_id", referencedColumnName = "category_id"))
private List<Category> categories;
// getters, setters,
}
#Entity(name = "categories")
class Category {
#Id
#Column(name = "category_id")
private Long category_id;
// getters, setters
}
When a Product has its categories changed, I have a requirement to also update something in database. I am thinking of maintain this integrity by creating a dedicated service method to update product's category.
class ProductService
#Autowired
private ProductRepository productRepository;
private ComplexDBService complexDBService;
#Transactional
public void addCategory(Long productId, Long categoryId) {
Product p = productRepository.findByProductId(productId);
Category c = categoryRepository.findByCategoryId(categoryId);
p.getCategories.add(c);
complexDBService.doSomething();
}
}
But I think this is not practical because a Product can still have categories changed in other places. For example, in a controller, someone can get a Product directly from the repository can change its categories. I don't want to forbid this use case.
So I am thinking of putting the logic addCategory(Long productId, Long categoryId) in Product class itself, which actually suggested by Domain Driven Design. But I cannot figure out how to do that because I cannot inject the ComplextDBService into Product. One way is to pass it as an argument to addCategory method as addCategory(Long productId, Long categoryId, ComplextDBService complexDBService), is this a good practice? Is there some other ways to put custom database manipulation logic in a domain class?
addCategory(Long productId, Long categoryId, ComplextDBService complexDBService), is this a good practice?
No, it isn't. In complicated business cases, you sometime have to pass some kind of "service" as a parameter into method invoked on aggregate, but as a rule of thumb you should only invoke read-only-query method on this "service".
Is there some other ways to put custom database manipulation logic in a domain class?
There should only occur Product related things inside Product aggregate, e.g. manipulation of Product state.
Your requirement is to respond to occurrence inside Product aggregate.
Domain Event to the rescue
You need to invert the control. Product aggregate should inform the outside about Events inside itself and the outside should react to this. Product should not depend on other not related aggregates/concepts.
class Product {
void addCategory(CategorySnapshot category) {
categories.add(category);
eventPublisher.publish(new ProductCategoryAdded(getSnapshot(), category));
}
}
Now you should register other components to listen to ProductCategoryAdded event, it doesn’t matter what those other components are (if you need to make db operations, maybe you are implementing CQRS?).
You can implement publisher by yourself or use frameworks like Guava Event Bus, Axon etc.
By the way, you are missing lots of important concepts of DDD.
Aggregate-Product should not have list of other Aggregate-Category (maybe this bounded-context of your project should not be implemented using DDD at all?)
You should not add objects directly into list owned by aggregate p.getCategories.add(c)
//getters, setters - those are not object-oriented…