Salta el contingut

4. Relacions

1. Mapejant Relacions

Com vam esmentar a la introducció, analitzarem com mapejar els diferents tipus de relacions. Abans de començar a discutir la cardinalitat de les relacions, hem de considerar el significat d'aquestes relacions, i revisarem el concepte de direccionalitat de les relacions.

  • Unidireccional → Direm que una relació és unidireccional quan accedim a l'objecte relacionat (component) des d'un altre objecte (propietari). Per exemple, si muntem un motor en un cotxe, el lògic és que el propietari sigui el cotxe, i des d'aquest obtindrem el motor. En aquest cas, dins de l'objecte Cotxe apareixerà un objecte Motor, i el Motor no tindrà una existència pròpia.
  • Bidireccional → Són relacions en les quals els elements relacionats solen tenir el mateix pes o entitat. Per exemple, un Grup d'un institut i un Tutor. Des d'un grup té sentit conèixer el tutor, i també podem des d'un professor (el tutor), accedir al grup que tutoritza. En aquest cas, dins de l'objecte Grup tenim una referència a l'objecte Tutor i viceversa.

Avís

En aquest tipus de referències, com es pot deduir, hi ha una recursió intrínseca. Per tant, quan gestionem aquest tipus de relacions bidireccionals, tingueu molta cura de no causar bucles, ja que fins i tot una cosa tan senzilla com imprimir pot fer que el nostre programa es bloquegi i aparegui la coneguda StackOverflowException.

A partir d'ara, podríem estudiar totes les representacions amb JPA.

2. Relacions Un a Un

Per a l'explicació dels exemples, veurem el disseny i la implementació a la base de dades de cada cas i com es veu a Hibernate. Per a aquest exemple representarem una relació 1:1 entre Grup i Professor, on es pot veure que un Grup té un Tutor, i un Tutor només pot tutoritzar un Grup.

one_to_one

Primerament, la classe que és apuntada per la clau forana. Molt fàcil, perquè no necessitem fer res.

Java
@Data
@NoArgsConstructor
@Entity
@Table(name="Profesor")
public class Profesor {

  static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  private int idTeacher;

  @Column
  private String name;

  public Profesor(String name) {
      this.name = name;
  }   
}

I ara, la classe que conté la clau aliena. Aquí hem de marcar que un Grup necessita un Professor com a tutor. Vegem-ho:

Java
@Data
@NoArgsConstructor
@Entity
@Table(name = "Grupo")
public class Grupo implements Serializable {

  static final long serialVersionUID = 137L;

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long idGroup;

  @Column
  private String level;

  @Column
  private String course;

  @Column
  private int year;

  @OneToOne(cascade = CascadeType.ALL)
  @JoinColumn(
      name="id_tutor",
      referencedColumnName = "idTeacher",
      unique=true,
      foreignKey = @ForeignKey(name = "FK_GRP_TEACH"))
  private Profesor tutor;

  public Grupo(String level, String course, int year) {
      this.level = level;
      this.course = course;
      this.year = year;
  }  

}
Tingueu en compte que la classe Grupo conté un camp anomenat tutor de la classe Profesor, i:

  • @OneToOne(cascade = CascadeType.ALL) marquem aquesta relació com a 1:1. A més, especifiquem l'atribut cascade, que és el més important. El cascading és la manera de dir que quan realitzem alguna acció sobre l'entitat objectiu (Grupo), la mateixa acció s'aplicarà a l'entitat associada (Profesor). Revisem les opcions més rellevants:
  • CascadeType.ALL propaga totes les operacions. La mateixa operació que fem en l'objectiu es farà en l'associat.
  • CascadeType.PERSIST propaga només l'operació de persistència a la base de dades (guardar).
  • CascadeType.SAVE_UPDATE és de Hibernate, no de JPA, i propaga el mètode saveOrUpdate(). És molt similar a persist.
  • CascadeType.REMOVE o CascadeType.DELETE propaga l'eliminació d'entitats. Tingueu molta cura amb aquesta opció per evitar perdre dades.
  • En el @JoinColumn establim:
  • el nom de la columna a la nostra base de dades
  • el nom de la columna referenciada en l'entitat objectiu Profesor
  • unique=true per assegurar que la relació és 1:1 (un professor no pot estar relacionat amb cap altre grup)
  • [opcional] per establir el nom de la restricció de clau forana, en cas que vulgueu canviar-lo o eliminar-lo en operacions futures.

Més informació a la següent web baeldung

2.1. Un a Un bidireccional

Si volem emmagatzemar en Professor els grups que està tutoritzant, necessitem afegir una referència al Grup. Com que hem fet la clau forana en Grup, serà molt fàcil:

Java
@OneToOne(mappedBy= "tutor") 
private Grupo elGrupo;

Amb mappedBy="tutor" estem dient que a la classe Grupo existeix un camp anomenat tutor amb tota la informació sobre la relació. Tingueu en compte que no s'afegiran camps addicionals a Profesor, perquè la informació sobre la relació es troba a la taula Grupo.

3. Un a Molts

Per a aquesta explicació començarem amb el següent model, en el qual un Llibre té un Autor que l'ha escrit, i un Autor pot haver escrit diversos Llibres. En l'esquema relacional, la relació és des de idAutor en Llibres, que és clau forana a la taula Autor (ID).

one_to_many

Primer, podem decidir qui és el propietari de la relació. Realment no importa, però en diversos dissenys és molt clar, per exemple entre Estudiant i Email, on òbviament el propietari és Estudiant. Normalment hauria de ser la classe amb cardinalitat molts el propietari. Vegem l'exemple.

Java
@Entity
@Table(name="Libro")
public class Libro implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long idLibro;

    @Column
    private String titol;

    @Column
    private String tipus;

    @ManyToOne(cascade=CascadeType.PERSIST)
    @JoinColumn(name="idAutor",
                foreignKey = @ForeignKey(name = "FK_LIB_AUT" ))
    private Autor elAutor;
En aquest exemple, un Llibre té un autor (únic). Ho implementem emmagatzemant una referència a un objecte Autor, anomenat elAutor dins del nostre Llibre. Hem d'escriure la informació de la relació en aquest camp:

  • Hem de marcar aquest camp com @ManyToOne, perquè Llibre està al costat dels molts de la relació (recordeu que un Autor pot escriure diversos Llibres)
  • La clau forana serà anotada amb l'etiqueta @JoinColumn, amb diversos atributs:
  • Com que elAutor és el punt inicial de la clau forana, que apunta a la taula Author, necessitem dir el nom de la clau primària en aquesta classe. Aquest atribut és opcional, però és una bona opció per millorar el nostre codi.
  • Opcionalment, podem anomenar la restricció de la clau forana, amb un nom ben estructurat, amb l'atribut foreignKey
Java
@Entity
@Table(name="Libro")
public class Autor implements Serializable{

    static final long serialVersionUID = 137L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long idAutor;

    @Column
    private String nom;

    @Column
    private String nacionalitat;

    @OneToMany(mappedBy="elAutor",
            cascade=CascadeType.PERSIST,
            fetch = FetchType.LAZY)
    private Set<Libro> elsLlibres;

    // rest of the class

La classe Autor està en el costat un, i això significa que pot escriure molts Llibres. Per aquesta raó, emmagatzemem tots els llibres que ha escrit en un Set de llibres. Les anotacions seran:

  • Com que un Autor pot escriure molts llibres, marquem el Set de llibres com @OneToMany. Com que hem escrit l'especificació de la relació en Llibre, podríem dir que la relació està mapejada en el camp elAutor dins de la classe Llibre, amb mappedBy="elAutor" fàcilment.

Decisió

En lloc d'emmagatzemar llibres en un Set, es poden emmagatzemar en una Llista. La principal diferència és respondre a aquesta pregunta: és important l'ordre?. Si respons , has d'utilitzar una Llista. Si la resposta és no, has d'utilitzar un Set.

Espai

La relació 1:N que hem explicat és bidireccional. Això vol dir que des d'un Autor podem obtenir tots els Llibres que ha escrit, i des d'un Llibre podem obtenir l'Autor.

one_to_many_bidirectional

Podeu trobar diverses pàgines i llibres que expliquen les relacions unidireccionals 1:N. Això vol dir que amb aquest tipus d'implementació només podem viatjar en una direcció. En aquest cas, hem d'emmagatzemar només dins d'un Llibre qui és l'autor, perquè el Llibre és el propietari. Hem d'eliminar el conjunt de llibres en l'autor per obtenir una relació unidireccional.

3.1. Tipus de Càrrega Fetch

Aquest atribut sol aparèixer quan tenim una relació 1:N o N:M en una classe que té una col·lecció de classes relacionades (també es pot especificar amb un 1:1 però és menys comú). Quan Hibernate carrega un objecte, carregarà els seus atributs generals (nom, nacionalitat, etc...), però què passa amb els Llibres que ha escrit, els carrega o no?

  • DateType.EAGER → Literalment traduït com ansiós. No podem esperar, i quan es carrega l'Autor, Hibernate resoldrà la relació i carregarà tots els llibres amb totes les dades internes de cada llibre. Tenim totes les dades en el moment.
  • DateType.LAZY → Literalment com mandrós (vago), però més representatiu com càrrega mandrosa. Si carreguem l'Autor, Hibernate només carrega els atributs propis de l'Autor, sense carregar els seus Llibres. Quan intentem accedir als seus llibres des del nostre programa, Hibernate s'activa i els carrega. És a dir, en mode LAZY, les dades es carreguen quan es necessiten.

"Què farem?

Què és millor o pitjor? La resposta no és senzilla, ja que ambdós tenen pros i contres:

  • En EAGER només es fa un accés, mentre que en LAZY dos accessos o més.
  • En EAGER es carreguen totes les dades, fins i tot si no són necessàries, en LAZY només es carrega el que és necessari.

El programador ha de valorar i equilibrar la quantitat d'informació requerida en un moment donat i el cost d'accés a la base de dades.

4. Molts a Molts

En aquesta secció acabarem amb l'últim tipus de relacions que podem trobar en el model E/R, que són les relacions molts a molts. Poden aparèixer altres relacions amb cardinalitats més altes, com les relacions ternàries, però com es va estudiar en el primer any, totes elles es poden modelar amb transformacions binàries.

Dins de les relacions binàries, podem trobar dues possibilitats:

  • Relacions que simplement indiquen la relació (per exemple, que un personatge pot o no portar un cert tipus d'arma en un joc de rol) o
  • Relacions que, a més d'indicar-ho, afegeixen nous atributs. Per exemple, un actor participa en una pel·lícula en un tipus de paper: principal, secundari, etc.

En el model relacional, ambdós casos acaben sent modelats com una nova taula (amb o sense l'atribut). Si ens trobem en el segon cas, una nova taula amb els atributs que posseeix ha de ser modelada amb una classe, així que la relació N:M entre dues taules es convertirà en dues relacions un a molts 1:N i N:1 (actor-actuació i actuació-pel·lícula). Ens centrarem en el primer cas, ja que ja estem preparats per resoldre el segon.

Anem a modelar el cas típic d'un Professor que imparteix diversos Mòduls, els quals poden ser impartits per diversos professors. L'esquema és el següent:

many to many

Com podem veure, la típica taula central de la relació N:M es manté. Com es va esmentar anteriorment, la taula Docència no existirà en el model OO, ja que només serveix per relacionar els elements.

Les classes Mòdul i Professor són les següents (només es mostra la part relacionada amb la relació) triant en aquest cas Professor com el propietari de la relació:

Java
// in Professor, a set of Modulo
@ManyToMany(cascade=CascadeType.PERSIST,
            fetch=FetchType.LAZY)
@JoinTable(name="Docencia",
          joinColumns = {@JoinColumn(
            name="idProfesor",
            foreignKey = @ForeignKey(name = "FK_DOC_PROF" ))},
          inverseJoinColumns = {@JoinColumn(
            name="idModulo",
            foreignKey = @ForeignKey(name = "FK_DOC_MOD" ))})
private Set<Modulo> losModulos=new HashSet<>();
Java
1
2
3
4
5
// in Modulo.. a set of Professor
@ManyToMany(cascade = CascadeType.PERSIST,
            fetch = FetchType.LAZY,
            mappedBy = "losModulos")
private Set<Profesor> losProfesores=new HashSet<>();;

Aquesta és l'especificació més complicada, anem-hi:

  • En ambdues classes el mapeig és @ManyToMany
  • En ambdós casos indiquem com gestionem les operacions en cascada (cascade) i la càrrega dels objectes relacionats de l'altra classe (fetch)
  • A la classe propietària (Professor) es maparà un Set<Module> amb la relació que començarà des de la meva classe actual Professor \(\rightarrow\) docència \(\rightarrow\) Modulo (el tipus base del Set)
  • Amb @JoinTable s'indica que la relació enllaça amb una taula amb nom Docencia, on:
  • S'enllaçarà (joincolumns), i l'enllaç és amb @JoinColumn:
    • Començant des del camp idProfesor dins de la taula Docencia
    • Acabant a la clau primària de Profesor,
    • La FK es nomena foreignKey = @ForeignKey(name = "FK_DOC_PROF" ).
  • Tingueu en compte que els noms dins de @JoinTable són noms a la taula Docencia (existent només a la base de dades).
  • Es mapeja des de la taula Docencia a l'entitat font Modulo inversament (des del punt fins a l'origen de la fletxa):
  • Això s'aconsegueix amb inverseJoinColumns:
    • Enllaçant des del camp idModule (@JoinColumn).
    • També nomenem la FK.
  • A la classe relacionada (Modulo), que no és la propietària, simplement indiquem que la propietària és Professor, mitjançant mappedBy="losModulos".

Un codi d'exemple seria així:

Java
Profesor p1 = new Profesor("Mario Benedé");
Profesor p2 = new Profesor("Jose Fernandez");

Modulo m1 = new Modulo("Acceso a Datos");
Modulo m2 = new Modulo("Bases de Datos");
Modulo m3 = new Modulo("Programación");
Modulo m4 = new Modulo("Diseño de Interfaces");

// añadimos modulos a p1
p1.addModulo(m3);
p1.addModulo(m1);

// añadimos modulos a p2
p2.addModulo(m2);
p2.addModulo(m3);
p2.addModulo(m4);

// guardamos
laSesion.persist(p1);
laSesion.persist(p2);

i la sortida de Hibernate serà semblant a:

Bash
Hibernate: drop table if exists Modulo
Hibernate: drop table if exists Profesor
Hibernate: create table Docencia (
    idProfesor bigint not null, 
    idModulo bigint not null, 
    primary key (idProfesor, idModulo)) engine=InnoDB
Hibernate: create table Modulo (
    idModulo bigint not null auto_increment, 
    nombre varchar(255), 
    primary key (idModulo)) engine=InnoDB
Hibernate: create table Profesor (
    idProfe bigint not null auto_increment, 
    nomProfe varchar(255), 
    primary key (idProfe)) engine=InnoDB
Hibernate: alter table Docencia add constraint FK_DOC_MOD 
    foreign key (idModulo) references Modulo (idModulo)
Hibernate: alter table Docencia add constraint FK_DOC_PROF 
    foreign key (idProfesor) references Profesor (idProfe)
Hibernate: insert into Profesor (nomProfe) values (?)
Hibernate: insert into Modulo (nombre) values (?)
Hibernate: insert into Modulo (nombre) values (?)
Hibernate: insert into Profesor (nomProfe) values (?)
Hibernate: insert into Modulo (nombre) values (?)
Hibernate: insert into Modulo (nombre) values (?)
Hibernate: insert into Docencia (idProfesor, idModulo) values (?, ?)
Hibernate: insert into Docencia (idProfesor, idModulo) values (?, ?)
Hibernate: insert into Docencia (idProfesor, idModulo) values (?, ?)
Hibernate: insert into Docencia (idProfesor, idModulo) values (?, ?)
Hibernate: insert into Docencia (idProfesor, idModulo) values (?, ?)

Despres de crear les taules, Hibernate crearà les claus alienes, i llavors inserirà els registres. Primer Professor i Mòdul i seguidament la relació entre ells de Docència

Molts a Molts amb atributs

Per a una visió completa, aquest tutorial explica com fer-ho N_M amb atributs.

Anem nosaltres a iplementar un exemple complet.

TO DO