Java design patterns, first part. Creational design patterns (Factory method, Singleton and Builder pattern)


Design patterns were developed as solutions for common problems that every developer faces. Design patterns are classified based on their main purpose; a design pattern may be any of the following types:

  •  Creational patterns
  •  Structural patterns
  •  Behavioral patterns

In this post, we are primarily focused on creational patterns and their implementation in Java.

Factory method

Factory method is a design pattern that centralizes the creation of objects that follow a hierarchical structure; this pattern is composed by the following elements:

  •  A factory class: In this case, as an example, we create the class SpritesFactory.
  • The product to be built: For this example, the Java objects built by the application.

Our example will be build using following hierarchical structure:
factory

Modelling our application

/*
*/
package com.raidentrance.model.stripes;

import java.awt.Graphics;

/**
 * @author raidentrance
 *
 */
public abstract class Sprite {
	protected int x;
	protected int y;
	protected int width;
	protected int heigth;

	public abstract void draw(Graphics g);

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getHeigth() {
		return heigth;
	}

	public void setHeigth(int heigth) {
		this.heigth = heigth;
	}
}

The Sprites class is an abstract class where we define common methods for all of the Sprites, such as: getX(), setX(int x), getY(), setY(int y), getWidth(),setWidth(int width), getHeigth() and  setHeigth(); as well as an abstract method called draw(Graphics g) that is different for each one of the implementations.

package com.raidentrance.model.stripes;

import java.awt.Graphics;

/**
 * @author raidentrance
 *
 */

public class Hero extends Sprite {

	@Override
	public void draw(Graphics g) {
		System.out.println("Drawing a hero");
	}
}
/*
**
*/
package com.raidentrance.model.stripes;

import java.awt.Graphics;

/**
 * @author raidentrance
 **/
public class BadBoy extends Sprite {
	@Override
	public void draw(Graphics g) {
		System.out.println("Drawing a bad boy");
	}
}

Hero.java and BadBoy.java are Sprite.java subclasses. We use the Factory method to instantiate these subclasses.

Creating the object factory class

The next step is to create the class responsible of following the Factory method design pattern. This class creates the instances within the application.

/**
*
*/
package com.raidentrance.factory;

import com.google.common.base.Preconditions;
import com.raidentrance.model.stripes.BadBoy;
import com.raidentrance.model.stripes.Hero;
import com.raidentrance.model.stripes.Sprite;

/**
 * @author raidentrance
 *
 */

public class SpritesFactory {
	public static enum SpriteType {
		HERO, BAD_BOY
	}

	public static Sprite createStripe(SpriteType type) {
		Preconditions.checkNotNull(type);
		switch (type) {
		case HERO:
			return new Hero();
		case BAD_BOY:
			return new BadBoy();
		}
		throw new IllegalArgumentException();
	}
}

There are two key points to highlight in the SpriteFactory class. Firstly, it contains an enumeration that defines all of the possible types of objects than can be built. Secondly, the method createStripes(StripeType type) receives an SpriteType as argument and depending on this argument it returns the corresponding instance.

Putting all together

Finally, we create a class to gather the classes previously described and observe how the objects are instantiated.

/**
*
*/
package com.raidentrance.factory;

import com.raidentrance.model.stripes.Sprite;

/**
 * @author raidentrance
 *
 */

public class Sample {
	/**
	 * @param args
	 */
	public static void main(String[] args) {

		Sprite sprite = SpritesFactory.createStripe(SpritesFactory.SpriteType.HERO);
		sprite.draw(null);
		Sprite sprite2 = SpritesFactory.createStripe(SpritesFactory.SpriteType.BAD_BOY);
		sprite2.draw(null);
	}
}

Output

Drawing a hero
Drawing a bad boy
Singleton pattern

Singleton pattern is used to define a class that contains a single instance and provides a single point of access to it.

In the following example, we create a Singleton class to define the set up parameters of an application.

Defining a class for error handling

/**
*
*/
package com.raidentrance.commons;

/**
 * @author raidentrance
 *
 */

public enum ErrorResult {

	EMPTRY_RESULT("%s cannot be null"), INVALID_ARGUMENT("%s is not a valid type");
	private String message;

	private ErrorResult(String message) {
		this.message = message;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}
}
/**
*
*/

package com.raidentrance.model;

/**
 * @author raidentrance
 *
 */

public class BussinessException extends Exception {

	private static final long serialVersionUID = 1788240335231977221L;

	public BussinessException(String msg) {
		super(msg);
	}
}

Defining the Singleton class

/**
*
*/

package com.raidentrance.singleton;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.google.common.base.Preconditions;
import com.raidentrance.commons.ErrorResult;
import com.raidentrance.model.BussinessException;

/**
 * @author raidentrance
 *
 */

public class Settings {
	private String host;
	private int port;
	private String context;
	private String username;
	private String password;
	private static Settings instance = new Settings();

	private Settings() {
	}

	public static Settings getInstance() {
		return instance;
	}

	public String getHost() {
		return host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public String getContext() {
		return context;
	}

	public void setContext(String context) {
		this.context = context;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getUrl() {
		Preconditions.checkNotNull(host,
				new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "host")));
		return host + ":" + port + context;
	}

	public String getCredentials() {
		return "Basic ".concat(Base64.getEncoder()
				.encodeToString(username.concat(":").concat(password).getBytes(StandardCharsets.UTF_8)));
	}
}

Points to highlight:

  1. Private constructor: It is fundamental to create a private constructor, by this means, the Singleton class is the only one capable to instantiate itself.
  2. Private static instance: It is necessary to define the instance as private and static, in order to make this variable accessible only inside the Singleton class.
  3. getInstance() method: This is the only point of access to the instance.

Putting all together.

/**
 *
 */
package com.raidentrance.singleton;

/**
 * @author raidentrance
 *
 */
public class Sample {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Settings settings = Settings.getInstance();
		System.out.println("Setting default settings.");
		settings.setHost("http://localhost");
		settings.setPort(8080);
		settings.setContext("/rest");
		settings.setUsername("user");
		settings.setPassword("SecretPassword");

		System.out.println(Settings.getInstance().getUrl());
		System.out.println(Settings.getInstance().getCredentials());

		System.out.println("Updating settings.");
		settings = Settings.getInstance();
		settings.setHost("http://192.168.5.1");
		settings.setUsername("prodUser");
		settings.setPassword("UltraSecretPassword");

		System.out.println(Settings.getInstance().getUrl());
		System.out.println(Settings.getInstance().getCredentials());
	}
}

Output

Setting default settings.
http://localhost:8080/rest
Basic dXNlcjpTZWNyZXRQYXNzd29yZA==
Updating settings.
http://72.51.22.13:8080/rest
Basic cHJvZFVzZXI6VWx0cmFTZWNyZXRQYXNzd29yZA==

As shown in this example, it doesn’t really matter how many times you invoke the getInstance() method, this will always return the same instance.

Builder Pattern

This design pattern is used to build complex object step by step.

Modelling our application

/**
 *
 */
package com.raidentrance.model;

import java.util.Date;

/**
 * @author raidentrance
 *
 */
public class Person {
	private String name;
	private Date birthDate;
	private int age;

	public Person() {
	}

	public Person(String name, Date birthDate, int age) {
		super();
		this.name = name;
		this.birthDate = birthDate;
		this.age = age;
	}

	public String getName() {
		return name;
	}

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

	public Date getBirthDate() {
		return birthDate;
	}

	public void setBirthDate(Date birthDate) {
		this.birthDate = birthDate;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + ", birthDate=" + birthDate + ", age=" + age + "]";
	}
}

 

/**
 *
 */
package com.raidentrance.model;

/**
 * @author raidentrance
 *
 */
public class Book {
    private String isbn;
    private String title;
    private String author;
    private double price;

    public Book() {
    }

    public Book(String isbn, String title, String author, double price) {
        super();
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.price = price;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book [isbn=" + isbn + ", title=" + title + ", author=" + author + ", price=" + price + "]";
    }

}

Helper class to calculate the age

The following class calculates a person’s age based on her birthday.

/**
 *
 */
package com.raidentrance.commons;

import java.util.Calendar;
import java.util.Date;

/**
 * @author raidentrance
 *
 */
public class TimeHelper {
    public static int getAge(Date birthDate) {
        Calendar dob = Calendar.getInstance();
        dob.setTime(birthDate);
        Calendar today = Calendar.getInstance();
        int age = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR);
        if (today.get(Calendar.MONTH) < dob.get(Calendar.MONTH)) {
            age--;
        } else if (today.get(Calendar.MONTH) == dob.get(Calendar.MONTH)
                && today.get(Calendar.DAY_OF_MONTH) < dob.get(Calendar.DAY_OF_MONTH)) {
            age--;
        }
        return age;
    }
}

Creating an abstract builder

An abstract builder class allows you to follow the same structure in all of the builders that you might want to develop. In this example, we create two builders, the first one to create Person objects and the second one to create Book objects, both of them extend AbstractBuilder.

/**
 *
 */
package com.raidentrance.builder.commons;

/**
 * @author raidentrance
 *
 */
public abstract class AbstractBuilder<T> {
    protected T instance;

    public AbstractBuilder(T instance) {
        this.instance = instance;
    }

    public T build() {
        inject();
        validate();
        return instance;
    }

    public abstract void validate();

    public abstract void inject();
}

Points to highlight on the AbstractBuilder class:

  1. This class has a generic instance that will contain the object that we want to build.
  2. Its constructor receives this instance as argument.
  3. It contains three important methods:
    • inject(): This method is used to generate values based on the attributes that it has.
    • validate(): This method validates that all of the prerequisites to correctly build an object are accomplished.
    • build(): This method calls the inject() and validate() method, and returns the instance.

Creating builders

PersonBuilder.java

/**
 *
 */
package com.raidentrance.builder;

import java.util.Date;

import com.google.common.base.Preconditions;
import com.raidentrance.builder.commons.AbstractBuilder;
import com.raidentrance.commons.ErrorResult;
import com.raidentrance.commons.TimeHelper;
import com.raidentrance.model.BussinessException;
import com.raidentrance.model.Person;

/**
 * @author raidentrance
 *
 */
public class PersonBuilder extends AbstractBuilder<Person> {

    public PersonBuilder() {
        super(new Person());
    }

    public PersonBuilder setName(String name) {
        instance.setName(name);
        return this;
    }

    public PersonBuilder setBirthDate(Date birthDate) {
        instance.setBirthDate(birthDate);
        return this;
    }

    @Override
    public void validate() {
        Preconditions.checkNotNull(instance.getName(),
                new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Name")));
        Preconditions.checkNotNull(instance.getBirthDate(),
                new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Birth date")));
        Preconditions.checkNotNull(instance.getAge(),
                new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Age")));
    }

    @Override
    public void inject() {
        Preconditions.checkNotNull(instance.getBirthDate(),
                new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Birth date")));
        instance.setAge(TimeHelper.getAge(instance.getBirthDate()));
    }
}

Points to highlight on the PersonBuilder class:

  1. The PersonBuilder constructor sends a new Person instance as argument.
  2. Each setter method returns a PersonBuilder instance.
  3. The inject() method is used to generate the age based on a birthday.
  4. The validate() method validates that the variables are not null.
  5. The build() method is already defined on the AbstractBuilder class and it returns a complete and correctly formed instance.

BookBuilder.java

package com.raidentrance.builder;

import com.google.common.base.Preconditions;
import com.raidentrance.builder.commons.AbstractBuilder;
import com.raidentrance.commons.ErrorResult;
import com.raidentrance.model.Book;
import com.raidentrance.model.BussinessException;

/**
 * @author raidentrance
 *
 */
public class BookBuilder extends AbstractBuilder<Book> {

    public BookBuilder() {
        super(new Book());
    }

    public BookBuilder setIsbn(String isbn) {
        instance.setIsbn(isbn);
        return this;
    }

    public BookBuilder setTitle(String title) {
        instance.setTitle(title);
        return this;
    }

    public BookBuilder setAuthor(String author) {
        instance.setAuthor(author);
        return this;
    }

    public BookBuilder setPrice(double price) {
        instance.setPrice(price);
        return this;
    }

    @Override
    public void validate() {
        Preconditions.checkNotNull(instance.getIsbn(),
                new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Isbn")));
    }

    @Override
    public void inject() {

    }
}

You can find the code previously explained in the following link:  https://github.com/raidentrance/design-patterns

Author: Alejandro Agapito Bautista

Twitter: @raidentrance

e-mail:raidentrance@gmail.com

Translated By: Oscar Camacho – melchior01

Contact: adamfirstangel@hotmail.com

Aplicación standAlone JPA + Hibernate +Maven en Español !


En este post se explicará paso a paso como crear una aplicación Stand alone que accede a una base de datos Mysql utilizando la implementación de Hibernate para JPA.

Configuración

El primer paso es definir las dependencias a utilizar en el proyecto, para este caso se utilizarán las siguientes:

<dependencies>
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-entitymanager</artifactId>
		<version>5.2.3.Final</version>
	</dependency>

	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<version>5.1.3</version>
	</dependency>
</dependencies>

Las dependencias a utilizar son :

  • hibernate-entitymanager : Implementación de Hibernate para JPA
  • mysql-connector-java : Driver de MySQL (Puede variar dependiendo del manejador a utilizar)

Definir tablas a utilizar, en este ejemplo se utilizará solo una tabla llamada USER:

CREATE TABLE USER(
USER_ID INTEGER PRIMARY KEY AUTO_INCREMENT,
USERNAME VARCHAR(100) NOT NULL,
PASSWORD VARCHAR(100) NOT NULL
);

Una vez definidas las dependencias y la tabla a utilizar se debe crear el archivo de configuración más importante en JPA este es el archivo persistence.xml y se debe colocar en el folder META-INF dentro de la carpeta src/main/resources. La estructura quedaría del siguiente modo src/main/resources/META-INF/persistence.xml con lo siguiente:

<?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="MyPersistenceUnit" 		transaction-type="RESOURCE_LOCAL">
		<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

		<class>com.raidentrance.entities.User</class>
		<properties>
			<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
			<property name="javax.persistence.jdbc.url" 				value="jdbc:mysql://localhost:3306/jpaexample?zeroDateTimeBehavior=convertToNull" />
			<property name="javax.persistence.jdbc.user" value="root" />
			<property name="javax.persistence.jdbc.password" value="root" />
		</properties>
	</persistence-unit>
</persistence>

Crear entidad a utilizar

JPA es un framework ORM, por esto es necesario definir una entidad Java que represente la tabla que se utiliza en la base de datos.

/**
 *
 */
package com.raidentrance.entities;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * @author raidentrance
 *
 */
@Entity
@Table(name = "USER")
public class User implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "USER_ID")
	private Integer userId;

	@Column(name = "USERNAME")
	private String username;

	@Column(name = "PASSWORD")
	private String password;

	private static final long serialVersionUID = -1382782006959182944L;

	public User() {
	}

	public User(String username, String password) {
		super();
		this.username = username;
		this.password = password;
	}

	public User(Integer userId, String username, String password) {
		this.userId = userId;
		this.username = username;
		this.password = password;
	}

	public Integer getUserId() {
		return userId;
	}

	public void setUserId(Integer userId) {
		this.userId = userId;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((userId == null) ? 0 : userId.hashCode());
		result = prime * result + ((username == null) ? 0 : username.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;
		User other = (User) obj;
		if (userId == null) {
			if (other.userId != null)
				return false;
		} else if (!userId.equals(other.userId))
			return false;
		if (username == null) {
			if (other.username != null)
				return false;
		} else if (!username.equals(other.username))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "User [userId=" + userId + ", username=" + username + "]";
	}

}
  • Por cada campo definido en la base de datos existe un campo definido en la entidad JPA que lo representa con esto cuando se realice una consulta en lugar de recibir un ResultSet como en Jdbc recibiremos un List lo cual facilitará el desarrollo.

Creando un contexto para la aplicación

Como este ejemplo no depende de Spring la aplicación debe ser capaz de crear los objetos y administrarlo.

/**
 *
 */
package com.raidentrance.util;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

/**
 * @author raidentrance
 *
 */
public class ApplicationContext {

	private EntityManagerFactory entityManagerFactory;

	private static ApplicationContext instance = new ApplicationContext();

	private ApplicationContext() {
		entityManagerFactory = Persistence.createEntityManagerFactory("MyPersistenceUnit");
	}

	public static ApplicationContext getInstance() {
		return instance;
	}

	public EntityManager getEntityManager() {
		return entityManagerFactory.createEntityManager();
	}

	public void closeEntityManager() {
		entityManagerFactory.close();
	}

}

Como se puede observar  este contexto sigue el patron de diseño Singleton y es responsable de crear un EntityManagerFactory e instancias de EntityManager.

El objeto EntityManager será el responsable de acceder a la base de datos, crear las consultas sql, traducir las respuestas a objetos java, entre otras cosas.

Creando un DAO abstracto

DAO es un patrón de diseño que significa ( Data access object ) el cual es una interfaz de acceso a una base de datos en una aplicación. En este ejemplo se creará un DAO abstracto el cuál definirá acciones comunes CRUD (Create, read, update y delete). Con esto las implementaciones de este ya no tendrán que reescribir código para estas acciones comunes.

/**
 *
 */
package com.raidentrance.dao;

/**
 * @author raidentrance
 *
 */
import java.util.List;

import javax.persistence.EntityManager;

public abstract class AbstractFacade<T> {

	private final Class<T> entityClass;
	private EntityManager entityManager;

	public AbstractFacade(Class<T> entityClass, EntityManager entityManager) {
		this.entityClass = entityClass;
		this.entityManager = entityManager;
	}

	protected EntityManager getEntityManager() {
		return entityManager;
	}

	public void create(T entity) {
		getEntityManager().getTransaction().begin();
		getEntityManager().persist(entity);
		getEntityManager().getTransaction().commit();
	}

	public void edit(T entity) {
		getEntityManager().getTransaction().begin();
		getEntityManager().merge(entity);
		getEntityManager().getTransaction().commit();
	}

	public void remove(T entity) {
		getEntityManager().getTransaction().begin();
		T find = getEntityManager().find(entityClass, entity);
		getEntityManager().remove(find);
		getEntityManager().getTransaction().commit();
	}

	public T find(Object id) {
		return getEntityManager().find(entityClass, id);
	}

	public List<T> findAll() {
		javax.persistence.criteria.CriteriaQuery<T> cq = getEntityManager().getCriteriaBuilder()
				.createQuery(entityClass);
		cq.select(cq.from(entityClass));
		return getEntityManager().createQuery(cq).getResultList();
	}

	public void close() {
		entityManager.close();
	}
}

Como se puede observar, en lugar de utilizar alguna entidad en específico se define un dato genérico que será definido en la clase hija.

Creando DAO para usuarios

El siguiente paso es crear un DAO específico para la clase Usuario que es la entidad que se creó para este ejemplo. En esta clase ya no se deben implementar todas las acciones CRUD ya que estas ya se encuentran definidas en el DAO abstracto.

/**
 *
 */
package com.raidentrance.dao;

import javax.persistence.EntityManager;

import com.raidentrance.entities.User;

/**
 * @author raidentrance
 *
 */
public class UserDao extends AbstractFacade<User> {

	public UserDao(EntityManager entityManager) {
		super(User.class, entityManager);
	}

}

Lo bueno de este tipo de patrón es que si se necesita crear 100 entidades, no es necesario reescribir el código para las operaciones CRUD para todos, solo es necesario heredar de el DAO abstracto.

Ejecutar la aplicación

El último paso es crear la clase principal utilizará todos los componentes creados para persistir objetos de tipo usuario y para obtenerlos de la base de datos.

/**
 *
 */
package com.raidentrance;

import java.util.List;
import java.util.Random;
import java.util.logging.Logger;

import javax.persistence.EntityManager;

import com.raidentrance.dao.UserDao;
import com.raidentrance.entities.User;
import com.raidentrance.util.ApplicationContext;

/**
 * @author raidentrance
 *
 */
public class JpaApplication {
	private static final Logger log = Logger.getLogger(JpaApplication.class.getName());

	public static void main(String[] args) {
		ApplicationContext context = ApplicationContext.getInstance();
		EntityManager entityManager = context.getEntityManager();

		UserDao dao = new UserDao(entityManager);

		User userEntity = new User("raidentrance ".concat(new Integer(new Random(100).nextInt()).toString()), "López");
		dao.create(userEntity);

		List<User> list = dao.findAll();
		for (User user : list) {
			log.info(user.toString());
		}

		dao.close();
		context.closeEntityManager();
	}
}

Conclusión

En este ejemplo se explica como acceder a una base de datos utilizando JPA sin depender de otros frameworks como Spring data, Spring Hibernate, etc. Sólo se utiliza la implementación de Hibernate de JPA, el driver de Jdbc de MySQL y patrones de diseño como AbstractFacade, DAO y Singleton.

Puedes encontrar el código completo del ejemplo en el siguiente enlace:

https://github.com/raidentrance/jpa-example

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com