JEE Back-end

Retours sur les exercices sur les mappings Hibernate

Comment factoriser les Repository ?

public interface Repository<T,K extends Serializable> {
   
   Class<T> clazz();
   
   default T create(T toBePersisted){
       return inTransaction(em ->{
           em.persist(toBePersisted);
           return toBePersisted;
       });
   }
   
   default boolean delete(K toBeDeletedId){
   	...
   }
   
   default T update(T toBeUpdated){
   	...
   }
   
   default Optional<T> get(K id){
   	...
   }
   
   default List<T> getAll(){
   	...
   }
}

Comment factoriser les Repository ? (2)

public class StudentRepository implements Repository<Student,Long> {

    @Override
    public Class<Student> getClazz() {
        return Student.class;
    }

    ...

}

Comment factoriser les Repository ? (3)

On peut éviter de passer explicitement l'Id pour la suppression.

public interface WithId<K> {
    K getId();
}
public interface Repository<T extends WithId<K>, K extends Serializable> {
   
   ...

   default boolean delete(T toBeDeleted){
      Objects.requireNonNull(toBeDeleted.getId());
      return inTransaction(em ->{
         var obj = em.find(getClazz(),toBeDeleted.getId());
         if (obj==null) { return false;}
         em.remove(obj);
         return true;
      });
    }    
}

Transient vs the world

Si l'on veut faire les choses proprement, il faut pouvoir distinguer les objets qui n'ont jamais eu de clé en BD (transients) des autres.

public class Student {

    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;
    ...
}

Transient : id == null.

Transient vs the world

En bonus, on peut faire une implémentation assez satisfaisante (cf. cours précédent) de equals et hashcode.

Les objets transients (i.e., sans id) sont considérés comme tous différents. Les autres objets sont comparés par leur id.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Student)) return false;

    Student student = (Student) o;
    if (id == null) return false;
    return id.equals(student.id);
}

@Override
public int hashCode() {
    return id != null ? id.hashCode() : super.hashCode();
}

Mapping uni-directionnel

public class Student implements WithId<Long> {

    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;
    @Embedded
    private Address address;

    @OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
    @JoinColumn(name = "phoneNumber_id")
    private PhoneNumber phoneNumber;

    @ManyToOne(fetch = FetchType.LAZY) // pq pas de cascade PERSIST ?
    private University university;

    @ManyToMany
    private Set<Lecture>  lectures;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "student_id")
    @OrderBy("date")
    private List<Remark> remarks;    

    ...
}

removeRemark

public void removeRemark(Student student,Remark remark){
        Objects.requireNonNull(student.getId());
        Objects.requireNonNull(remark.getId());
        inTransaction(em -> {
            var q="SELECT s FROM Student s LEFT JOIN FETCH s.remarks WHERE s.id = :id";
            var query=em.createQuery(q,Student.class);
            query.setParameter("id",student.getId());
            var studentPersistent = query.getSingleResult();
            if (studentPersistent == null){
                throw new IllegalStateException("No student with id "+student.getId());
            }
            var remarkPersistent = em.find(Remark.class,remark.getId());
            if (remarkPersistent==null){
                throw new IllegalStateException("No remark with id" +remark.getId());
            }
            em.remove(remarkPersistent);
            studentPersistent.getRemarks().remove(remarkPersistent);

        });
}

Sans la ligne surlignée, il ne se passe rien! Pourquoi ?

Une version plus efficace mais subtile

Objects.requireNonNull(student.getId());
Objects.requireNonNull(remark.getId());
inTransaction(em -> {
    var remarkPersistent = em.find(Remark.class,remark.getId());
     ...
     em.remove(remarkPersistent);
 });    

Elle ne marche que parce que l'EntityManager est vide et parce qu'on à fait un join column.

Suppression d'un cours FAUX !

public boolean deleteWrong(Lecture lecture) {
				...
        return inTransaction(em -> {
           var lectureToBeDeleted = em.find(Lecture.class, lecture.getId());
           ...
           var q = "SELECT s FROM Student s LEFT JOIN FETCH s.lectures l WHERE l.id = :id";
           var students = em.createQuery(q,Student.class)
                                    .setParameter("id", lectureToBeDeleted.getId())
                                    .getResultList();
           for (var student : students){
               student.getLectures().remove(lectureToBeDeleted);
           }
           em.remove(lectureToBeDeleted);
           return true;
        });
    }

Supprime tous les cours des Student de lecture.

La version sans le FETCH est correcte mais fait un select par étudiant qui suit le cours.

Suppression d'un cours (2)

public boolean delete(Lecture lecture) {
        Objects.requireNonNull(lecture.getId());
        return inTransaction(em -> {
            var lectureToBeDeleted = em.find(Lecture.class, lecture.getId());
            ...
            var q = "SELECT s FROM Student s LEFT JOIN FETCH s.lectures WHERE :lecture MEMBER OF s.lectures";
            var students = em.createQuery(q,Student.class)
                    .setParameter("lecture", lectureToBeDeleted)
                    .getResultList();
            for (var student : students){
                student.getLectures().remove(lectureToBeDeleted);
            }
            em.remove(lectureToBeDeleted);
            return true;
        });
}	

A retenir

La norme JPA impose de toujours maintenir le graphe des objets dans un état cohérent.