pippo-java / pippo.github.io

Pippo site repository
5 stars 9 forks source link

Add "Database" section #72

Open decebals opened 5 years ago

decebals commented 5 years ago

Explain how to deal with database. It's simple and natural to work with micro/lite JDBC frameworks like:

Show some examples code from real life application.

mhagnumdw commented 5 years ago

I use only JPA/Hibernate + Guice Persist. If you want I can write about it later.

mhagnumdw commented 5 years ago

The steps below describe how to use Pippo with JPA/Hibernate + Guice Persist

In this example

Let's now configure JPA/Hibernate + Guice Persist for use in our application.

Initially we need to define some dependencies in pom.xml

<properties>
    <hibernate.version>5.2.1.Final</hibernate.version>
    <google.guice.version>4.0</google.guice.version>
</properties>

<dependencies>
    <!-- Engine to Pippo convert to Json -->
    <dependency>
        <groupId>ro.pippo</groupId>
        <artifactId>pippo-gson</artifactId>
        <version>${pippo.version}</version>
    </dependency>

    <!-- In this example we will work with Controllers -->
    <dependency>
        <groupId>ro.pippo</groupId>
        <artifactId>pippo-controller</artifactId>
        <version>${pippo.version}</version>
    </dependency>

    <!-- Our controllers will be able to inject Guice components -->
    <dependency>
        <groupId>ro.pippo</groupId>
        <artifactId>pippo-guice</artifactId>
        <version>${pippo.version}</version>
    </dependency>

    <!-- Hibernate -->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>${hibernate.version}</version>
    </dependency>

    <!-- Guice extension to manager the EntityManager lifecycle -->
    <dependency>
        <groupId>com.google.inject.extensions</groupId>
        <artifactId>guice-persist</artifactId>
        <version>${google.guice.version}</version>
    </dependency>

    <!-- H2 database driver -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.194</version>
    </dependency>
</dependencies>

Now we need to set up a JPA peristence unit in /src/main/java/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
    xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

    <persistence-unit name="pippojpaPersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- Oracle -->
            <!--
            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle12cDialect" />
            <property name="javax.persistence.jdbc.driver" value="oracle.jdbc.OracleDriver"/>
             -->
            <!-- Postgres -->
            <!--
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL95Dialect" />
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
             -->
            <!-- H2 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>

            <!-- Hibernate 5.2: none (default value), create-only, drop, create, create-drop, validate, and update -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

            <!--
            <property name="hibernate.hbm2ddl.import_files" value="importInicial.sql,import.sql" />
             -->

            <property name="javax.persistence.jdbc.url" value="jdbc:h2:./db/h2/pippojpa/pippojpa;AUTO_SERVER=true" />
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value="sa"/>

            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.use_sql_comments" value="true" />
            <property name="hibernate.format_sql" value="false" />

            <!-- AUTO  : The Session is sometimes flushed before query execution -->
            <!-- COMMIT: The Session is only flushed prior to a transaction commit -->
            <property name="org.hibernate.flushMode" value="COMMIT"/> <!-- default: AUTO -->

            <property name="hibernate.connection.autocommit" value="false"/> <!-- default is false -->

            <property name="hibernate.connection.pool_size" value="3"/>

            <property name="hibernate.enable_lazy_load_no_trans" value="false"/> <!-- default is false -->
        </properties>
    </persistence-unit>

</persistence>

We will need a filter to enable the HTTP Request unit of work and to have guice-persist manage the lifecycle of active units of work. In other words: to manage the EntityManager during the HTTP request lifecycle.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Injector;
import com.google.inject.persist.UnitOfWork;

import ro.pippo.core.route.RouteContext;
import ro.pippo.core.route.RouteHandler;

/**
 * Filter created based on {@link com.google.inject.persist.PersistFilter}.
 * <p>
 * https://github.com/google/guice/wiki/JPA
 * </p>
 */
class PersistFilter implements RouteHandler {

    private static final Logger log = LoggerFactory.getLogger(PersistFilter.class);

    private static final String KEY_UNIT_OF_WORK = "UnitOfWork";

    private final boolean begin;
    private final Injector injector;

    private PersistFilter() {
        throw new RuntimeException("instancing is not allowed");
    }

    private PersistFilter(Injector injector, boolean begin) {
        this.begin = begin;
        this.injector = injector;
    }

    static PersistFilter buildBegin(Injector injector) {
        return new PersistFilter(injector, true);
    }

    static PersistFilter buildEnd() {
        return new PersistFilter(null, false);
    }

    @Override
    public void handle(RouteContext routeContext) {
        log.trace("PersistFilter, begin: {} (end: {})", begin, !begin);
        if (begin) { // begin
            final UnitOfWork unitOfWork = begin();
            routeContext.setLocal(KEY_UNIT_OF_WORK, unitOfWork);
            routeContext.next();
        } else { // end
            final UnitOfWork unitOfWork = routeContext.removeLocal(KEY_UNIT_OF_WORK);
            if (unitOfWork != null) {
                end(unitOfWork);
            }
        }
    }

    private UnitOfWork begin() {
        log.trace("unitOfWork.begin");
        UnitOfWork unitOfWork = injector.getInstance(UnitOfWork.class);
        unitOfWork.begin();
        return unitOfWork;
    }

    private void end(UnitOfWork unitOfWork) {
        log.trace("unitOfWork.end");
        if (unitOfWork != null) {
            unitOfWork.end();
        }
    }

}

For Guice to manage the EntityManager it needs to know the persistence unit to create the instances in the dependency injections. Let's then create a Guice Module pointing to our persistence unit. It's very simple.

import com.google.inject.Binder;
import com.google.inject.Module;
import com.google.inject.persist.jpa.JpaPersistModule;

public final class JPAGuiceModule implements Module {

    @Override
    public void configure(Binder binder) {
        binder.install(new JpaPersistModule("pippojpaPersistenceUnit"));
    }

}

Let's now build a JPA entity, a DAO, and a controller to test our configuration.

Below is a simple JPA entity called City.

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class City implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID", unique = true, updatable = false, nullable = false)
    private Long id;

    @Column(name = "NAME", nullable = false)
    private String name;

    public City() {
        super();
    }

    public City(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "City [id=" + id + ", name=" + name + "]";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        City other = (City) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }

}

CityDAO.java Here is a very simple implementation just to demonstrate this use case.

import java.io.Serializable;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.persist.Transactional;

public class CityDAO {

    @Inject
    private Provider<EntityManager> emProvider;

    private EntityManager getEntityManager() {
        return emProvider.get();
    }

    // Transaction is not required
    public List<City> getAll() {
        final String qlString = "FROM " + City.class.getName();
        TypedQuery<City> query = getEntityManager().createQuery(qlString, City.class);
        return query.getResultList();
    }

    // Transaction is not required
    public City getById(Serializable id) {
        return getEntityManager().find(City.class, id);
    }

    @Transactional
    public City save(City city) {
        City entity = persist(city);
        getEntityManager().flush();
        return entity;
    }

    @Transactional
    public void deleteById(Serializable id) {
        City entity = getEntityManager().find(City.class, id);
        // Case of attached entity - simply remove it
        if (getEntityManager().contains(entity)) {
            getEntityManager().remove(entity);
        } else {
            // Case of unattached entity, first it is necessary to perform
            // a merge, before doing the remove
            entity = getEntityManager().merge(entity);
            getEntityManager().remove(entity);
        }
    }

    private <T> T persist(T entity) {
        // Case of new, non-persisted entity
        if (extractId(entity) == null) {
            getEntityManager().persist(entity);
        } else if (!getEntityManager().contains(entity)) {
            // In the case of an attached entity, we do nothing (it
            // will be persisted automatically on synchronisation)
            // But... in the case of an unattached, but persisted entity
            // we perform a merge to re-attach and persist it
            entity = getEntityManager().merge(entity);
        }
        return entity;
    }

    private Object extractId(Object entity) {
        return getEntityManager().getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity);
    }

}

CityController.java

import java.util.List;

import com.google.inject.Inject;

import ro.pippo.controller.Controller;
import ro.pippo.controller.DELETE;
import ro.pippo.controller.GET;
import ro.pippo.controller.POST;
import ro.pippo.controller.Path;
import ro.pippo.controller.Produces;
import ro.pippo.controller.extractor.Bean;
import ro.pippo.controller.extractor.Param;

@Path("/pages")
public class CityController extends Controller {

    @Inject
    private CityDAO cityDAO;

    @GET("/city/getAll")
    @Produces(Produces.JSON)
    public List<City> cities() {
        return cityDAO.getAll();
    }

    @GET("/city/get/{id}")
    @Produces(Produces.JSON)
    public City getCity(@Param("id") Long id) {
        return cityDAO.getById(id);
    }

    @DELETE("/city/delete/{id}")
    @Produces(Produces.TEXT)
    public String delete(@Param("id") Long id) {
        cityDAO.deleteById(id);
        return "City with id " + id + " was deleted";
    }

    @POST("/city/save")
    @Produces(Produces.JSON)
    public City save(@Bean City city) {
        return cityDAO.save(city);
    }

}

Finally we need to make final configurations in the PippoApplication.onInit()

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.persist.PersistService;

import ro.pippo.controller.ControllerApplication;
import ro.pippo.guice.GuiceControllerFactory;

public class PippoApplication extends ControllerApplication {
    @Override
    protected void onInit() {
        // http://docs.jboss.org/hibernate/orm/5.2/topical/html_single/logging/Logging.html
        System.setProperty("org.jboss.logging.provider", "slf4j"); // Required for Hibernate when using slf4j

        Module guiceModule = new JPAGuiceModule();
        Injector injector = Guice.createInjector(guiceModule); // create guice injector
        setControllerFactory(new GuiceControllerFactory(injector)); // register GuiceControllerFactory

        PersistService service = injector.getInstance(PersistService.class);
        service.start(); // starts the persistence unit

        // This filter should be applied only to contexts that may require the EntityManager
        ANY("/pages/.*", PersistFilter.buildBegin(injector));

        addControllers(CityController.class);

        // Here your other controllers, routes, configurations, ...

        // Cleanup - It should actually be the last call of the onInit() method
        ANY("/pages/.*", PersistFilter.buildEnd()).runAsFinally();
    }
}

Here we have Pippo fully configured to work with Hibernate / JPA, including with the automatic management of resources allocated by EntityManager through the HTTP filter created.

Launch the application... Lets test... Let's use cURL to make requests.

$ # getting all cities, must be empty
$ curl http://localhost:8338/pages/city/getAll
[]

$ # inserting three cities and getting the IDs
$ curl -X POST http://localhost:8338/pages/city/save -H 'content-type: multipart/form-data' -F name=city1
{"id":1,"name":"city1"}
$ curl -X POST http://localhost:8338/pages/city/save -H 'content-type: multipart/form-data' -F name=city2
{"id":2,"name":"city2"}
$ curl -X POST http://localhost:8338/pages/city/save -H 'content-type: multipart/form-data' -F name=city3
{"id":3,"name":"city3"}

$ # getting all cities (there must be three)
$ curl http://localhost:8338/pages/city/getAll
[{"id":1,"name":"city1"},{"id":2,"name":"city2"},{"id":3,"name":"city3"}]

$ # query the city with ID 1
$ curl http://localhost:8338/pages/city/get/1
{"id":1,"name":"city1"}

$ # delete the city with ID 2
$ curl -X DELETE http://localhost:8338/pages/city/delete/2
City with id 2 was deleted

$ # getting all cities, but the city with ID 2 no longer available
$ curl http://localhost:8338/pages/city/getAll
[{"id":1,"name":"city1"},{"id":3,"name":"city3"}]

$ # done!
mhagnumdw commented 5 years ago

:arrow_up: @decebals, let me know about any improvement. I put a complete example. Maybe you want to reduce something.

decebals commented 5 years ago

@mhagnumdw It's OK. Thanks!