Spring MVC CRUD con ejemplo

Spring MVC es un marco Web MVC para crear aplicaciones web. Es un módulo Spring igual que Spring Boot, Spring-Security, etc. El término MVC significa arquitectura Model-View-Controller. En este artículo, crearemos una aplicación CRUD de seguimiento de cursos simple que se centrará en el módulo Spring MVC. CRUD significa crear, leer, actualizar y eliminar.

Estructura del proyecto 

Project structure

Estructura del proyecto

dependencias

Agregue las siguientes dependencias al archivo build.gradle si aún no están presentes.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.projectlombok:lombok:1.18.20'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

Capa de modelo

Cree un POJO simple (objeto Java antiguo simple) con algunas anotaciones JPA y Lombok.

  • @NoArgsConstructor: esta anotación genera constructores sin argumentos.
  • @AllArgsConstructor: esta anotación genera constructores con todos los argumentos de campo.
  • @Data: esta anotación genera getters, setter, toString, constructores de argumentos requeridos, equals y hashCode.
  • @Entity:  esta anotación define que una clase se puede asignar a una tabla.
  • @Table: esta anotación especifica el nombre de la tabla de la base de datos utilizada para la asignación.
  • @Id: esta anotación marca la clave principal de la entidad.
  • @GeneratedValue: esta anotación proporciona la especificación de estrategias de generación para los valores de las claves principales.
  • @Column: esta anotación marca el nombre de la columna en la tabla para ese atributo en particular.

Java

import lombok.*;
import javax.persistence.*;
  
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "courses")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
  
    @Column(name = "course_name") 
    private String courseName;
    @Column(name = "instructor")
    private String instructor;
    @Column(name = "email")
    private String email;
}

DAO (Objeto de acceso a datos) / Capa de repositorio

  • @Repository: Esta anotación es un estereotipo que marca la clase del repositorio. Indica que la clase anotada actúa como una base de datos en la que se pueden realizar operaciones CRUD.
  • JpaRepository< Course, Long > : JpaRepository es una extensión específica de JPA del Repositorio. Contiene la API completa de CrudRepository y PagingAndSortingRepository. Por lo tanto, contiene API para operaciones CRUD básicas y también API para paginación y clasificación. Aquí habilitamos operaciones de base de datos para empleados. Para obtener más información sobre Spring Data Jpa, consulte este artículo.

Java

import com.example.testing_001.model.Course;
import org.springframework.data.jpa.repository.JpaRepository;
  
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
  
}

Capa de servicio

La capa de servicio proporciona una abstracción sobre la capa de datos. Su objetivo principal es cumplir con los requisitos comerciales. Siempre es mejor separar la lógica comercial de la lógica de la aplicación.

Java

import com.example.testing_001.model.Course;
import java.util.List;
import org.springframework.data.domain.Page;
  
public interface CourseService {
    List<Course> getAllCourses();
    void saveCourse(Course course);
    Course getCourseById(long id);
    void deleteCourseById(long id);
    Page<Course> findPaginated(int pageNum, int pageSize,
                               String sortField,
                               String sortDirection);
}

La clase (descrita a continuación) CourseServiceImpl implementa la interfaz CourseService y nos proporciona toda la lógica de operaciones CRUD, que es nuestra lógica comercial aquí.

Anotaciones utilizadas:

  • @Service: esta anotación es un estereotipo que se usa para anotar clases en las capas de servicio.
  • @Autowired: esta anotación se utiliza para conectar automáticamente un bean a otro bean.

Java

import com.example.testing_001.model.Course;
import com.example.testing_001.repository.CourseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
  
import java.util.List;
import java.util.Optional;
  
@Service
public class CourseServiceImpl implements CourseService{
  
    @Autowired
    private CourseRepository courseRepository;
  
    @Override
    public List<Course> getAllCourses() {
        return courseRepository.findAll();
    }
  
    @Override
    public void saveCourse(Course course) {
        this.courseRepository.save(course);
    }
  
    @Override
    public Course getCourseById(long id) {
        Optional<Course> optionalCourse = courseRepository.findById(id);
        Course course = null;
        if (optionalCourse.isPresent()) {
            course = optionalCourse.get();
        } else {
            throw new RuntimeException("Course not found for id : " + id);
        }
        return course;
    }
  
    @Override
    public void deleteCourseById(long id) {
        this.courseRepository.deleteById(id);
    }
  
    @Override
    public Page<Course> findPaginated(int pageNum, int pageSize, String sortField, String sortDirection) {
        Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortField).ascending() :
                Sort.by(sortField).descending();
  
        Pageable pageable = PageRequest.of(pageNum - 1, pageSize, sort);
        return this.courseRepository.findAll(pageable);
    }
}

Capa de controlador

En Spring MVC, la capa del controlador es la capa superior de la arquitectura que se usa para manejar las requests web. Luego, dependiendo del tipo de solicitud, la pasó a las capas correspondientes.

Anotaciones utilizadas:

  • @Controller: esta anotación marca que la clase en particular cumple la función de controlador.
  • @GetMapping: esta anotación marca la asignación de requests HTTP GET en métodos de controlador específicos.
  • @PostMapping: esta anotación marca la asignación de requests HTTP POST en controladores de métodos específicos.

Java

import com.example.testing_001.model.Course;
import com.example.testing_001.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
  
import java.util.List;
  
@Controller
public class CourseController {
  
    @Autowired
    private CourseService courseService;
  
    @GetMapping("/")
    public String viewHomePage(Model model) {
        return findPaginated(1, "courseName", "asc", model);
    }
  
    @GetMapping("/add")
    public String showNewCourseForm(Model model) {
        Course Course = new Course();
        model.addAttribute("course", Course);
        return "new_course";
    }
  
    @PostMapping("/save")
    public String saveCourse(@ModelAttribute("course") Course course) {
        // save Course to database
        courseService.saveCourse(course);
        return "redirect:/";
    }
  
    @GetMapping("/update/{id}")
    public String showFormForUpdate(@PathVariable( value = "id") long id, Model model) {
  
        Course course = courseService.getCourseById(id);
        model.addAttribute("course", course);
        return "update_course";
    }
  
    @GetMapping("/delete/{id}")
    public String deleteCourse(@PathVariable (value = "id") long id) {
  
        this.courseService.deleteCourseById(id);
        return "redirect:/";
    }
  
  
    @GetMapping("/page/{pageNo}")
    public String findPaginated(@PathVariable (value = "pageNo") int pageNo,
                                @RequestParam("sortField") String sortField,
                                @RequestParam("sortDir") String sortDir,
                                Model model) {
        int pageSize = 5;
  
        Page<Course> page = courseService.findPaginated(pageNo, pageSize, sortField, sortDir);
        List<Course> listCourses = page.getContent();
  
        model.addAttribute("currentPage", pageNo);
        model.addAttribute("totalPages", page.getTotalPages());
        model.addAttribute("totalItems", page.getTotalElements());
  
        model.addAttribute("sortField", sortField);
        model.addAttribute("sortDir", sortDir);
        model.addAttribute("reverseSortDir", sortDir.equals("asc") ? "desc" : "asc");
  
        model.addAttribute("listCourses", listCourses);
        return "index";
    }
}

PRUEBE: Cambie el mapeo GET para la actualización del curso y elimine el curso para PONER el mapeo y ELIMINAR el mapeo respectivamente. Se considera una buena práctica utilizar asignaciones basadas en la funcionalidad de la API.

Plantillas HTML

Hemos utilizado thymleaf como nuestro motor de plantillas en lugar de jsps tradicionales.

página de inicio

HTML

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="ISO-8859-1">
  <title>Course Tracker</title>
  
  <link rel="stylesheet"
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
        integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
        crossorigin="anonymous">
  
</head>
<body>
  
<div class="container my-2">
  <h1>Courses List</h1>
  
  <a th:href = "@{/add}" class="btn btn-primary btn-sm mb-3"> Add Course </a>
  
  <table border="1" class = "table table-striped table-responsive-md">
    <thead>
    <tr>
      <th>
        <a th:href="@{'/page/' + ${currentPage} + '?sortField=courseName&sortDir=' + ${reverseSortDir}}">
          Course Name</a>
      </th>
      <th>
        <a th:href="@{'/page/' + ${currentPage} + '?sortField=instructor&sortDir=' + ${reverseSortDir}}">
          Course Instructor</a>
      </th>
      <th>
        <a th:href="@{'/page/' + ${currentPage} + '?sortField=email&sortDir=' + ${reverseSortDir}}">
          Course Email</a>
      </th>
      <th> Actions </th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="course : ${listCourses}">
      <td th:text="${course.courseName}"></td>
      <td th:text="${course.instructor}"></td>
      <td th:text="${course.email}"></td>
      <td> <a th:href="@{/update/{id}(id=${course.id})}" class="btn btn-primary">Update</a>
        <a th:href="@{/delete/{id}(id=${course.id})}" class="btn btn-danger">Delete</a>
      </td>
    </tr>
    </tbody>
  </table>
  
  <div th:if = "${totalPages > 1}">
    <div class = "row col-sm-10">
      <div class = "col-sm-5">
        Total Rows: [[${totalItems}]]
      </div>
      <div class = "col-sm-3">
                    <span th:each="i: ${#numbers.sequence(1, totalPages)}">
                        <a th:if="${currentPage != i}" th:href="@{'/page/' + ${i}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">[[${i}]]</a>
                        <span th:unless="${currentPage != i}">[[${i}]]</span>     
                    </span>
      </div>
      <div class = "col-sm-1">
        <a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${currentPage + 1}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">Next</a>
        <span th:unless="${currentPage < totalPages}">Next</span>
      </div>
  
      <div class="col-sm-1">
        <a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${totalPages}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">Last</a>
        <span th:unless="${currentPage < totalPages}">Last</span>
      </div>
    </div>
  </div>
</div>
</body>
</html>

Agregar página del curso

HTML

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="ISO-8859-1">
    <title>Course </title>
    <link rel="stylesheet"
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
          integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
          crossorigin="anonymous">
</head>
<body>
<div class="container">
    <h1>Course Tracker</h1>
    <hr>
    <h2>Save Course</h2>
  
    <form action="#" th:action="@{/save}" th:object="${course}"
          method="POST">
        <input type="text" th:field="*{courseName}"
               placeholder="Course Name" class="form-control mb-4 col-4">
  
        <input type="text" th:field="*{instructor}"
               placeholder="Instructor Name" class="form-control mb-4 col-4">
  
        <input type="text" th:field="*{email}"
               placeholder="Course Email" class="form-control mb-4 col-4">
  
        <button type="submit" class="btn btn-info col-2"> Save Course</button>
    </form>
  
    <hr>
  
    <a th:href = "@{/}"> Back to Course List</a>
</div>
</body>
</html>

Actualizar página del curso

HTML

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="ISO-8859-1">
    <title>Course Tracker</title>
  
    <link rel="stylesheet"
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <h1>Course Tracker</h1>
    <hr>
    <h2>Update Course</h2>
  
    <form action="#" th:action="@{/save}" th:object="${course}"
          method="POST">
  
        <!-- Add hidden form field to handle update -->
        <input type="hidden" th:field="*{id}" />
        <input type="text" th:field="*{courseName}" class="form-control mb-4 col-4" placeholder="Course name">
        <input type="text" th:field="*{instructor}" class="form-control mb-4 col-4" placeholder="Course instructor">
        <input type="text" th:field="*{email}" class="form-control mb-4 col-4" placeholder="Email">
        <button type="submit" class="btn btn-info col-2"> Update Course</button>
    </form>
  
    <hr>
  
    <a th:href = "@{/}"> Back to Course List</a>
</div>
</body>
</html>

Operaciones CRUD

Creando un curso

Adding a course

Agregar un curso

Cursos de lectura

Viewing Course list  - page 1

Visualización de la lista de cursos – página 1

Viewing Course list - page 2

Visualización de la lista de cursos – página 2

Curso de actualización

Updating course

Curso de actualización

Curso actualizado con éxito

Course updated successfully

Curso actualizado con éxito

Eliminando curso

Deleted the updated course

Eliminado el curso actualizado

Publicación traducida automáticamente

Artículo escrito por anuanu0_0 y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *