emhk.

Conhecendo o Spring Data JPA

16 de Aug, 2023Emanoel Henrick

Spring Data JPA é um projeto Spring que facilita a interação com banco de dados implementando as interfaces do JPA e trazendo funcionalidades como a criação de repositórios em tempo de execução. A implementação do JPA padrão do projeto é o Hibernate, um dos ORMs mais utilizados no ecossistema.

Além da dependência do Spring Data JPA, precisamos do driver do banco de dados que vamos utilizar, cada banco de dados pode ser configurado de forma diferente e portanto precisa de atenção na hora de especificar os valores no application.properties. Nesse exemplo vamos usar o MySQL. Podemos já iniciar um projeto com um Starter ou adicionar as dependências manualmente:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>

Subido o banco de dados podemos configurar no application.properties. Em spring.datasource.url vamos atribuir a url jdbc para o banco de dados, logo embaixo atribuimos o username e senha do banco. Em spring.jpa.hibernate.ddl-auto habilitamos o DDL que pode receber alguns valores, como o create e o update, um cria as tabelas do banco de dados baseado nas entidades da aplicação e o outro compara as entidades do banco de dados com os da aplicação e atualiza caso necessário, sempre adicionando coisas novas e nunca retirando as que já existiam. É válido lembrar para não usar essa configuração em produção, nesses casos, onde há alteração de tabelas, o melhor é seguir o padrão das migrations, mas um outro dia falamos sobre isso!

spring.datasource.url=jdbc:mysql://localhost:10000/db?createDatabaseIfNotExists=true&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root spring.jpa.hibernate.ddl-auto=update

A primeira tabela

Como o Hibernate implementa as interfaces do JPA, vamos usar apenas as interfaces do Javax/Jakarta para anotar nossas tabelas, isso torna possível uma troca mais fácil de implementações visto que as interfaces não mudam.

Tabelas ou entidades podem ser criadas anotando uma classe com @Entity, dessa forma, todas as propriedades da classe serão mapeadas para uma coluna da tabela:

@Entity public class User { private Long id; private String email; private String username; private String password; }

Para definirmos uma chave primária em uma das propriedades, anotamos com @Id. Nesse exemplo vamos usar a opção de auto incremento do banco de dados, para isso anotamos a chave primária com @GenerationType(strategy = GenerationType.IDENTITY), dessa forma o Id será gerado automaticamente:

@Entity public class User { @Id @GenerationType(strategy = GenerationType.IDENTITY) private Long id; private String email; private String username; private String password; }

Por padrão, a tabela recebe o mesmo nome da entidade mas pode ser alterado anotando com @Table(name = novo_nome), em casos onde as tabelas do banco de dados já existem, possuem nomes com palavras reservadas do banco de dados ou queira simplesmente usar outro nome:

@Entity @Table(name = users) public class User {

Colunas também recebem o mesmo nome das propriedades por padrão e podem ser alteradas seguindo a mesma ideia do @Table só que usando @Column. Essa anotação possui alguns parâmetros importantes responsáveis por configurar a coluna como o nullable para definir se a propriedade pode ou não ser nula, columnDefinition para especificar um tipo do banco de dados, unique que especifica se algo deve ser único, name que altera o nome da coluna no banco de dados e etc. No exemplo abaixo criamos uma propriedade que deve guardar uma data:

@Column(nullable = false, columnDefinition = "datetime") private OffsetDateTime date;

Anotando uma propriedade do tipo data com @CreationTimestamp o banco fica encarregado de gerar a data automaticamente an hora da criação do objeto, vendo todos os conceitos aplicados ficaria assim:

@Entity @Table(name = users) public class User { @Id @GenerationType(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String email; @Column(nullable = false) private String username; @Column(nullable = false) private String password; @Column(name = user_group) private String group; @CreationTimestamp @Column(nullable = false, columnDefinition = "datetime") private OffsetDateTime createdAt; }

Podemos usar a anotação @Data do Lombok para reduzir o boilerplate dos setters, getters e outros.

Criando relações entre tabelas

Para criar relações usamos algumas anotações como @ManyToOne, @OneToMany, @ManyToMany e @OneToOne. Anotamos nossas propriedades com a relação que queremos, nesse exemplo vamos ter um usuário que pode estar em apenas um grupo enquanto os grupos podem ter vários usuários.

@Entity @Table(name = groups) public class Group { @Id @GenerationType(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; // Atribuímos em mappedBy a propriedade respectiva @OneToMany(mappedBy = group) private List<User> users; }
@Entity @Table(name = users) public class User { @Id @GenerationType(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String email; @Column(nullable = false) private String username; @Column(nullable = false) private String password; /* * Para colunas relacionadas usamos @JoinColumn * para configurações ao invés do @Column */ @ManyToOne @JoinColumn(name = user_group) private Group group; @CreationTimestamp @Column(nullable = false, columnDefinition = "datetime") private OffsetDateTime createdAt; }

Relações de vários para vários criam novas tabelas apenas para o mapeamento, nesses casos anotamos a propriedade com @ManyToMany e configuramos com @JoinTable que pode receber alguns parâmetros como joinColumns que se refere à coluna que referencia a classe em que estamos e inverseJoinColumns que referencia á coluna que estamos relacionando. As relações podem ser unidirecionais ou bidirecionais. Em relações do primeiro tipo é necessário anotar apenas a classe que deve identificar essa relação, vamos fazer um exemplo em que vários usuários podem estar em vários grupos e vice-versa:

@Entity @Table(name = users) public class User { @Id @GenerationType(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String email; @Column(nullable = false) private String username; @Column(nullable = false) private String password; @ManyToMany @JoinTable( name = "user_groups", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "group_id") ) private List<Group> groups; @CreationTimestamp @Column(nullable = false, columnDefinition = "datetime") private OffsetDateTime createdAt; }

Em relações bidirecionais:

@Entity @Table(name = groups) public class Group { @Id @GenerationType(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; @ManyToMany(mappedBy = groups) private List<User> users; }

No próximo post veremos como criar repositórios facilmente com o Spring Data JPA usando uma des suas features mais interessantes: a criação automática em tempo de execução!