DEV Community

John Enad
John Enad

Posted on

Beginner Spring Boot Series - Part 2

Resuming our Spring Boot Beginner Series, I am continuing with the Simple Person CRUD example. I figured that it's better to use a newer version of Spring Boot version to 3.1.0. This decision caused has caused changes that would impact the JPA/Hibernate configurations and the OpenAPI setup. There are some nuances there which I'll point out later on. It is important to note that this is just a demo project in fact it employs some bad practices like hard-coded usernames and passwords. This should never be allowed in a real application.

As per my promise in the last post, I've uploaded the updated code to my GitHub repository, accessible here: https://github.com/jenad88/enad-spring-1

The project follows a structure that has been organized into several distinct packages: controller, service, repository, model, and DTO, along with a few others.

Image description

The role of the controller code is to serve as the visible interface for users and other APIs. This segment of the code handles incoming requests, performs validations where necessary, and hands it over to the service code for the execution of business processing. Once it retrieves the results from the invoked service, the controller leverages the DTO code to shape the returned response.

Our project's service code forms the backbone of our business logic and processing. It has an understanding of the DTO and model structures. This takes inputs from the controller (along with the configuration/environment) and uses them to execute specific tasks, and upon completion, relays the results back to the controller. Whenever required, it "talks" with databases or other services.

The repository code serves as the project's connection with the database. It is in-charge of data storage and retrieval and is thus integrated with the model structures which I'll explain next.

The model package houses code that mirrors the data in the database tables. For instance, in our simple project, we have a 'Person' class that aligns with the 'Person' table in MySQL. The fields within the 'Person' class map directly to the respective columns in the 'Person' table.

The DTO package hosts the data structures used when invoking controller API methods and when returning data from the API.

A core principle used in our project is the idea of 'Separation of Concerns'. This often used (and over-mentioned) concept concerns code organization. In simple terms, it means that your code should be grouped together based on its functionality. The controller focuses on controlling request flow, the service is dedicated to executing business logic, and the repository looks after data storage and retrieval.

Why is it important? For one, it clarifies the roles and responsibilities of each code grouping and thus helps make the code easier to understand. More importantly, this significantly makes it easier to create and execute unit tests (a potential future topic).

The PersonController class is used to handle HTTP requests related to 'Person' entities. Because it implements the PersonApi interface, it provides the concrete implementation of the methods defined in the PersonApi interface. The PersonController has a dependency on PersonService which is used to perform business process operations related to 'Person' entities.

Below are the methods of the PersonController:

getAll(): Handles HTTP GET requests to fetch all 'Person' entities.

getPersonById(long id): Handles HTTP GET requests for a specific 'Person' entity, identified by its unique 'id'.

createPerson(PersonDTO personDTO): Handles HTTP POST requests to create a new 'Person' entity.

updatePerson(long id, PersonDTO personDTO): Handles HTTP PUT requests to update an existing 'Person' entity. It uses the provided 'id' to identify the 'Person' entity to be updated.

deletePerson(long id): Handles HTTP DELETE requests to remove an existing 'Person' entity.

findByActive(): Handles HTTP GET requests to fetch 'Person' entities marked as 'active'.

Each method returns an instance of ResponseEntity, a class that represents the entire HTTP response, including headers, body, and status. It is used to fully configure the HTTP response.

@RestController
@RequestMapping(PATH_V1 + "/persons")
public class PersonController implements PersonApi {

    private final PersonService personService;

    @Autowired
    public PersonController(PersonService personService) {
        this.personService = personService;
    }

    public ResponseEntity<PersonResponse> getAll() {
        try {

            List<PersonDTO> persons = new ArrayList<>(personService.fetchAll());

            if (persons.isEmpty()) {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

            return new ResponseEntity<>(PersonResponse.builder().data(persons).build(), HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    public ResponseEntity<PersonResponse> getPersonById(long id) {
        Optional<PersonDTO> personOpt = personService.getById(id);

        return personOpt.map(personDTO -> new ResponseEntity<>(PersonResponse.builder().data(personDTO).build(), HttpStatus.OK))
                .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    public ResponseEntity<PersonResponse> createPerson(PersonDTO personDTO) {
        try {
            personDTO.setActive(true);
            PersonDTO newPerson = personService.save(personDTO);
            return new ResponseEntity<>(PersonResponse.builder().data(newPerson).build(), HttpStatus.CREATED);
        } catch (Exception e) {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    public ResponseEntity<PersonResponse> updatePerson(long id, PersonDTO personDTO) {
        personDTO.setId(id);
        PersonDTO updatedPersonDTO = personService.update(personDTO);
        if (updatedPersonDTO == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        PersonResponse personResponse = PersonResponse.builder().data(updatedPersonDTO).build();

        return new ResponseEntity<>(personResponse, HttpStatus.OK);
    }

    public ResponseEntity<HttpStatus> deletePerson(long id) {
        try {
            personService.delete(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    public ResponseEntity<PersonResponse> findByActive() {
        try {
            List<PersonDTO> persons = personService.fetchByActive(true);

            if (persons.isEmpty()) {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
            return new ResponseEntity<>(PersonResponse.builder().data(persons).build(), HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

The PersonServiceImpl class which lives in the service package is responsible for business logic and can call methods in repository classes. Because it implements the PersonService interface, it provides all the concrete implementation of the methods defined in that interface.

It has a PersonRepository instance which is used to interact with the MySQL database used in the application.

Below are the methods of the PersonServiceImpl

fetchAll(): This method retrieves all 'Person' entities from the repository, converts them to DTOs (Data Transfer Objects), and returns them as a list.

fetchByActive(boolean active): This method retrieves all 'Person' entities with a specific active status from the repository, converts them to DTOs, and returns them as a list.

getById(long id): This method retrieves a 'Person' entity with a specific 'id'. If such an entity exists, it converts it to a DTO and wraps it in an Optional. If no such entity exists, it returns Optional.empty().

update(PersonDTO personDTO): This method updates an existing 'Person' entity. It retrieves the 'Person' entity from the repository, updates its fields, saves the updated entity back to the repository, and returns the updated entity as a DTO. If no such entity exists, it returns null.

add(String firstName, String lastName, boolean active): This method creates a new 'Person' entity, saves it to the repository, and returns the saved entity as a DTO.

delete(long id): This method deletes a 'Person' entity with a specific 'id'. If no such entity exists, it does nothing.

save(PersonDTO personDTO): This method creates a new 'Person' entity from a DTO, saves it to the repository, and returns the saved entity as a DTO.

The class uses PersonDTO.convert() to convert between 'Person' entities and DTOs. This is an example of separation of concerns: the service layer handles the business logic, while the DTOs are used for transferring data between different application layers (in this case, between the service layer and a web layer).

@Service
public class PersonServiceImpl implements PersonService {

    final private PersonRepository personRepository;

    @Autowired
    public PersonServiceImpl(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Override
    public List<PersonDTO> fetchAll() {
        List<Person> persons = personRepository.findAll();
        return PersonDTO.convert(persons);
    }

    @Override
    public List<PersonDTO> fetchByActive(boolean active) {
        List<Person> persons = personRepository.findByActive(active);
        return PersonDTO.convert(persons);
    }

    @Override
    public Optional<PersonDTO> getById(long id) {
        Optional<Person> personOpt = personRepository.findById(id);
        return personOpt.map(PersonDTO::convert);
    }

    @Override
    public PersonDTO update(PersonDTO personDTO) {
        Optional<Person> p = personRepository.findById(personDTO.getId());
        if (p.isEmpty()) {
            return null;
        }

        Person person = p.get();
        person.setFirstName(personDTO.getFirstName());
        person.setLastName(personDTO.getLastName());
        person.setActive(personDTO.isActive());

        Person updatedPerson = personRepository.save(person);
        return PersonDTO.convert(updatedPerson);
    }

    @Override
    public PersonDTO add(String firstName, String lastName, boolean active) {
        Person person = personRepository.save(new Person(firstName, lastName, active));
        return PersonDTO.convert(person);
    }

    @Override
    public void delete(long id) {
        Optional<Person> person = personRepository.findById(id);
        person.ifPresent(personRepository::delete);
    }

    @Override
    public PersonDTO save(PersonDTO personDTO) {
        Person person = new Person();
        person.setFirstName(personDTO.getFirstName());
        person.setLastName(personDTO.getLastName());
        person.setActive(personDTO.isActive());

        Person savedPerson = personRepository.save(person);
        return PersonDTO.convert(savedPerson);
    }
}
Enter fullscreen mode Exit fullscreen mode

This post is getting a bit on the long side so will stop here. In the next post, I will continue improving this application.

The GitHub repository can be found here: https://github.com/jenad88/enad-spring-1

Top comments (0)