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 Spring
  • public 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 JpaTransactionManager
  • public 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)

Tags

#hibernate#jpa#restcontroller#codeexample#java#java11