Java Persistance API e Hibernate
La differenza tra Java Persistance API e Hibernate
Java persistance API astrae l’interazione con database, è un protocollo che viene implementato da Hibernate. JPA Definisce un insieme di interfacce standard e annotazioni che ti consentono di gestire dati relazionali nelle applicazioni Java.
Hibernate è un framework ORM (Object-Relational Mapping) che implementa JPA. Oltre alle specifiche standard del protocollo, fornisce funzionalità aggiuntive come il caching, il lazy loading, e query avanzate oltre che a supportare diversi dialetti di database.
Hibernate
Codice preso da InsecureSite
Per iniziare: un esempio di configurazione con i @Bean
@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableJpaRepositories(basePackages = "xyz.krsh.insecuresite")
public class InsecuresiteApplication {
public static void main(String[] args) {
SpringApplication.run(InsecuresiteApplication.class, args);
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
jpaVendorAdapter.setDatabase(Database.MYSQL);
jpaVendorAdapter.setGenerateDdl(true);
return jpaVendorAdapter;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean lemfb = new LocalContainerEntityManagerFactoryBean();
lemfb.setDataSource(dataSource());
lemfb.setJpaVendorAdapter(jpaVendorAdapter());
lemfb.setPackagesToScan("xyz.krsh.insecuresite");
return lemfb;
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/insecureshop");
dataSource.setUsername("insecureshop");
dataSource.setPassword("insecureshop");
return dataSource;
}
}I bean utilizzati:
public JpaTransactionManager transactionManager(EntityManagerFactory emf)- Questa classe si occupa di gestire le transazioni per le entità JPA in un applicazione Springpublic JpaVendorAdapter jpaVendorAdapter()- Interfaccia che implementa specifiche in base al vendor del database, nel mio caso serve a configurare MySQL, sarebbe diverso se usassi PostgreSQL e così via.public LocalContainerEntityManagerFactoryBean entityManagerFactory()- ti permette di configurare un EntityManager che poi verrà passato al JpaTransactionManagerpublic DataSource dataSource()- ci metti le informazioni per collegarti al database, nel mio esempio questo è embeddato nel codice, una altra soluzione è leggere da un file.properties
Un esempio di una tabella semplice rappresentata come ORM in Hibernate
Una volta configurato il database come sopra, si può passare ad inserire delle tabelle nel database gestendole interamente dal codice Java.
package xyz.krsh.insecuresite.rest.dao;
import java.util.Set;
import java.util.HashSet;
import javax.persistence.*;
@Entity
@Table(name = "Boardgame")
public class Boardgame {
@Id
private String name = null;
@Column
private float price = 0.0f;
@Column
private int quantity = 0;
@Column
private String description = null;
public Boardgame() { // Required by JPA
}
public Boardgame(String name) {
this.name = "";
if (name != null) {
this.name = name;
}
this.description = "";
}
public Boardgame(String name, float price, int quantity, String description) {
this.name = name;
this.price = price;
this.quantity = quantity;
this.description = description;
}
//Setters -
//Getters - SONO IMPORTANTI altrimenti non si ha alcun modo per ottenere i dati e altrimenti le repository non funzionano bene!
}Esempio di Repository associata all’esempio precedente
/*
* Implements the Repository pattern for Boardgame by extending the CrudRepository
*/
@Repository
public interface BoardgameRepository extends CrudRepository<Boardgame, String> {
List<Boardgame> findByNameContaining(String name);
List<Boardgame> findAll();
}Si possono definire ulteriori metodi custom, ad esempio findByNameContaining è un custom name
Esempio Query custom
/*
* Implements the Repository pattern for Boardgame by extending the CrudRepository
*/
@Repository
public interface OrdersRepository extends CrudRepository<Order, Integer> {
Optional<Order> findById(Integer id);
@Query("SELECT u FROM User u WHERE u.email = :email")
List<Order> findByCustomerName(@Param("email") String email);
List<Order> findAll();
}Si possono fare anche query più complesse.
Esempio complesso: Relazione Many to Many con attributi + custom repository + custom id
Note
ho ricevuto feedback da una collega con più senority sulla scelta di embeddare un Id che non è un long int ma una classe, è una scelta “stilistica” inusuale che non si vede in ambito business.
E’ meglio usare al posto di @EmbeddedId, un @Id e definire un long int id
Nota
In questo esempio manca la separazione della logica di business dal controller che viene effettuata con un Service Layer
(BoardgameService.java)>Per l’esempio più aggiornato vedi Github
@Entity
@Table(name = "Orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int orderId;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column
private Date orderDate;
public Order() {
}
//Setters
//Getters
}(Boardgame è nel primo esempio)
@Entity
public class OrderedBoardgames {
@EmbeddedId
private OrderedBoardgamesId id = new OrderedBoardgamesId(); // hai bisogno di una
// Classe quando devi fare una tabella many to many con attributi
// Questo perchè devi rappresentare un id campi multipli e quindi una classe
@ManyToOne
@MapsId("orderId")
private Order order;
@ManyToOne
@MapsId("boardgameName")
private Boardgame boardgame;
private int quantity;
public OrderedBoardgames() {
}
public OrderedBoardgames(OrderedBoardgamesId id, Order order, Boardgame boardgame, int quantity) {
this.id = id;
this.order = order;
this.boardgame = boardgame;
this.quantity = quantity;
}
//Setters
//Getters
}Nota come sia richiesta una classe OrderedBoardgamesId che serve per rappresentare una chiave primaria composta più attributi.
C’è un ulteriore commento da fare, nel codice precedente: quando si definisce il bean @MapsId("nome-tabella") deve essere lo stesso nome utilizzato nel codice successivo:
@Embeddable
public class OrderedBoardgamesId implements Serializable {
private static final long serialVersionUID = 1L;
private int orderId;
private String boardgameName;
public OrderedBoardgamesId() {
}
public OrderedBoardgamesId(int id, String name) {
orderId = id;
boardgameName = name;
}
//Setters
//Getters
}Altrimenti sul database si va a creare una tabella con un attributo chiave primaria che non viene riempito e viene generata una eccezione SQLException ogni volta che si prova ad inserire qualcosa.
“The field id doesn’t have a default value”
Ecco anche la repository con una query innestata
@Repository
public interface OrderedBoardgamesRepository extends CrudRepository<OrderedBoardgames, OrderedBoardgamesId> {
@Query("Select ob FROM OrderedBoardgames ob WHERE ob.order.orderId = :id")
Optional<OrderedBoardgames> findById(Integer id);
@Query("SELECT ob FROM OrderedBoardgames ob WHERE ob.order.orderId = (SELECT o.orderId FROM Order o WHERE o.user.email = :email)")
List<OrderedBoardgames> findByCustomerName(@Param("email") String email);
List<OrderedBoardgames> findAll();
}A questo link 🔗 c’è il controller completo, riporto giusto qualche snippet interessante:
Dichiarazione del controller e definizione delle repository
Il bean @Autowired significa che Spring risolverà e inietterà in automatico le dipendenze appropriate nella componente annotata.
@RestController
@RequestMapping("/api/orders")
public class OrdersController {
@Autowired
OrdersRepository ordersRepository;
@Autowired
BoardgameRepository boardgameRepository;
@Autowired
OrderedBoardgamesRepository orderedBoardgamesRepository;
@Autowired
UserRepository userRepo;
/* ... resto del codice */
}Metodo per farsi restituire l’utente loggato e metodo che controllo se sei admin
public String getLoggedUsername() throws UnauthorizedException {
// Get Authentication info
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
if ((userDetails.getUsername() instanceof String) == false) {
throw new UnauthorizedException();
}
return userDetails.getUsername();
}
public boolean isAdmin() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAdmin = authentication.isAuthenticated() && authentication.getAuthorities().stream()
.anyMatch(authority -> "admin".equals(authority.getAuthority()));
return isAdmin;
}Esempio complesso di aggiungi Boardgame ad un ordine
@RequestMapping("/{orderId}/addBoardgame")
public OrderedBoardgames addBoardgameToOrder(
@PathVariable("orderId") int orderId,
@RequestParam("boardgameName") String boardgameName, @RequestParam("quantity") int quantity)
throws ItemNotFoundException, UnauthorizedException {
Optional<Order> queryOrder = ordersRepository.findById(orderId);
Optional<Boardgame> queryBoardgame = boardgameRepository.findById(boardgameName);
OrderedBoardgames ob = null;
if (queryOrder.isPresent() && queryBoardgame.isPresent()) {
String email = this.getLoggedUsername();
if (queryOrder.get().getUser().getName() != email) {
throw new UnauthorizedException();
}
OrderedBoardgamesId id = new OrderedBoardgamesId(orderId, boardgameName);
ob = new OrderedBoardgames(id, queryOrder.get(), queryBoardgame.get(), quantity);
orderedBoardgamesRepository.save(ob);
} else {
throw new ItemNotFoundException();
}
return ob;
}
Delete/update dei record
- Prova ad usare @Modifying(clearAutomatically = true) e @Transactional come annotazioni nel metodo all’interno della repository
public interface BoardgameRepository extends CrudRepository<Boardgame, String> {
List<Boardgame> findByNameContaining(String name);
List<Boardgame> findAll();
@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Boardgame b SET b.quantity = :#{#boardgame.quantity}, b.price = :#{#boardgame.price}, b.description = :#{#boardgame.description} WHERE b.name = :#{#boardgame.name}")
void update(@Param("boardgame") Boardgame boardgame);
}Per eliminare invece, un altro modo che ho trovato è questo:
- Utilizza una query per farti restituire l’oggetto
- Cancella l’oggetto utilizzando il metodo già presente
.delete(TuoOggetto tuoOggetto)