Spring Data JPA Master Class

Anotações e comentários sobre o curso Spring Data JPA Master Class, ministrado por Amigoscode

Henrique S.
16 min readOct 11, 2021

Durante meus estudos de Spring Data JPA conheci o canal do Nelson no YouTube. Achei a didática dele muito boa e acabei fazendo dois cursos dele: Getting Started with Spring Boot e Spring Data JPA Master Class. Como anotei bastante coisa, resolvi compartilhar aqui o que aprendi no curso. Isso me ajuda a fixar o conhecimento adquirido e também pode ajudar outros que porventura se deparem com este artigo.

Leia também:

9 boas práticas para desenvolver uma API REST

Série sobre API REST com Java e Spring Boot:

Parte 1 — Preparando o ambiente

Parte 2 — Classes, camadas e endpoint

Parte 3 — Banco de dados e Tratamento de erros

Parte 4 — Métodos PUT e DELETE

Parte 5 — Testando no Postman

Curso completo de Spring Boot

Eu fui escrevendo o texto enquanto fazia o curso, durante a criação do projeto. Portanto não estranhem alterações no código e diferentes formas de implementá-lo que venham a surgir ao longo das etapas. Vamos lá?

Parte I: Setup

O primeiro passo é arrumar o ambiente para começar o curso. Para isso, devemos clonar o repositório do GitHub com o uso de uma chave SSH.

Após clonar com sucesso, o projeto, na main branch, contém apenas os arquivos básicos: a classe do método main já criada, o application.properties já preenchido e as dependências já colocadas no pom.xml. As principais são spring-boot-starter-data-jpa e spring-boot-starter-web. Por padrão, o projeto veio com a versão 15 do Java. Alterei para 1.8, que é a versão que tenho instalada no meu PC. Além disso, o curso usa o banco PostgreSQL e a IDE IntelliJ, então fiz o mesmo para poder acompanhar melhor.

A versão do IntelliJ utilizada pelo Nelson é a chique (Ultimate). Como eu não sou chique, eu uso a Community Edition. Esta versão não fornece as Database Tools com SQL, que permitem a interação com o banco direto na IDE. Mas não tem problema, é possível fazer tudo no shell (PSQL) mesmo.

Parte II: Getting Started with Spring Data JPA

Nesta parte, nós criamos a primeira classe (Student) que representará uma entidade e tabela no banco de dados. Também definimos as propriedades dessa classe, os getters e setters, o toString e os construtores.

Para o Spring fazer a “mágica” no banco é muito simples. Basta que utilizemos as anotações necessárias (@Entity na classe e @Id e @GeneratedValue na chave primária — e @SequenceGenerator, pois escolhemos a estratégia SEQUENCE). Mas além do necessário, é boa prática especificarmos algumas outras coisas (como nome, nulidade, tamanho etc.). Para isso, utilizamos anotações como @Table e @Column, com seus devidos parâmetros.

Hibernate criando a tabela

Pronto. Classe mapeada para o banco. Eis aqui uma parte da classe Student:

@Entity(name = "Student")
@Table(name = "student", uniqueConstraints = {@UniqueConstraint(name = "student_email_unique", columnNames = "email")})
public class Student {

@Id
@SequenceGenerator(name = "student_sequence", sequenceName = "student_sequence", allocationSize = 1)
@GeneratedValue(strategy = SEQUENCE, generator = "student_sequence")
@Column(name = "id", updatable = false)
private Long id;

@Column(name = "first_name", nullable = false, columnDefinition = "TEXT")
private String firstName;

@Column(name = "last_name", nullable = false, columnDefinition = "TEXT")
private String lastName;

@Column(name = "email", nullable = false, columnDefinition = "TEXT")
private String email;

@Column(name = "age", nullable = false)
private Integer age;

public Student() {
}

public Student(String firstName, String lastName, String email, Integer age) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;

Parte III: Repositories

O Repository é uma camada criada para facilitar o nosso acesso aos dados. É uma interface que, por meio da abstração, reduz grande parte do boilerplate code e, assim, nos auxilia nas operações CRUD.

No nosso caso, criamos a interface StudentRepository, que herda de JpaRepository (que herda de PagingAndSortingRepository, que herda de CrudRepository). Passamos no generics a entidade com a qual estamos trabalhando e o tipo de seu ID (no caso, Long). Desse modo, facilmente já teremos acesso a muitos métodos pré-criados para realizar as operações que desejarmos.

Observe a quantidade de métodos que já vêm prontos para uso

Mas se nós quisermos um método mais específico, que não existe pronto na interface, podemos criá-lo de acordo com um padrão, pois o Spring Data JPA já saberá como criar as respectivas queries. Por exemplo, se criarmos o método findStudentByEmail, isso já será suficiente para que encontremos o estudante que possui o e-mail passado.

Com essa padronização de nomenclatura, podemos criar inúmeros métodos bem específicos sem ter o trabalho de escrever o código deles, como por exemplo findStudentByFirstNameEqualsAndAgeIsGreaterThanEqual. Esse método já nos retorna a lista de estudantes cujo primeiro nome é igual ao passado como parâmetro e cuja idade é maior ou igual a passada como parâmetro. Isso acontece pois, por debaixo dos panos, o Spring está usando JPQL para a construção dessas queries. Isso demonstra muito bem o poder dessa ferramenta.

public interface StudentRepository extends JpaRepository<Student, Long> {

Optional<Student> findStudentByEmail(String email);

List<Student> findStudentByFirstNameEqualsAndAgeIsGreaterThanEqual (String firstName, Integer age);

Os métodos podem ser chamados da seguinte forma:

studentRepository.findStudentByEmail("maria@joana.com").ifPresent(System.out::println);studentRepository.findStudentByFirstNameEqualsAndAgeIsGreaterThanEqual("Maria", 18).forEach(System.out::println);

E o Hibernate se comporta assim:

Queries no Hibernate

Parte IV: Querying Data

E por falar em JPQL, temos a opção de escrever à mão essa query. Basta usar a anotação @Query. Nesse caso, podemos até mudar o nome do método, pois a query que escrevermos valerá independentemente disso.

@Query("SELECT s FROM Student s WHERE s.email = ?1")
Optional<Student> findStudentByEmail(String email);

@Query("SELECT s FROM Student s WHERE s.firstName = ?1 AND s.age >= ?2")
List<Student> findStudentByFirstNameEqualsAndAgeIsGreaterThanEqual (String firstName, Integer age);

E ainda temos mais uma opção: podemos também escrever queries nativas do banco de dados que estamos utilizando. No caso do PostgreSQL, o mesmo código para o segundo método ficaria assim:

@Query(value = "SELECT * FROM student WHERE first_name = ?1 AND age >= ?2", nativeQuery = true)
List<Student> selectStudentWhereFirstNameAndAgeGreaterOrEqualNative (String firstName, Integer age);

O problema desta última forma de fazer queries é que o código fica específico para o banco de dados para o qual foi escrito. Se mudarmos do PostgreSQL para um MySQL, por exemplo, esse código já não funcionaria. Escrevendo em JPQL, o framework adaptará o código para cada banco.

Uma outra possibilidade é usar o recurso de parâmetros com nome. Nesse caso, utilizamos a anotação @Param para associar o parâmetro da query ao parâmetro do método.

@Query(value = "SELECT * FROM student WHERE first_name = :firstName AND age >= :age", nativeQuery = true)
List<Student> selectStudentWhereFirstNameAndAgeGreaterOrEqualNative (@Param("firstName") String firstName, @Param("age") Integer age);

Em todos esses casos, mesmo que a query seja feita de forma diferente, nós obteremos o mesmo resultado.

Por enquanto só falamos em consultas ao banco de dados. Mas e se quisermos alterar algo na tabela, por exemplo deletar ou atualizar um estudante?

Nesse caso, criamos um método sem retorno. Aqui utilizamos a anotação @Modifying, que diz ao Spring que estamos apenas modificando dados no banco, e que a query não precisa mapear nada do banco para entidades.

Além disso, precisamos da anotação @Transactional, pois ações de desse tipo precisam estar dentro de uma Transaction (isso será explicado posteriormente de forma mais detalhada). Portanto, para deletarmos um estudante, podemos criar um método assim:

@Transactional
@Modifying
@Query("DELETE FROM Student u WHERE u.id = ?1")
int deleteStudentById(Long id);

Observe na figura anterior que, ao fazermos a consulta, tínhamos como retorno duas Marias (id=1 e id=2). Agora, após a utilização do método deleteStudentById(1L) em nosso studentRepository temos que o id=1 foi deletado e o retorno da mesma consulta só traz a Maria de id=2.

Parte V: Sorting and Pagination

Para trabalhar melhor esta parte do curso, foi utilizada uma biblioteca muito útil: o Java Faker, que serve para criar dados “fake”. Aqui, criamos estudantes com nome e idade aleatórios e seus respectivos e-mails para popular o banco de dados.

private void generateRandomStudents(StudentRepository studentRepository) {

Faker faker = new Faker();

for (int i = 0; i < 20; i++) {
String firstName = faker.name().firstName();
String lastName = faker.name().lastName();
String email = String.format("%s.%s@amigoscode.edu",
firstName.toLowerCase(), lastName.toLowerCase());
Integer age = faker.number().numberBetween(17,55);

Student student = new Student(firstName, lastName, email, age);
studentRepository.save(student);
}
}
Vários estudatnes fake

Desse modo, podemos começar o Sorting, isto é, a ordenação de dados. Para fazer isso, utilizamos a classe Sort, que possui vários métodos com essa finalidade. No exemplo abaixo, utilizo duas formas de fazer a ordenação (a escolha fica a critério do desenvolvedor).

generateRandomStudents(studentRepository);

Sort sort = Sort.by(Sort.Direction.ASC, "firstName").and(Sort.by("age").descending());

studentRepository.findAll(sort).forEach(student -> System.out.println(student.getFirstName() + " " + student.getAge()));

Printando no console, obtemos o seguinte:

Select do Hibernate + Resultado da ordenação

Já a paginação serve para que obtenhamos dados desejados do banco sem que seja necessário buscar todo o conteúdo do banco. Assim, conseguimos pegar só alguns resultados por vez.

Alguns métodos do Repository retornam uma Page. Então, para usá-los, precisamos criar variáveis de tipos específicos para realizarmos a paginação. Aqui, usamos um PageRequest, cujo método of recebe como parâmetros a página que queremos buscar, o número de elementos nela e ainda podemos juntar isso com o Sort para fazer a ordenação.

PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("firstName").ascending());

Page<Student> page = studentRepository.findAll(pageRequest);

Em seguida entramos no modo debug para explorar alguns métodos disponíveis em Page. Resolvi por conta própria fazer uns testes e printar no console o seguinte:

List<Student> students = page.getContent();
students.forEach(System.out::println);

O resultado foi:

Select e apresentação da lista de estudantes da página 1

Agora vamos para uma parte muito importante: as relações entre entidades.

Parte VI: One to One Relationships

Como cada carteirinha pertence a apenas um estudante e cada estudante só tem uma carteirinha, temos uma relação “one to one”. Para estabelecer essa relação com a entidade Student, criaremos a classe StudentIdCard de forma análoga (construtores, getters, setters, anotações etc.).

Dentro de StudentIdCard, criamos um atributo do tipo Student e colocamos as devidas anotações (@JoinColumn e @OneToOne) com seus respectivos parâmetros, como mostra o código abaixo.

@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "student_id", referencedColumnName = "id",
foreignKey = @ForeignKey(name = "student_id_card_student_id_fk"))
private Student student;

Pronto, a relação está feita.

Para podermos rodar a aplicação e testarmos, precisamos criar um Repository para StudentIdCard, análogo ao StudentRepository.

Criação e relacionamento entre as entidades
Como fica no banco de dados

Deu certo, nova tabela criada com sucesso e a relação entre elas também.

Agora, para popularmos as tabelas, precisamos criar um Student e seu respectivo StudentIdCard. Para isso, na classe StudentIdCard criamos um novo construtor com os parâmetros student e cardNumber.

Usando novamente o Faker criamos um estudante, o qual utilizaremos na criação da carteirinha. Então usamos o Repository para salvar no banco.

Faker faker = new Faker();
String firstName = faker.name().firstName();
String lastName = faker.name().lastName();
String email = String.format("%s.%s@amigoscode.edu", firstName.toLowerCase(), lastName.toLowerCase());
Integer age = faker.number().numberBetween(17,55);
Student student = new Student(firstName, lastName, email, age);

StudentIdCard studentIdCard = new StudentIdCard("123456789", student);
Inserção dos dados na tabela
Como fica no PostgreSQL

Como podemos ver, o estudante e sua carteirinha foram criados juntos, sem que precisássemos invocar o studentRepository. Isso se deve ao CascadeType.ALL que escrevemos na anotação @OneToOne. Com esse tipo de “cascade”, uma operação (CRUD) em uma entidade se propaga para as outras.

Agora precisamos falar sobre o ciclo de vida de um entidade, que é um tanto complexo. Tentaremos explicar brevemente.

Quando um objeto é criado, ele fica no estado transient (transitório). Quando salvamos esse objeto ele é persistido e passa para o estado managed (gerenciado). Se fecharmos essa sessão, esse objeto passa para o estado detached (desacoplado, ou seja, fora do contexto de persistência). E quando queremos remover o objeto do banco de dados, ele vai para o estado removed.

Peguei a imagem aqui

É importante sabermos como a coisa toda funciona por debaixo dos panos, mas o Spring Data JPA com o Hibernate abstrai tudo isso para nós. Assim, não precisamos lidar diretamente com o “entity manager” (que gerencia a camada de persistência que fica entre o banco de dados e nossos objetos).

Outra coisa importante para se saber é o “Fetch Type” (tipo de busca). Para relacionamentos one to one, o default é o EAGER. Isso significa que quando buscamos o IdCard, o Hibernate busca automaticamente o Student.

Vale também lembrar que as relações one to one podem ser unidirecionais ou bidirecionais. Quando fizemos nossa relação, ela foi unidirecional. Utilizamos a anotação @OneToOne apenas na classe StudentIdCard. Isso significa que se fizéssemos uma busca de IdCards, os Students respectivos também seriam carregados, mas o contrário não aconteceria (Students carregarem automaticamente os IdCards).

Para tornarmos a relação bidirecional, basta colocarmos a mesma anotação na classe Student, no atributo que representa a carteirinha.

@OneToOne(mappedBy = "student", orphanRemoval = true)
private StudentIdCard studentIdCard;

Aqui, o mappedBy serve justamente para fazer o mapeamento correto. O orphanRemoval settado para true significa que deletar um estudante deleta também automaticamente sua carteirinha. Isso é importante para termos o total controle do que queremos deletar do banco e o que não queremos.

Por fim, utilizamos o parâmetro foreignKey na anotação @JoinColumn para deixarmos mais amigável o nome dessa chave estrangeira.

Parte VII: One to Many Relationships

Nesta parte de relacionamentos “um para muitos”/” muitos para um”, utilizaremos a classe Book. Aqui, novamente começamos com uma relação unidirecional: muitos livros para um estudante. A classe Book também é criada de forma análoga às outras, com as devidas anotações e atributos. Nesta nossa classe, nosso foco é o Student:

@ManyToOne
@JoinColumn(name = "student_id", nullable = false, referencedColumnName = "id", foreignKey = @ForeignKey(name = "student_book_fk"))
private Student student;

Basta fazermos isso que o Spring já vai entender a relação e criará corretamente as tabelas.

Relacionamento Many To One feito com sucesso

Porém convém tornarmos essa relação bidirecional. Na classe Student, criamos uma lista de livros (já que estamos tratando de um estudante para muitos livros) e anotamos da seguinte maneira:

@OneToMany(mappedBy = "student", orphanRemoval = true, cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.LAZY)
private List<Book> books = new ArrayList<>();

Aqui temos novamente o mappedBy e o orphanRemoval tais como explicados anteriormente. Além disso, para fins didáticos, optamos por utilizar o tipo de cascade desta outra forma: especificando o PERSIST e o REMOVE, isto é, quando quisermos persistir ou remover um estudante no banco, isso será aplicado automaticamente aos livros que ele possui (é o “efeito cascata”).

Agora, para testarmos, vamos criar um método addBook na classe Student e criaremos livros na classe do main. Como podemos ver na imagem a seguir, mesmo sem termos criado um BookRepository, os livros foram adicionados ao banco. Isso se deve ao tipo de cascade que escolhemos anteriormente (neste caso, o PERSIST).

Livros criados com sucesso

Enquanto nos relacionamentos “um para um” o fetch type é EAGER por default, nos relacionamentos “um para muitos” o tipo default é LAZY. Se executarmos o programa com o findById em studentRepository, o Hibernate vai buscar somente o estudante e sua carteirinha, pois o relacionamento deles é OneToOne. Na imagem abaixo, mostramos como Hibernate faz o JOIN de student somente em student_id_card e os livros só são carregados quando chamamos o método getBooks.

studentRepository.findById(1L).ifPresent(s -> {
System.out.println("fetching book LAZY...");

List<Book> books = student.getBooks();

books.forEach(book -> {
System.out.println(s.getFirstName() + " borrowed " + book.getBookName());

});
});
Hibernate: EAGER e LAZY fetch types

Caso queiramos que os dados dos livros sejam carregados de forma EAGER, basta mudarmos o fetch type nos books da classe Student.

@OneToMany(mappedBy = "student", orphanRemoval = true, cascade = 
{CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.EAGER)
private List<Book> books = new ArrayList<>();

Desse modo, como a imagem abaixo mostra, os livros são carregados de forma EAGER: há um JOIN para book e outro JOIN para student_id_card.

Hibernate: EAGER fetch type

Contudo, para relacionamentos “um para muitos”, o recomendado é de fato deixarmos a configuração padrão, isto é, fetch type LAZY, pois caso a lista relacionada tenha muitos dados, nossa aplicação pode ficar muito lenta.

Parte VIII: Many to Many Relationships

Nas relações “muitos para muitos” usaremos o exemplo de muitos estudantes para muitos cursos. Primeiramente, então, vamos criar a classe Course. Feito isso, criamos em Student uma lista de courses e utilizamos a anotação @ManyToMany. Além disso, usamos uma anotação nova: @JoinTable.

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "enrolment", joinColumns = @JoinColumn(name = "student_id", foreignKey = @ForeignKey(name = "enrolment_student_id_fk")), inverseJoinColumns = @JoinColumn(name = "course_id", foreignKey = @ForeignKey(name = "enrolment_course_id_fk")))
private List<Course> courses = new ArrayList<>();

E na classe Course fazemos assim:

@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();

Com esse mapeamento, o Hibernate cria uma tabela intermediária de enrolments (matrículas).

Criando automaticamente a tabela “enrolment”
Vejam ela no banco

O próximo passo é criar métodos em Student para adicionar e remover cursos de modo que a relação bidirecional seja respeitada:

public void enrolToCourse(Course course) {
courses.add(course);
course.getStudents().add(this); //mantém a relação bidirecional
}

public void unEnrolCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this); //mantém a relação bidirecional
}

Na classe do main chamamos esses métodos:

student.enrolToCourse(new Course("Computer Science", "IT"));student.enrolToCourse(new Course("Java Java Java", "Java"));
Resultado nas tabelas separadas e com JOIN

Em seguida, vamos fazer alguns ajustes nas constraints e tomar mais controle sobre nossas tabelas. Criamos, então, a classe EnrolmentId para trabalharmos a chave composta student_id e course_id. Para que tudo funcione, precisamos da anotação @Embeddable, que indicará a incorporação à entidade enrolment.

Criando a classe Enrolment, que representa a entidade enrolment, passamos a criar a tabela manualmente em vez de deixar isso com o Spring (que fez a criação por meio do @JoinTable, lembra?).

@Embeddable
public class EnrolmentId implements Serializable {

@Column(name = "student_id")
private Long studentId;

@Column(name = "course_id")
private Long courseId;

Observe as anotações @Embeddable acima e @EmbeddedId abaixo.

@Entity(name = "Enrolment")
@Table(name = "enrolment")
public class Enrolment {
@EmbeddedId
private EnrolmentId id;

Agora precisamos conectar Student e Course à nossa nova entidade, que serve justamente para fazer essa ligação entre os “many” estudantes e os “many” cursos.

Na classe Student, substituímos a lista de cursos por uma lista de matrículas e trocarmos a anotação @ManyToMany para @OneToMany.

@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, mappedBy = "student")
private List<Enrolment> enrolments = new ArrayList<>();

Da mesma forma, na classe Course substituímos a lista de estudantes por uma lista de matrículas e também trocarmos a anotação @ManyToMany para @OneToMany.

@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, mappedBy = "course")
private List<Enrolment> enrolments = new ArrayList<>();

Em ambas as classes precisamos também de métodos para adicionar e remover os enrolments, por exemplo:

public void addEnrolment(Enrolment enrolment) {
if (!enrolments.contains(enrolment)) {
enrolments.add(enrolment);
}
}
public void removeEnrolment(Enrolment enrolment) {
enrolments.remove(enrolment);
}

Em seguida, na classe principal, adicionamos as matrículas e rodamos a aplicação.

Funcionando corretamente

Para melhorar um pouco nossa aplicação, vamos agora adicionar uma nova coluna na tabela de matrículas: createdAt. Para isso, basta criar essa coluna na classe Enrolment e anotar corretamente.

@Column(name = "created_at", nullable = false, columnDefinition = "TIMESTAMP WITHOUT TIME ZONE")
private LocalDateTime createdAt;
Hibernate atuando
E o resultado

Por fim, convém alterarmos o nome não muito amigável que aparece quando o Hibernate cria as chaves estrangeiras na tabela.

Os nomes ficam ilegíveis mesmo

Para melhorarmos isso, na classe Enrolment usamos o parâmetro foreignKey dentro da anotação @JoinColumn de cada atributo:

@ManyToOne
@MapsId("studentId")
@JoinColumn(name = "student_id", foreignKey = @ForeignKey(name = "enrolment_student_id_fk"))
private Student student;
@ManyToOne
@MapsId("courseId")
@JoinColumn(name = "course_id", foreignKey = @ForeignKey(name = "enrolment_course_id_fk"))
private Course course;

Feito isso, nosso resultado é:

Muito mais amigável, não?

Parte IX: Transactions

Uma “transação” é basicamente uma interação de coleta ou de mudança que acontece no banco de dados. Uma transação deve nos propiciar as características conhecidas como “ACID”, para assegurar a consistência e a validade dos dados. ACID é um acrônimo para Atomicity, Consistency, Isolation e Durability.

A Atomicidade significa o famoso “é tudo ou nada”, isto é: ou todas as operações são executadas e a transação é completa, ou nada é feito (rollback).

A Consistência indica que o estado dos dados de uma transação devem ser consistentes com o estado do banco, ou seja, deve-se passar por toda a checagem de restrições para que o commit aconteça.

O Isolamento se refere ao fato de que os dados de uma transação não podem ser visíveis a outras até que um commit (ou um rollback) seja feito.

A Durabilidade significa que as mudanças comittadas devem ser persistidas no banco, ou seja, não serão perdidas.

Por padrão, todos os métodos de query são “transacionais” quando estamos trabalhando com Spring Data JPA. A anotação @Transactional se faz necessária quando estamos modificando dados. Essa anotação pode ser usada tanto a nível de método, quanto a nível de classe/interface.

Podemos, por exemplo, colocar @Transactional(readOnly = true) ao criar uma interface. Se, por acaso, criarmos um método que modificará dados no banco (como um delete ou um update), aí é só marcar o método com @Transactional que essa anotação sobrescreverá a anotação a nível de classe — e, neste caso, nem precisamos colocar readOnly = false, pois o padrão é falso.

É por isso que lá na Parte IV, quando criamos o método deleteStudentById, usamos a anotação @Transactional. Como pôde-se ver, funcionou tudo corretamente.

Essa questão de “transacionalidade” é muito abrangente e tem muitos detalhes, mas com o que temos aqui já conseguimos entender e fazer uma boa aplicação e todo seu relacionamento com um banco de dados, graças aos incríveis recursos fornecidos pelo Spring Data JPA.

Chegamos ao fim da nossa Spring Data JPA Master Class, que teve como base o curso do Amigoscode. Com isso, pudemos ter uma boa noção de como operar um banco de dados utilizando Java e Spring. O nível de complexidade de se trabalhar com Spring Data JPA é um tanto alto, mas ele nos facilita muito a vida e nos dá muita agilidade.

Saber Spring Data JPA é essencial para quem está inserido no mundo do desenvolvimento de software com Java. Eu recomendo esse curso fortemente.

Siga-me no LinkedIn.

--

--

Henrique S.
Henrique S.

No responses yet