Skip to content

1 Introducción a las ORM

Introducción

En la mayoría de las aplicaciones actuales se requiere acceso al menos a una fuente de datos. Dichas fuentes son, por lo general, base de datos relacionales. En principio, esto no debe suponer ningún problema y podremos realizar operaciones y consultas sobre la base de datos SQL, pero en ciertas ocasiones podemos tener más de un fuente de datos a la vez o la fuente que tenemos pude sufrir ciertos cambios. Todo esto puede dar lugar a una refactorización de gran parte del código.

Supongamos que tenemos una fuente de datos que corresponde a una base de datos MySQL llamada students. La base de datos tiene la siguiente estructura:

+-------------+       +--------------+       +--------+
| Estudiante  |-------| Inscripción  |-------| Curso  |
+-------------+       +--------------+       +--------+
| id          |       | id           |       | id     |
| nombre      |       | idEstudiante |       | nombre |
| edad        |       | idCurso      |       | créditos|
+-------------+       +--------------+       +--------+

De todo ello, obtenemos el siguiente script:

CREATE DATABASE IF NOT EXISTS `students` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `students`;

CREATE TABLE `course` (
  `id` int(11) NOT NULL,
  `name` varchar(30) NOT NULL,
  `credits` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

INSERT INTO `course` (`id`, `name`, `credits`) VALUES
(1, 'Matemáticas', 3),
(2, 'Historia', 4),
(3, 'Programación', 5),
(4, 'Literatura', 3),
(5, 'Química', 4),
(6, 'Física', 5),
(7, 'Inglés', 3),
(8, 'Economía', 4),
(9, 'Biología', 5),
(10, 'Arte', 3);


CREATE TABLE `inscription` (
  `id` int(11) NOT NULL,
  `idStudent` int(11) NOT NULL,
  `idCourse` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

INSERT INTO `inscription` (`id`, `idStudent`, `idCourse`) VALUES
(1, 1, 1),
(2, 2, 3),
(3, 3, 2),
(4, 4, 4),
(5, 5, 5),
(6, 6, 6),
(7, 7, 8),
(8, 8, 7),
(9, 9, 9),
(10, 10, 10);


CREATE TABLE `student` (
  `id` int(11) NOT NULL,
  `name` varchar(30) NOT NULL,
  `age` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;


INSERT INTO `student` (`id`, `name`, `age`) VALUES
(1, 'Juan Pérez', 20),
(2, 'María López', 22),
(3, 'Carlos Rodríguez', 21),
(4, 'Laura García', 23),
(5, 'Pedro Sánchez', 22),
(6, 'Ana Martínez', 24),
(7, 'Miguel González', 20),
(8, 'Elena Ramírez', 21),
(9, 'Javier Fernández', 23),
(10, 'Sofía Díaz', 22);

ALTER TABLE `course`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `inscription`
  ADD PRIMARY KEY (`id`),
  ADD KEY `fk_insc_stu` (`idStudent`),
  ADD KEY `fk_insc_cour` (`idCourse`);

ALTER TABLE `student`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `course`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11;

ALTER TABLE `inscription`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11;

ALTER TABLE `student`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11;

ALTER TABLE `inscription`
  ADD CONSTRAINT `fk_insc_cour` FOREIGN KEY (`idCourse`) REFERENCES `course` (`id`),
  ADD CONSTRAINT `fk_insc_stu` FOREIGN KEY (`idStudent`) REFERENCES `student` (`id`);
COMMIT;

Proyecto JPAExample

Supongamos que vamos a crear una aplicación para poder registrar las compras que se hacen en una librería. Teniendo en cuenta las siguientes clases POJOs:

classDiagram
    class Student{
        -Integer id
        -String name
        -int age
        +Student copy(String name)
    }
    class Course{
        -Integer id
        -String name
        -int credits
        +Course copy(String name)
    }
    class Inscription{
        -Integer id
        -Student student
        -Course course
    }

    Inscription "1.." *-- "1" Student
    Inscription "1" *-- "1" Course
public class Student implements Comparable<Student>{

    private Integer id;
    private String name;
    private int age;

    public Student(Integer id, String name, int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Student(String name, int age){
        this(null, name, age);
    }

    public static Student of(ResultSet result) throws SQLException {
        return new Student(result.getInt("id"), result.getString("name"), result.getInt("age"));
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && student.id.equals(id) && name.equalsIgnoreCase(student.name);
    }

    @Override
    public int hashCode() {
        return id.hashCode() + name.hashCode() + Integer.hashCode(age);
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return id.compareTo(o.id);
    }

    public Student copy(String newName) {
        return new Student(id, newName, age);
    }
}   
public class Course implements Comparable<Course>{
    private Integer id;
    private String name;
    private int credits;

    public Course(Integer id, String name, int credits) {
        this.id = id;
        this.name = name;
        this.credits = credits;
    }

    public Course(String name, int credits) {
        this(null, name, credits);
    }

    public static Course of(ResultSet result) throws SQLException {
        return new Course(result.getInt("id"), result.getString("name"), result.getInt("credits"));
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getCredits() {
        return credits;
    }

    public void setCredits(int credits) {
        this.credits = credits;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Course course = (Course) o;
        return credits == course.credits && id.equals(course.id) && name.equalsIgnoreCase(course.name);
    }

    @Override
    public int hashCode() {
        return id.hashCode() + name.hashCode() + Integer.hashCode(credits);
    }

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", credits=" + credits +
                '}';
    }

    @Override
    public int compareTo(Course o) {
        return id.compareTo(o.id);
    }

    public Course copy(String newName) {
        return new Course(id, newName, credits);
    }
}
public class Inscription implements Comparable<Inscription>{
    private Integer id;
    private Student student;
    private Course course;

    public Inscription(Integer id, Student student, Course course) {
        this.id = id;
        this.student = student;
        this.course = course;
    }

    public Inscription(Student student, Course course) {
        this(null, student, course);
    }

    public static Inscription of(ResultSet result) throws SQLException {
        return new Inscription(result.getInt("id"), Student.of(result), Course.of(result));
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }

    public Course getCourse() {
        return course;
    }

    public void setCourse(Course course) {
        this.course = course;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Inscription that = (Inscription) o;
        return id.equals(that.id) && student.equals(that.student) && course.equals(that.course);
    }

    @Override
    public int hashCode() {
        return id.hashCode() + student.hashCode() + course.hashCode();
    }

    @Override
    public String toString() {
        return "Inscription{" +
                "id=" + id +
                ", student=" + student +
                ", course=" + course +
                '}';
    }

    @Override
    public int compareTo(Inscription o) {
        return 0;
    }
}

Además el proyecto contiene un interfaz que servirá para cada uno de los modelos de nuestras clases POJOs, en la cual se encuentras los métodos necesarios para trabajar con las consultas comunes de cada una de nuestras clases:

public interface Source<T> {
    T getById(Integer id);
    List<T> getAll();
    boolean save(T t);
    boolean update(T t);
    boolean delete(T t);
}

Es una interfaz genérica, donde T es el tipo de clase POJO con la que estamos trabajando.

Haciendo uso del patrón de diseño Singleton, crearemos una clase que trabaje con la conexión a nuestra base de datos:

public class Database {
    private static Database database;

    private Connection connection;

    private Database(){
        try{
            Class.forName(DRIVER);
            connection = DriverManager.getConnection(URL, USER, PASSWORD);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (SQLException e) {
            System.out.println(RED + "Connection to MySQL DB failed: " + e.getMessage() + RESET);
        }
    }

    public PreparedStatement query(String sql) throws SQLException {
        return connection.prepareStatement(sql);
    }

    public Statement query() throws SQLException {
        return connection.createStatement();
    }

    public static Database instance(){
        if(database == null){
            database = new Database();
        }

        return database;
    }
}

Una vez creada la clase para trabajar con la conexión creamos los modelos que implementen la interfaz que hemos creado anteriormente:

public class StudentModel implements Source<Student> {

    private final Database db;

    public StudentModel(Database db) {
        this.db = db;
    }

    @Override
    public Student getById(Integer id) {
        try(PreparedStatement statement = db.query("select * from student where id = ?")) {
            statement.setInt(1, id);
            ResultSet result = statement.executeQuery();

            if(!result.next())  return null;

            return Student.of(result);
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return null;
        }
    }

    @Override
    public List<Student> getAll() {
        final List<Student> students = new ArrayList<>();

        try(Statement statement = db.query()) {
            ResultSet result = statement.executeQuery("select * from student");

            while (result.next()){
                students.add(Student.of(result));
            }
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }

        return students;
    }

    @Override
    public boolean save(Student student) {
        try(PreparedStatement statement = db.query("insert into student (name, age) values (?, ?)")) {
            statement.setString(1, student.getName());
            statement.setInt(2, student.getAge());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }

    @Override
    public boolean update(Student student) {
        try(PreparedStatement statement = db.query("update student set name = ?, age = ? where id = ?")) {
            statement.setString(1, student.getName());
            statement.setInt(2, student.getAge());
            statement.setInt(3, student.getId());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }

    @Override
    public boolean delete(Student student) {
        try(PreparedStatement statement = db.query("delete from student where id = ?")) {
            statement.setInt(1, student.getId());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }
}
public class CourseModel implements Source<Course> {
    private final Database db;

    public CourseModel(Database db) {
        this.db = db;
    }

    @Override
    public Course getById(Integer id) {
        try(PreparedStatement statement = db.query("select * from course where id = ?")) {
            statement.setInt(1, id);
            ResultSet result = statement.executeQuery();

            if(!result.next())  return null;

            return Course.of(result);
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return null;
        }
    }

    @Override
    public List<Course> getAll() {
        final List<Course> courses = new ArrayList<>();

        try(Statement statement = db.query()) {
            ResultSet result = statement.executeQuery("select * from course");

            while (result.next()){
                courses.add(Course.of(result));
            }
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }

        return courses;
    }

    @Override
    public boolean save(Course course) {
        try(PreparedStatement statement = db.query("insert into course (name, credits) values (?, ?)")) {
            statement.setString(1, course.getName());
            statement.setInt(2, course.getCredits());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }

    @Override
    public boolean update(Course course) {
        try(PreparedStatement statement = db.query("update course set name = ?, age = ? where id = ?")) {
            statement.setString(1, course.getName());
            statement.setInt(2, course.getCredits());
            statement.setInt(3, course.getId());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }

    @Override
    public boolean delete(Course course) {
        try(PreparedStatement statement = db.query("delete from course where id = ?")) {
            statement.setInt(1, course.getId());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }
}
public class InscriptionModel implements Source<Inscription> {
    private final Database db;

    public InscriptionModel(Database db) {
        this.db = db;
    }

    @Override
    public Inscription getById(Integer id) {
        try(PreparedStatement statement = db.query("select i.id, s.*, c.* from inscription i JOIN student s on i.idStudent = s.id join course c on i.idCourse = c.id where i.id = ?")) {
            statement.setInt(1, id);
            ResultSet result = statement.executeQuery();

            if(!result.next())  return null;

            return Inscription.of(result);
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return null;
        }
    }

    @Override
    public List<Inscription> getAll() {
        final List<Inscription> inscriptions = new ArrayList<>();

        try(Statement statement = db.query()) {
            ResultSet result = statement.executeQuery("select i.id, s.*, c.* from inscription i JOIN student s on i.idStudent = s.id join course c on i.idCourse = c.id");

            while (result.next()){
                inscriptions.add(Inscription.of(result));
            }
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }

        return inscriptions;
    }

    @Override
    public boolean save(Inscription inscription) {
        try(PreparedStatement statement = db.query("insert into inscription (idStudent, idCourse) values (?, ?)")) {
            statement.setInt(1, inscription.getStudent().getId());
            statement.setInt(2, inscription.getCourse().getId());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }

    @Override
    public boolean update(Inscription inscription) {
        return false;
    }

    @Override
    public boolean delete(Inscription inscription) {
        try(PreparedStatement statement = db.query("delete from inscription where id = ?")) {
            statement.setInt(1, inscription.getId());

            return statement.executeUpdate() > 0;
        } catch (SQLException e) {
            System.out.println(e.getMessage());
            return false;
        }
    }
}

Info

Puedes encontrar este proyecto en: https://github.com/irubenboy/jpaexample

Problemas de carencia de ORM

Al usar esta estructura de proyecto, podemos evitar el error de cambio de fuente de forma sencilla, pero aún así tenemos muchos otros problemas, como por ejemplo, si queremos añadir más queries, o si quisiéramos cambiar la estructura de una clase POJO habría que cambiar también su modelo.