First I am a newbie with DBs in general so if this turns out to be a dumb question, please be a bit tolerant and generous with details in ur answer and thanks alot for any effort put in to help !!
I am having trouble designing the class structure for my code and will welcome any suggestions concerning the matter. I have 3 data classes
1) School
2) Teacher
3) Workshop
A School Entity has a List<Teacher> and a List<Workshop> they hosted.
A Workshop Entity has a single Host School Entity and a List<Teacher> of participants.
A Teacher Entity had a List<Workshop> they attended and an employment history List<School> (not showing school List in the code below, as I am leaving it till later when I figure simpler things first, but its a target)
Every single school in a given city will be assigned one entry and no more, everything else from Teacher and Workshop has to reference that single Entry.
Every single teacher in a given city will be assigned on entry/ account and no more, so everything else has to reference it
I know I can work with IDs, but that will involve tons of duplication and I wont be able to get an object from within another object, the entry will be a Long and and even if I make methods to automate the whole thing, this will eat up my query quotas very fast( would really love to avoid that)
I would like to be able to query for a single entity(School or Teacher or Workshop) and be able to see all the other Entity Lists associated .
Plus,
Teachers move around, so I must be able to remove a Teacher (as a Child Entity) from one School and add it to another, while maintaining the record of which School hosted their previous workshops.
I have done this much on my own
#Entity
public class School
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long ID;
#OneToMany(cascade = CascadeType.ALL, mappedBy="school")
private List<Teacher> teachers;
#OneToMany(cascade = CascadeType.ALL,mappedBy="school")
private List<Workshop> workshops;
// Getters and Setters and some methods
}
#Entity
public class Teacher
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
#ManyToOne
private School school;
private List<Workshop> Workshops;
// Getters and Setters and some methods}
#Entity
public class Workshop
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
#ManyToOne
private School school;
private List<Teacher> Participants;
// Getters and Setters and some methods}
Currently I am able to assign as many teachers and workshops to a particular School entity, however, I cannot assign teacher entities (that are already assigned to a School - key point here) to a Workshop. This is the Error I always get
Detected attempt to establish Workshop(no-id-yet) as the parent of School(6148469022523392)/Teacher(5585519069102080) but the entity identified by School(6148469022523392)/Teacher(5585519069102080) is already a child of School(6148469022523392). A parent cannot be established or changed once an object has been persisted.
The order of which is which varies depending on which Entity got created and persisted first.
Thanks alot and awaiting any advice and consultation ... I am not looking for a complete solution, I just need someone to point out how this could be done ( I am sure I am not the first to get stuck here and I am sure that generous experts will help out)
Now look I have some points : Three Entites you have : School , Workshop , Teacher.
School have oneTomany with - workshop & teacher. So once we are persisting school we'll have entries in both the tables - workshop & teacher.And also you wanted to have A Workshop Entity has a single Host School so we achieved that also while persisting as per below code.
Your School Entity :
#Entity
public class School
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long ID;
#OneToMany(cascade = CascadeType.PERSIST, mappedBy="school")
private List<Teacher> teachers;
#OneToMany(cascade = CascadeType.PERSIST,mappedBy="school")
private List<Workshop> workshops;
// Getters and Setters and some methods
}
Your Teacher Entity:
#Entity
public class Teacher
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="key")
private School school;
// Getters and Setters and some methods}
Your WorkShop Entity:
#Entity
public class Workshop
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="key")
private School school;
and then :
em.getTransaction().begin();
School schoolObj = new School();
schoolObj.setName("School 1");
List <Teacher> teachers = new ArrayList<Teacher>();
List <Workshop> workshopList = new ArrayList<Workshop>();
Teacher teacher = new Teacher();
teacher.setSchool(schoolObj);
teacher.setName("Teacher 1");
teachers.add(teacher);
Teacher teacher1 = new Teacher();
teacher1.setSchool(schoolObj);
teacher1.setName("Teacher 2");
teachers.add(teacher1);
teacher teacher2 = new Teacher();
teacher2.setSchool(schoolObj);
teacher2.setName("Teacher 3");
teachers.add(teacher2);
Workshop ws = new Workshop();
ws.setSchool(schoolObj); //By this you setted schoolObj in workshop entity
ws.set(some attribute);
workshopList.add(ws);
school.setTeachers(teachers); //By this you setted teachers through school ,i.e., entry came in teachers table too.
school.setWorkshops(workshopList); //By this you setted workshopList through school ,i.e., entry came in workshop table too.
em.persist(schoolObj);
em.getTransaction().commit();
em.close();
Now you mentioned that: A WorkShop Entity also has a List of participants .And A Teacher Entity had a List they attended and an employment history List. This shows you are having ManyToMany between Workshop & Teacher. As in your case Workshop has List of teachers and Teachers also have List of Workshop's.So here you will be requiring a joining table to lonk this ManyToMany relationship. Similarly, between teacher & school you have ManyToMany as both have List of one other.So here also you will be needing joining table.To learn more about this click here.
Hence to set this ManyToMany relationship you have to link through a joining table not by persisting here as it will clash then. And if you want to fetch data as per this ManyToMany relationship then you have make a separate query.
Hope this help !
Related
I'm trying to insert an row into my database. I have following sql setup (its just an example):
Table person:
(id, name)
Table person_street:
(person_id, street_id)
Table street
(id, name)
This should be a many to many relation. This example doesn't make sense, but I think you'll understand the idea.
Now I have these entities in java
#Entity
#Table(name = "person")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private long id;
#JoinTable(
name = "person_street",
joinColumns = #JoinColumn(name = "person_id"),
inverseJoinColumns = #JoinColumn(name = "street_id")
)
#ManyToMany
private List<Street> streets;
}
This is a good solution to work with my person objects (after reading them from my database).
But I have problems while inserting/creating person objects.
I want to create a new person in my frontend. It calls an REST interface at my backends side with String name, List<Long> streets.
Now I want to insert this new person (with the given name and streets) into my database.
But I don't want to do a select for all List<Long> streets on my street table. There is no need to change any value of the street objects. I just want to insert the link between the new person and the existing streets (in the table person_street).
What is the easiest way to do that?
Can I use my Person class for this, or does I need a new different class.
T
you can add this method to the Person class
public void addStreets(Street street) {
if(streets==null) {
streets=new ArrayList<Street>();
}
streets.add(street);
}
and after that, you get the street by the id from the street table and added to the corresponding person which you are getting from the front end after that you save the whole person.
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 have an object called Lecture and it has 2 fields which are both of type Teacher
class Lecture{
private Teacher teacher;
private Teacher subsTeacher;
}
Lets assume a Teacher may teach many lectures. So there is a one to many relation between Teacher -> Lecture
Now in Teacher class I have:
#OneToMany(mappedBy = "teacher")
#Cascade(value = {org.hibernate.annotations.CascadeType.DETACH,
org.hibernate.annotations.CascadeType.LOCK})
public Set<Lecture> getLectures() {
return lectures;
}
However, this will not return me the Lectures the teacher is subsTeacher to. But I need them as well.
How can I achieve this?
Table structure:
Table: Lecture
Columns: id, teacher_id, substeacher_id
Table Teacher:
Columns: id, name
For this you need to filter lectures according to teacher/subteacher and manually set to the lectures list. Otherwise you need to do separate mapping. if you need to save the lectures with the teacher you should add separate mapping and use accordingly.
#OneToMany(mappedBy = "subsTeacher", cascade = {CascadeType.ALL})
private Set<Lecture> subLectures;
I've been searching for days but can't seem to find the answer.
Given this many to many (employee/meeting)
#Entity
#Table(name="EMPLOYEE")
public class Employee {
#Id
#Column(name="EMPLOYEE_ID")
#GeneratedValue
private Long employeeId;
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name="EMPLOYEE_MEETING",
joinColumns={#JoinColumn(name="EMPLOYEE_ID")},
inverseJoinColumns={#JoinColumn(name="MEETING_ID")})
private Set<Meeting> meetings = new HashSet<Meeting>();
}
#Entity
#Table(name="MEETING")
public class Meeting {
#Id
#Column(name="MEETING_ID")
#GeneratedValue
private Long meetingId;
#ManyToMany(mappedBy="meetings")
private Set<Employee> employees = new HashSet<Employee>();
}
I can add employees to a meeting and it shows up in the employee_meeting table.
When I do get a meeting object and delete it, it's also gone from the join table but remains in the employee set... Is this the expected behaviour?
Here's how I would remove a meeting object
session.delete(meeting);
transaction.commit();
At this point it's gone from the table.
Thanks!
Yes this is correct behaviour. If you have many-to-many relationship, then you need to delete it manually. Please refer this link for hibernate collection mapping strategy
I would like to change a concrete superclass to one of its subclass. I've included an example below:
#Entity
#Table(name = "employees")
#Inheritance(strategy = InheritanceType.JOINED)
public class Employee {
#Id
#Column(name = "id")
private String id;
public Employee( String id ) {
this.id = id;
}
...
}
#Entity
#Table(name = "managers")
#PrimaryKeyJoinColumn(name = "id", referencedColumnName = "id")
public class Manager extends Employee {
public Manager( String id ) {
super(id);
}
...
}
#Entity
#Table(name = "history")
public class History {
...
/**
*
*/
#ManyToOne
#JoinColumn(name = "employee_id")
private Employee employee;
...
}
The three classes I'm working with are Employee, Manager and History. All Managers are Employees, but not all Employees are Managers. All Employees (and Managers) have a History. Employees may be promoted to Management. When this happens an Employee's history should be retained by keeping their Employee ID the same. This will allow a Manager's History through employment to be easily found.
Implementing the promotion operation is complicated by constraints withing the database: the database will not allow removing the old Employee and creating a new Manager with the same ID without removing all of the History objects by cascading operation - which Human Resources won't allow, otherwise my job would be easy!
Is it possible to add or attach the Manager (new managers) row to an existing Employee without resorting to custom SQL operation?
I've tried the following solutions without success:
public void promote( Employee employee ) {
/* copy over the ID of the employee to the manager. Will not work because employee with ID already exists */
Manager manager = new Manager(employee.getId());
this.entityManager.persist( manager );
}
... or ...
public void promote( Employee employee ) {
/* detach the employee then merge. Will not work: fails, again, with a NonUniqueObject Exception */
this.entityManager.detach( employee );
Manager manager = new Manager(employee.getId());
this.entityManager.merge( manager );
}
How can I get this done? Am I even on the right track with detach() and merge()?
Is this possible in Hibernate/JPA?
Any ideas will be helpful at this point as I'm stumped!
Aaron
As you're no doubt starting to see, you're rowing against the wind here. Unfortunately, it looks like you have a classic example of using inheritance to represent role. Both Hibernate--an Object-Relational Mapper--and Java--an Object-oriented language--are going to fight you on this one because your object model is wrong. I'd say your best bet is to fix it now with a refactoring and data migration. The end result should be that everyone is an Employee, and some Employees have some kind of "manages" relationship with one or more departments or other Employees.
I ran into a similar situation and I don't believe a flawed data model is to blame since the data model matches the real world model quite well.
You can insert the subclass record manually to achieve the desired result. Sometimes it's requisite to go around Hibernate to get something done, so this is what I did in this case as a workaround:
sessionFactory.getCurrentSession().createSQLQuery(
"insert into manager (employee_id, rank) " +
"values (:employeeId, :rank) ")
.setParameter("employeeId", employeeId)
.setParameter("rank", rank)
.executeUpdate();