Herramientas de usuario

Herramientas del sitio


clase:daw:arquitectura:layers

02 - Arquitectura por capas

La arquitectura de capas es un patrón de diseño de software que organiza el sistema en diferentes niveles o capas. Cada capa tiene una responsabilidad clara y definida, y las capas interactúan entre sí de manera jerárquica. Las capas superiores utilizan los servicios ofrecidos por las capas inferiores, pero las capas inferiores no dependen de las superiores.

Se caracteriza por la separación de responsabilidades, donde cada capa está encargada de una funcionalidad específica del sistema. Esto permite una mejor organización del código y facilita la mantenibilidad y escalabilidad del mismo.

Además, presenta ventajas como la reutilización de código y la posibilidad de realizar pruebas de manera más efectiva. Sin embargo, también tiene desventajas, como una posible sobrecarga en la comunicación entre capas y una complejidad adicional en la gestión de las interacciones.

Vamos a desarrollar una aplicación web sencilla que servirá como ejemplo para ilustrar los conceptos y prácticas de la arquitectura de capas. Esta aplicación estará enfocada en la gestión de una librería, donde implementaremos las distintas capas: presentación, dominio y persistencia. A medida que avancemos, utilizaremos este ejemplo para mostrar cómo se estructuran y se comunican las capas, así como para resaltar las decisiones de diseño que impactan la calidad y la mantenibilidad del software.

Para el ejemplo, usaremos la siguiente base de datos.

Capas

Las capas básicas de la arquitectura de la aplicación web son la capa de presentación, la capa de dominio y la capa de persistencia.

La capa de presentación es responsable de interactuar con el usuario. Aquí es donde se manejan las solicitudes y respuestas, y se presenta la información de una manera comprensible. En esta capa, se implementan los controladores que gestionan la lógica de la interfaz y coordinan la interacción entre el usuario y el sistema.

La capa de dominio contiene la lógica de negocio de la aplicación. Está compuesta por los servicios que procesan la información y las entidades del modelo que representan los datos fundamentales. Esta capa se encarga de las reglas de negocio y la validación de los datos antes de que sean enviados a la capa de persistencia.

La capa de persistencia se encarga de la gestión del almacenamiento de datos. Aquí se implementan los repositorios que facilitan la interacción con los datos, permitiendo realizar operaciones de creación, lectura, actualización y eliminación (CRUD) sobre las entidades del modelo.

Además de estas capas básicas, es posible incorporar más capas según las necesidades de la aplicación. Por ejemplo, se pueden incluir capas de servicio adicional para la integración con servicios externos, capas de presentación para distintos tipos de interfaces (como API REST y aplicaciones web), o capas de configuración y seguridad. Esta flexibilidad permite adaptar la arquitectura a los requisitos específicos de cada proyecto.

Modelos de datos

Antes de implementar las capas de la arquitectura, es fundamental definir los modelos de la aplicación, que representan los datos con los que trabajaremos. En el contexto de nuestra aplicación de librería, necesitaremos modelos que reflejen las entidades clave del dominio, como Book, Author, Publisher

Cada modelo debe encapsular las propiedades y comportamientos relevantes, y definir claramente los atributos que representan el estado de cada entidad. Por ejemplo, el modelo Book podría incluir propiedades como isbn, title, author, publisher, entre otros.

Es importante diferenciar entre los modelos de la capa de dominio y los modelos de persistencia, que corresponden, por ejemplo, a las tablas en una base de datos relacional. Los modelos de dominio son representaciones de las entidades en el contexto de la lógica de negocio, mientras que los modelos de persistencia son diseñados específicamente para interactuar con el almacén de datos (como una base de datos). Esta separación permite mantener la independencia de la lógica de negocio respecto a los detalles de implementación del almacen de datos, facilitando así el mantenimiento y la evolución del sistema.

Este enfoque no solo proporciona una base sólida para las capas de dominio y persistencia, sino que también garantiza que la lógica de negocio y las interacciones entre entidades estén claramente definidas desde el principio. Una vez que hayamos definido los modelos, podremos avanzar en la implementación de la lógica de negocio en la capa de dominio.

Por ahora, vamos a crear 5 modelos: Book, Author, Category, Genre y Publisher:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Author {

    private long id;
    private String name;
    private String nationality;
    private String biography;
    private int birthYear;
    private int deathYear;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Category {

    private long id;
    private String name;
    private String slug;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Genre {

    private long id;
    private String name;
    private String slug;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Publisher {

    private long id;
    private String name;
    private String slug;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {

    private String isbn;
    private String title;
    private String synopsis;
    private BigDecimal price;
    private float discount;
    private String cover;
    private Publisher publisher;
    private Category category;
    private List<Author> authors;
    private List<Genre> genres;
}

Durante la aplicación, utilizaremos Lombok, una biblioteca que simplifica la creación de clases Java al reducir la cantidad de código boilerplate. En los modelos, las etiquetas usadas son:

  • @Data: Esta anotación genera automáticamente los métodos getters y setters, así como toString(), equals() y hashCode(), lo que permite un manejo más fácil de los datos dentro de la clase. También incluye @RequiredArgsConstructor, que crea un constructor para los atributos que son finales o requeridos.
  • @NoArgsConstructor: Esta anotación genera un constructor sin parámetros, lo cual es útil para instanciar objetos de la clase sin necesidad de proporcionar todos los atributos de inmediato.
  • @AllArgsConstructor: Esta anotación crea un constructor que toma como parámetros todos los atributos de la clase, permitiendo inicializar un objeto con todos sus valores desde el momento de su creación.

Capa de presentación

La capa de presentación, también conocida como la capa de controladores, es responsable de interactuar con el usuario y gestionar las solicitudes y respuestas de la aplicación. En esta capa, los controladores actúan como intermediarios entre la interfaz de usuario y la lógica de negocio, manejando la entrada del usuario y coordinando la interacción con las capas inferiores.

Los controladores reciben las solicitudes HTTP, procesan los datos y devuelven respuestas adecuadas. Utilizan los servicios de la capa de dominio para llevar a cabo la lógica de negocio necesaria. En el contexto de nuestra aplicación de librería, podríamos tener controladores para gestionar las operaciones de libros, autores y usuarios.

Es común utilizar anotaciones de Spring en esta capa, como:

  • @RestController: Esta anotación indica que la clase es un controlador y que las respuestas de los métodos se serializan automáticamente en formato JSON o XML.
  • @RequestMapping: Se utiliza para mapear las solicitudes HTTP a los métodos del controlador.
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping: Estas anotaciones específicas permiten definir los métodos que manejarán las solicitudes HTTP correspondientes a las operaciones de lectura, creación, actualización y eliminación, respectivamente.

Por ejemplo, nuestro BookController podría quedar así:

@RestController
@RequestMapping(BookController.URL)
@RequiredArgsConstructor
public class BookController {

    public static final String URL = "/books";

    private final BookService bookService;

    @GetMapping
    public List<Book> getAll() {
        return bookService.getAll();
    }

    @GetMapping("/{isbn}")
    public Book findByIsbn(@PathVariable String isbn) {
        return bookService.findByIsbn(isbn);
    }
}

Los métodos getAll() y findByIsbn() manejan las solicitudes para obtener todos los libros y para encontrar un libro específico por su ISBN, respectivamente. Ambos métodos utilizan el servicio BookService para realizar la lógica de negocio, manteniendo así una clara separación entre la presentación y la lógica del dominio. Por ahora, dejaremos la inyección de dependencias a Spring, para lo cual necesitamos convertir los servicios en Beans, añadiendo la anotación @Service a nuestros servicios.

Es importante notar que al añadir @Service en nuestros servicios para convertirlos en un Bean de Spring, estamos rompiendo uno de los principios básicos de una arquitectura limpia, que aboga por la independencia de las capas interiores respecto a los detalles de implementación de los frameworks. Sin embargo, a medida que avancemos, iremos modificando las capas para acercarnos a ese objetivo.

Capa de dominio

Aquí, además de los modelos creados anteriormente, estarán los servicios que implementan las operaciones fundamentales relacionadas con el dominio, como la gestión de libros, autores, editoriales…

Para nuestro ejemplo, crearemos una interfaz BookService y su implementación BookServiceImpl:

public interface BookService {
    List<Book> getAll();

    Book findByIsbn(String isbn);
}

@Service
@RequiredArgsConstructor
public class BookServiceImpl implements BookService {

    private final BookRepository bookRepository;

    @Override
    public List<Book> getAll() {
        return bookRepository.getAll();
    }

    @Override
    public Book findByIsbn(String isbn) {
        return bookRepository.findByIsbn(isbn).orElseThrow(() -> new RuntimeException("Book not found"));
    }
}

La interfaz define los métodos que deben implementarse para la gestión de libros. Al utilizar interfaces, logramos una serie de ventajas:

  • Abstracción: Separa la definición de la lógica de negocio de su implementación concreta, permitiendo que el código cliente (como los controladores) dependa de la interfaz en lugar de una implementación específica. Esto facilita la adaptación de la implementación sin afectar a las clases que dependen de ella.
  • Flexibilidad: Permite crear múltiples implementaciones de la misma interfaz. Esto es útil para pruebas unitarias, donde podemos crear implementaciones simuladas (mocks) para probar los controladores sin necesidad de acceder a la lógica de negocio real.
  • Mantenibilidad: Facilita el mantenimiento del código, ya que los cambios en la implementación de un servicio no afectan la interfaz. Esto reduce el riesgo de introducir errores en otras partes del sistema.

En cuanto a la implementación del servicio BookServiceImpl:

  • @Service: La anotación se utiliza para indicar que BookServiceImpl es un Bean de Spring, lo que permite que Spring maneje la inyección de dependencias.
  • @RequiredArgsConstructor: Utiliza Lombok para generar un constructor que inyecta la dependencia de BookRepository, que es responsable de acceder a los datos de los libros.

Los métodos getAll() y findByIsbn() implementan la lógica de negocio para obtener todos los libros y buscar un libro específico por su ISBN, utilizando el repositorio BookRepository para interactuar con la base de datos. Como antes, para que la inyección de dependencias de Spring funcione correctamente, necesitaremos convertir nuestros repositorios en Beans.

Capa de persistencia

La capa de persistencia es responsable de gestionar la interacción con los datos (en nuestro caso, la base de datos), permitiendo el almacenamiento y recuperación de datos. En esta implementación, utilizaremos spring-boot-starter-jdbc, que incluye JdbcTemplate, facilitando la manipulación de datos en la base de datos sin necesidad de un ORM como JPA (ya lo añadiremos más adelante).

Para añadir la dependencia de Spring Boot Starter JDBC en el archivo pom.xml, debemos incluir lo siguiente:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    <version>3.3.2</version>
</dependency>

Además, es necesario configurar los parámetros de conexión a la base de datos. Esto se puede hacer en el archivo application.properties, donde se definen los detalles de conexión:

spring.datasource.url=jdbc:mariadb://localhost:3306/bookstore
spring.datasource.username=root
spring.datasource.password=root

  • spring.datasource.url: Especifica la URL de conexión a la base de datos, en este caso, a una base de datos MariaDB llamada bookstore.
  • spring.datasource.username: El nombre de usuario para conectarse a la base de datos.
  • spring.datasource.password: La contraseña correspondiente al usuario de la base de datos.

Con esta configuración, podemos utilizar JdbcTemplate para realizar operaciones de lectura y escritura en la base de datos. A continuación, definimos la interfaz de repositorio BookRepository, que proporcionará métodos para interactuar con los datos de los libros:

public interface BookRepository {
    List<Book> getAll();

    Optional<Book> findByIsbn(String isbn);
}

Esta interfaz define los métodos que se utilizarán para acceder a los datos relacionados con los libros. Los métodos incluyen:

  • getAll(): Devuelve una lista de todos los libros disponibles en la base de datos.
  • findByIsbn(String isbn): Busca un libro específico por su ISBN y devuelve un Optional<Book>, permitiendo manejar la posibilidad de que no se encuentre un libro con el ISBN dado.

Para implementar la interfaz BookRepository, necesitaremos utilizar mapeadores que nos permitan convertir los resultados obtenidos de la base de datos (representados como ResultSet) a nuestros modelos de dominio (Book).

Los mapeadores son esenciales porque JdbcTemplate devuelve los resultados en forma de ResultSet, que no coincide directamente con nuestros modelos de dominio. Por lo tanto, debemos definir un proceso de mapeo que transforme cada fila del ResultSet en una instancia de nuestro modelo Book. Esto asegurará que los datos se gestionen correctamente y se mantenga la integridad del modelo de dominio a lo largo de la aplicación.

Para llevar a cabo el mapeo de los resultados de la base de datos a nuestros modelos de dominio, creamos una clase de mapeador, como el siguiente ejemplo de AuthorRowMapper:

public class AuthorRowMapper implements RowMapper<Author> {

    @Override
    public Author mapRow(ResultSet rs, int rowNum) throws SQLException {
        Author author = new Author();
        author.setId(rs.getInt("id"));
        author.setName(rs.getString("name"));
        author.setNationality(rs.getString("nationality_es"));
        author.setBiography(rs.getString("biography_es"));
        author.setBirthYear(rs.getInt("birth_year"));
        author.setDeathYear(rs.getInt("death_year"));
        return author;
    }
}

  • RowMapper<Author>: La clase AuthorRowMapper implementa la interfaz RowMapper de Spring, especificando que convertirá los datos de las filas del ResultSet en instancias del modelo Author. Esta interfaz permite definir un método, mapRow, que recibe el ResultSet y el número de fila actual, y debe devolver una instancia del tipo indicado (en este caso, Author).
  • mapRow: Dentro de este método, se extraen los datos del ResultSet utilizando sus métodos específicos, como getInt y getString, y se asignan a las propiedades del objeto Author. Finalmente, se devuelve el objeto Author mapeado.

Es importante notar que, por ahora, no estamos teniendo en cuenta los diferentes idiomas para los campos como el nombre y la biografía. En este ejemplo, devolvemos los nombres y descripciones en español (nationality_es, biography_es). Esta decisión se puede modificar más adelante para soportar múltiples idiomas, conforme la aplicación evolucione.

El resto de los mapeadores que implementaremos, con excepción de BookRowMapper, seguirán una estructura similar a la de AuthorRowMapper. Cada uno de estos mapeadores se encargará de convertir las filas del ResultSet en instancias de sus respectivos modelos de dominio. Utilizaremos la misma lógica para extraer los datos del ResultSet y asignarlos a las propiedades correspondientes de los modelos, asegurando así la coherencia en el proceso de mapeo. Esto facilitará el manejo de los datos a medida que avancemos en la implementación de la capa de persistencia.

Al implementar el BookRowMapper, nos enfrentamos a una problemática relacionada con las relaciones entre modelos en nuestra aplicación. En nuestro caso, el modelo Book incluye referencias a otros modelos, como Publisher y Category. Esto presenta dos escenarios que debemos considerar al mapear un libro:

  • Listado de Libros: Cuando queremos obtener una lista de libros, a menudo solo necesitamos datos básicos como el título, la sinopsis… En este caso, no es necesario cargar todos los datos asociados, como el editor o la categoría, lo que podría afectar el rendimiento de la aplicación. La idea es simplificar la información presentada y reducir la carga de datos innecesarios.
  • Detalles de un Libro: Cuando se solicita información detallada sobre un libro específico, es importante incluir todos los datos relevantes, incluyendo la información del editor, la categoría, autores… En este escenario, el mapeo debe incluir la creación de esas instancias, y asignarlas al objeto Book.

Además, al diseñar las consultas en la base de datos, podemos asegurarnos de que recuperamos solo la información necesaria para cada uno de estos escenarios, evitando así cargas excesivas y manteniendo un buen rendimiento en la aplicación.

La problemática es diferente si tenemos relaciones 1-N (uno a muchos), donde podemos traer todos los datos asociados con un simple JOIN, dado que la tabla books tendrá claves ajenas hacia publishers y categories. Por otro lado, si tratamos con relaciones N-M (muchos a muchos), como en el caso de los autores y géneros, necesitaremos acceder a una tabla intermedia para construir los listados necesarios, lo que añade complejidad al mapeo.

Comenzaremos abordando primero las relaciones 1-N, específicamente las relacionadas con Publisher (editoriales) y Category (categorías). Estas relaciones son más simples de manejar, ya que la tabla books contendrá claves foráneas que apuntan a las tablas de publishers y categories. Esto nos permitirá realizar consultas utilizando JOIN para obtener toda la información necesaria de manera eficiente.

Para abordar esta problemática, podemos considerar implementar dos mapeadores distintos: uno que solo maneje los datos básicos del libro para la lista y otro que incluya todas las propiedades y relaciones asociadas cuando se soliciten los detalles de un libro. Alternativamente, podemos optar por recuperar siempre la información de los modelos asociados, dependiendo de las necesidades específicas de la aplicación.

Sin embargo, las soluciones propuestas anteriormente, aunque efectivas, presentan algunas desventajas. Por un lado, traer datos innecesarios puede afectar el rendimiento, y crear múltiples mapeadores puede complicar el código.

Una alternativa a estas soluciones es implementar mapeadores que verifiquen la existencia de ciertos campos en el ResultSet. Por ejemplo, si el campo publishers.id está presente en el resultado, podemos asumir que la consulta se ha realizado utilizando un JOIN para incluir información de la tabla de publishers. De este modo, el mapeador puede adaptar su comportamiento en función de los datos disponibles, permitiendo una mayor flexibilidad y evitando la necesidad de crear múltiples mapeadores para diferentes escenarios. Esto facilitará la gestión de las relaciones y mantendrá el código más limpio y mantenible.

Una solución efectiva para abordar el problema de verificar la existencia de columnas en el ResultSet es crear nuestra propia interfaz, CustomRowMapper, que extienda la funcionalidad de RowMapper. Esta interfaz incluirá un método por defecto que intentará localizar una columna específica en el ResultSet utilizando el método findColumn. Si la columna no existe, se capturará la excepción y se devolverá false. De esta forma, podemos determinar si una columna está presente sin interrumpir la ejecución del código.

public interface CustomRowMapper<T> extends RowMapper<T> {

    default boolean existsColumn(ResultSet rs, String columnName) {
        try {
            rs.findColumn(columnName);
            return true;
        } catch (SQLException e) {
            return false;
        }
    }
}

Luego, en el mapeador de libros, podemos utilizar esta interfaz para comprobar la existencia de los campos de Publisher y Category antes de intentar asignarles valores:

public class BookRowMapper implements CustomRowMapper<Book>{

    private final CategoryRowMapper categoryRowMapper = new CategoryRowMapper();
    private final PublisherRowMapper publisherRowMapper = new PublisherRowMapper();

    @Override
    public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
        Book book = new Book();
        book.setIsbn(rs.getString("books.isbn"));
        book.setTitle(rs.getString("books.title_es"));
        book.setSynopsis(rs.getString("books.synopsis_es"));
        book.setPrice(new BigDecimal(rs.getString("books.price")));
        book.setDiscount(rs.getFloat("books.discount"));
        book.setCover(rs.getString("books.cover"));
        if(this.existsColumn(rs, "publishers.id")) {
            book.setPublisher(publisherRowMapper.mapRow(rs, rowNum));
        }
        if(this.existsColumn(rs, "categories.id")) {
            book.setCategory(categoryRowMapper.mapRow(rs, rowNum));
        }
        return book;
    }
}

Con esta implementación, BookRowMapper podrá mapear los datos del libro de manera flexible, adaptándose a los datos disponibles en el ResultSet. Si se realiza una consulta con JOIN y las columnas correspondientes están presentes, se asignarán los valores a Publisher y Category. En caso contrario, simplemente se omitirán, lo que permite mantener un enfoque eficiente en la obtención de datos.

De esta forma, nuestro repositorio de libros quedaría:

@Repository
@RequiredArgsConstructor
public class BookRepositoryJdbc implements BookRepository {

    private final JdbcTemplate jdbcTemplate;

    @Override
    public List<Book> getAll() {
        String sql = """
                        SELECT * FROM books
                     """;
        return jdbcTemplate.query(sql, new BookRowMapper());
    }

    @Override
    public Optional<Book> findByIsbn(String isbn) {
        String sql = """
                SELECT * FROM books
                LEFT JOIN categories ON books.category_id = categories.id
                LEFT JOIN publishers ON books.publisher_id = publishers.id
                WHERE books.isbn = ?
           """;
        try {
            Book book = jdbcTemplate.queryForObject(sql, new BookRowMapper(), isbn);
            return Optional.of(book);
        } catch (Exception e) {
            return Optional.empty();
        }
    }
}

De esta forma, recuperamos sólo los datos básicos de los libros cuando nos traemos el listado y los modelos simples asociados (publisher y category) cuando accedemos al detalle.

En el repositorio, estamos utilizando JdbcTemplate, una herramienta proporcionada por Spring para facilitar la interacción con bases de datos a través de JDBC. Nos permite ejecutar consultas SQL de manera eficiente sin la necesidad de manejar manualmente la apertura y cierre de conexiones o el mapeo de resultados. En esta ocasión estamos utilizando los siguientes métodos:

  • query: Este método se utiliza cuando esperamos recibir varias filas de resultados de la consulta SQL. La función toma la consulta SQL y un RowMapper, que se encarga de convertir cada fila del ResultSet en una instancia de nuestra clase de dominio (en este caso, Book). El método ejecuta la consulta y retorna una lista de objetos mapeados.
  • queryForObject: A diferencia de query, este método se utiliza cuando esperamos que la consulta devuelva un único resultado. Si la consulta devuelve más de una fila o ninguna, se lanza una excepción. También requiere un RowMapper para transformar la única fila resultante en un objeto del dominio. Este método es útil, por ejemplo, cuando buscamos un registro específico, como un libro por su ISBN.

Ambos métodos facilitan enormemente el trabajo con bases de datos, permitiendo mantener el código limpio y centrado en la lógica del negocio, mientras Spring se encarga de gestionar las complejidades subyacentes.

Para manejar las relaciones N-M, como las existentes entre libros y autores o libros y géneros, es necesario realizar consultas adicionales a las tablas intermedias. Cada libro puede tener múltiples autores y géneros, por lo que utilizaremos repositorios separados para gestionar estas relaciones. Esto nos permitirá mantener el código limpio y modular, delegando en cada repositorio la lógica correspondiente a su entidad.

Crearemos dos repositorios adicionales: uno para autores y otro para géneros. Estos repositorios se encargarán de realizar las consultas necesarias para obtener los autores y géneros asociados a un libro, utilizando el ISBN como clave de búsqueda.

public interface AuthorRepository {

    List<Author> getByIsbnBook(String isbn);
}

@Repository
@RequiredArgsConstructor
public class AuthorRepositoryJdbc implements AuthorRepository {

    private final JdbcTemplate jdbcTemplate;

    @Override
    public List<Author> getByIsbnBook(String isbn) {
        String sql = """
                SELECT authors.* FROM authors
                JOIN books_authors ON authors.id = books_authors.author_id
                JOIN books ON books_authors.book_id = books.id
                AND books.isbn = ?
           """;
        return jdbcTemplate.query(sql, new AuthorRowMapper(), isbn);
    }
}

public interface GenreRepository {

    List<Genre> getByIsbnBook(String isbn);
}

@Repository
@RequiredArgsConstructor
public class GenreRepositoryJdbc implements GenreRepository {

    private final JdbcTemplate jdbcTemplate;

    @Override
    public List<Genre> getByIsbnBook(String isbn) {
        String sql = """
                SELECT genres.* FROM genres
                JOIN books_genres ON genres.id = books_genres.genre_id
                JOIN books ON books_genres.book_id = books.id
                AND books.isbn = ?
           """;
        return jdbcTemplate.query(sql, new GenreRowMapper(),isbn);
    }
}

Para completar la funcionalidad del método findByIsbn en el repositorio de libros, se deben agregar las consultas a los repositorios de autores y géneros. Al obtener los detalles de un libro específico, no solo recuperaremos los datos del propio libro, sino que también incluiremos los autores y géneros asociados.

El proceso implica realizar las consultas necesarias para recuperar los autores y géneros del libro utilizando los repositorios creados anteriormente, y luego asignarlos al objeto Book. Esto mantiene el código modular y sigue el principio de responsabilidad única, donde cada repositorio gestiona la carga de su entidad correspondiente:

@Repository
@RequiredArgsConstructor
public class BookRepositoryJdbc implements BookRepository {

    private final JdbcTemplate jdbcTemplate;

    private final AuthorRepository authorRepository;
    private final GenreRepository genreRepository;

    @Override
    public List<Book> getAll()
    {
        String sql = """
                        SELECT * FROM books
                     """;
        return jdbcTemplate.query(sql, new BookRowMapper());
    }

    @Override
    public Optional<Book> findByIsbn(String isbn) {
        String sql = """
                SELECT * FROM books
                LEFT JOIN categories ON books.category_id = categories.id
                LEFT JOIN publishers ON books.publisher_id = publishers.id
                WHERE books.isbn = ?
           """;
        try {
            Book book = jdbcTemplate.queryForObject(sql, new BookRowMapper(), isbn);
            book.setAuthors(authorRepository.getByIsbnBook(isbn));
            book.setGenres(genreRepository.getByIsbnBook(isbn));
            return Optional.of(book);
        } catch (Exception e) {
            return Optional.empty();
        }
    }
}

clase/daw/arquitectura/layers.txt · Última modificación: 2024/09/26 18:11 por cesguiro