I am trying to develop a system for managing dormitories. Since I don't have much experience with databases, I am stuck on a problem and I have a solution but I am not sure if this would be the right approach.
So I have Room and User. Each user can be accommodated in one room, but one room can accommodate more users. I would like to manage this relationship in one entity - Accommodation. Here I would have more properties, like start/end Date, etc.
I am using Hibernate to map the tables. From what I've read, persisting Collections from Java can be done in two ways, either by #OneToMany or by #ElementCollection. I am not quite sure if I should define this relationship in the Room entity or in the Accommodation entity? If I do it in the room entity then the Accommodation would hold just fk from the room/user tables?
Also, is it possible to only fetch the primary key when doing one-to-many relations instead of getting the whole object? I know that FETCH.LAZY does this, but in my Accommodation entity ideally I would want to store only Set studentsIds.
Thank you in advance.
#Table(name = "student")
#AllArgsConstructor
#Data
#Embeddable
#NoArgsConstructor
#javax.persistence.Entity
public class Student implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "username")
private String username;
#Column(name = "password")
private String password;
#Column(name = "role")
private String role;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "room", nullable = false)
private Room room_number;
}
Here is the Room entity
#javax.persistence.Entity
#Table(name = "room")
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Data
public class Room
{
#Id
#Column(name = "room_number")
private Long roomNumber;
#Column(name = "location_address")
private String locationAddress;
#Column(name = "dormitory_name")
private String dormitoryName;
}
Accommodation entity
#javax.persistence.Entity
#Table(name = "accommodation")
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Data
public class Accommodation extends Entity {
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "room_number")
private Room room_number;
#OneToMany(mappedBy = "room_number", // I am not sure about this
cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
orphanRemoval = true) private List<Student> students;
#Column(name = "date_from")
private Date dateFrom;
#Column(name = "date_to")
private Date dateTo;
}
Related
I have a Flight class and a AircraftReport class. The AircraftReport class contains an inbound flight and an outbound flight which I would both like to be mapped as a #OneToOne. How do I correctly define the relationship?
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#ToString
#Entity
#Table
public class Flight implements Serializable {
#Id
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "flight_sequence"
)
#SequenceGenerator(
name = "flight_sequence",
allocationSize = 1
)
#Column(nullable = false, updatable = false)
private Long id;
private String callsign;
private Date date;
private String origin;
private String destination;
private String registration;
private String aircraftType;
#OneToOne(mappedBy = "--what should it be mapped by here--")
private AircraftReport aircraftReport;
}
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table
public class AircraftReport implements Serializable {
#Id
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "taxsheet_sequence"
)
#SequenceGenerator(
name = "taxsheet_sequence",
allocationSize = 1
)
#Column(nullable = false, updatable = false)
private Long id;
...
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "inbound_flight_id")
private Flight inboundFlight;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "outbound_flight_id")
private Flight outboundFlight;
...
}
IMHO you don't need to use a bidirectional relationship for a OneToOne mapping. If you want the AircraftReport just get the AircraftReport and the Flight instances will come with it. Otherwise if you're not working with a report just get the Flight and be happy.
Also, I don't recommend using Cascade operations on a OneToOne, or at any time for that matter. Do you really ever want the Flight deleted?
Map to either inboundFlight or outboundFlight. You can't have one bidirectional relationship take the place of two of them. If you want two bidirectional relationships then add another to Flight.
#OneToOne(mappedBy = "inboundFlight")
private AircraftReport aircraftReportInbound;
#OneToOne(mappedBy = "outboundFlight")
private AircraftReport aircraftReportOutbound;
As you have two separate relationships from AircraftReport two Flight, you also have to separate relationships from Flight to AircraftReport.
#OneToOne(mappedBy = "inboundFlight")
private AircraftReport aircraftReportInbound;
#OneToOne(mappedBy = "outboundFlight")
private AircraftReport aircraftReportOutbound;
I've created an API that has actor, movie and category entities. Actor and movie are connected by many-to-many relationship that maps to a join table called movie_actor and category is connected with movie by one-to-many relationship.
I'm trying to write a native query that returns an integer that would represent the amount of movies from a specific category where specific actor has played so for example query would return 2 if actor played in 2 different sci-fi movies. I have no problem doing that from the database level where I can see the join table movie_actor but that table remains unaccessible in my api because it's not a separate entity. How can I create it that it automatically maps actor and movie ids as the movie_actor table ?
Here is an example code that works for me in the H2 Database:
SELECT COUNT(*) FROM MOVIE M JOIN MOVIE_ACTOR MA on M.MOVIE_ID = MA.MOVIE_ID WHERE ACTOR_ID = 1 AND CATEGORY_ID = 1
Here are my entities:
Actor:
#Entity
#Data
#Table(name = "actor")
#AllArgsConstructor
#NoArgsConstructor
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "actor_id")
private long actorId;
#Column(name = "name")
private String name;
#Column(name = "surname")
private String surname;
#Nullable
#ManyToMany(mappedBy = "actors", fetch = FetchType.EAGER)
#JsonBackReference
private List<Movie> movies = new ArrayList<>();
public Actor(String name, String surname){
this.name = name;
this.surname = surname;
}
}
Movie:
#Entity
#Data
#Table(name = "movie")
#AllArgsConstructor
#NoArgsConstructor
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "movie_id")
private long movieId;
#Column(name = "title")
private String title;
#ManyToMany
#JoinTable(
name = "movie_actor",
joinColumns = #JoinColumn(name = "movie_id"),
inverseJoinColumns = {#JoinColumn(name = "actor_id")}
)
#JsonManagedReference
private List<Actor> actors = new ArrayList<>();
#ManyToOne
#JoinColumn(name = "CATEGORY_ID")
#JsonManagedReference
private Category category;
}
So you have to make it accessible in your API. One option would be to map the intersection table movie_actor to the entity MovieActor and split ManyToMany relationship between Actor and Movie to OneToMany relationship with MovieActor, like that:
#Entity
#Data
#Table(name = "movie_actor")
#AllArgsConstructor
#NoArgsConstructor
public class MovieActor {
#EmbeddedId
private MovieActorId productOrderId = new MovieActorId();
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#MapsId("movieId")
#JoinColumn(name = "product_id", nullable = false)
private Movie movie;
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#MapsId("actorId")
#JoinColumn(name = "order_id", nullable = false)
private Actor actor;
public void addMovieActor(Movie aMovie, Actor aActor) {
movie = aMovie;
actor = aActor;
aMovie.getMovieActors().add(this);
aActor.getMovieActors().add(this);
}
}
#Embeddable
#Getter
#Setter
public class MovieActorId implements Serializable {
#Column(name = "movie_id")
private Long movieId;
#Column(name = "actor_id")
private Long actorId;
}
#Entity
#Data
#Table(name = "actor")
#AllArgsConstructor
#NoArgsConstructor
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "actor_id")
private long actorId;
#OneToMany(mappedBy = "actor")
private List<MovieActor> movieActors = new ArrayList<>();
}
#Entity
#Data
#Table(name = "movie")
#AllArgsConstructor
#NoArgsConstructor
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "movie_id")
private long movieId;
#OneToMany(mappedBy = "movie")
private List<MovieActor> movieActors = new ArrayList<>();
}
Now you can access the intersection table MovieActor inside the query. You can even add more columns to this table if you want.
I have 4 Entities, that a related to each other with #OneToMany relationships.
When I try to save Order that contains OrderItem - Orderitem has no backreference.
In the code below only important fields are showed for brevity ( usual strings and primitives are omitted ). I decided to include Dish and User Entities also.
Order:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne(fetch = FetchType.LAZY)
private User user;
#OnDelete(action = OnDeleteAction.CASCADE)
#OneToMany(
mappedBy = "order",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
orphanRemoval = true)
private List < OrderItem > orderItems;
}
Dish:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
public class Dish {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "dish")
#ToString.Exclude
private List < OrderItem > orderItems;
}
OrderItem:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
public class OrderItem {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne(fetch = FetchType.LAZY)
#ToString.Exclude
private Dish dish;
#ManyToOne(fetch = FetchType.LAZY)
private Order order;
private int quantity;
}
User:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#OneToMany(
mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List < Order > orders;
}
The problem happens when I try to save Order with Spring data JPA.
Let's print Order to see OrderItem before saving.
public Order saveOrder(Order order) {
System.out.println("SERVICE saving order " + order);
return orderRepository.save(order);
}
As you can see, orderItems backreference is null before saving ( I though spring data jpa should deal with setting it ).
SERVICE saving order Order(id=0,
orderItems=[OrderItem(id=0, quantity=2, order=null)])
Here is what I have in DB ( Order and OrderItem entities ).
In your OrderItem class, add annotation below:
#ManyToOne(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.PERSIST}, fetch=FetchType.LAZY)
#JoinColumn(name="order_id", referencedColumnName="id", nullable = false)
Order order.
One more thing, I suggest you use SEQUENCE_GENERATOR, beacause IDENTITY means: I'll create the entity with a null ID and the database will generate one for me. I don't think Postgres even supports that, and even if it does, a sequence generator is a better, more efficient choice.
The best option that I found for this is doing something like:
order.getOrderItems().forEach(orderItem -> orderItem.setOrder(order));
Before your save() call. Even though order is not persisted at this point, it seems like Hibernate can resolve the relation and the back references will be set correctly.
If you do not want to bother setting the back reference in your business logic, you can add something like this to your entity:
class Order {
...
#PrePersist
public void prePersist() {
setMissingBackReferences();
}
private void setMissingBackReferences() {
orderItems.forEach(oderItem -> {
if (oderItem.getOrder() == null) {
oderItem.setOrder(this);
}
});
}
...
}
I have Product entity and ProductRating entity, each Product can have many ProductRatings. When Product is deleted I want to have associated ratings deleted too, but nothing works so far (also orphanRemoval set to true)...
Classes:
#Getter
#Setter
#Entity
#Table(name = "PRODUCT")
public class Product extends AbstractEntity<Long> {
#Column(nullable = false)
private String name;
private String description;
#Column(nullable = false)
#Min(value = 0)
private Float cost;
#OneToMany(mappedBy = "product",
orphanRemoval = true, cascade = CascadeType.PERSIST,
fetch = FetchType.EAGER)
//#OnDelete(action = OnDeleteAction.CASCADE)
#Fetch(value = FetchMode.SELECT)
private Set<ProductRating> productRatings;
}
#Getter
#Setter
#Entity
#Table(name = "PRODUCT_RATING")
public class ProductRating extends Rating {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "product_id")
#NotNull(message = "Rating must be in context of Product")
private Product product;
}
After Product deletion ratings stay with deleted Product's ID
AbstractEntity implementation:
#Getter
#Setter
#MappedSuperclass
public abstract class AbstractEntity<I> implements Serializable {
private static final long serialVersionUID = 1700166770839683115L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID", unique = true, nullable = false)
private I id;
}
In the #OneToMany relation you need to add the cascade type delete: cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
Or if you don't mind having all cascade types you can just put: cascade = CascadeType.ALL
EDIT:
Also check the name of the Product primary key in the database.
It should match the defined in the #JoinColumn annotation of ProductRating
The default database field for the attribute id of the Product class would be product_id.
However you have defined the id in AbstractEntity as name = "ID" so the #JoinColumn should be something like: #JoinColumn(name = "ID")
My alternative approach to fix this problem is to:
On parent-side relation create method with #PreRemove annotation
in this method iterate over collection with #[One/Many]ToMany annotation and call delete(obj) method for corresponding repository on child
On child-side relation create method with #PreRemove annotation
In this method set parent to null
I have two table with many-to-one relationship. Example is, I have Office table and Employee table. One Employee belong to one Office and one Office belong to many Employee.
Office
#Entity(name = "office")
#Table(name = "office", uniqueConstraints = {#UniqueConstraint(columnNames = {"id"})})
public class Office {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "office_name", nullable = false)
private String officeName;
}
Employee
#Entity(name = "employee")
#Table(name = "employee", uniqueConstraints = {#UniqueConstraint(columnNames = {"id"})})
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "employee_name", nullable = false)
private String employeeName;
#ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
#JoinColumn(name = "office_id", referencedColumnName = "id", nullable = false)
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private Office office;
}
OfficeDto
public class OfficeDto {
private Long id;
private String officeName;
}
EmployeeDto
public class EmployeeDto {
private Long id;
private String employeeName;
private OfficeDto office;
}
With above way of defining the entity and the DTO, when I do employee.findAll(), the JSON result is also include the detail of the office data.
Is there any way that I could achieve (objective):
When do saving new employee, I just have to mention the id of the office.
When do findAll employee, I could choose whether I want to gove the id only or also with the entire object to the client.
Because, with current situation, I think I need to define two employee DTO. First one is contain the entire office data (like the code of EmployeeDto) and the second one is replace private OfficeDto office with private int office.
The second problem you can solve by projection : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections
Or just specific mapper to DTO, for mapping you can use mapstruct : http://mapstruct.org/documentation/installation/
For the first problem i found some answer in stack, but you need verify it : JPA many-to-one relation - need to save only Id