JDBI y el patrón Especificación

Advertisements

JDBI es una biblioteca para acceder a datos de relaciones en Java, proporciona una forma conveniente e idiomática de interactuar fácilmente con la capa de persistencia. Al igual que otros marcos/bibliotecas de persistencia, se basa en JDBC y proporciona un enfoque diferente a ORM (JPA, Hibernate, Spring Data) para interactuar con la base de datos, orientado a ejecutar las sentencias SQL a través de un API Fluido o un API Declarativo (usando anotaciones).

JDBI es una buena opción para algunos proyectos, donde la sobrecarga del mapeo JPA y, al mismo tiempo, la sobrecarga de trabajar directamente con JDBC no son necesarias ni deseadas. Proporcionará muchas funciones útiles para simplificar la vida de los desarrolladores.

Sin embargo, no proporciona una función lista para usar para implementar el patrón de especificación, como ya proporciona Spring JPA Data. Entonces, si queremos implementar el patrón de especificación usando JDBI, necesitaremos crear un código personalizado para admitirlo. Afortunadamente, requiere solo unas pocas clases para hacerlo.

En este artículo, comparto un código para implementar el patrón de especificación en JDBI.

Implementando el patrón Especificación en JDBI

La interface JdbiSpecification

Comenzamos creando una nueva interfaz JdbiSpecification con algunos métodos abstractos, que se implementarán.

import org.skife.jdbi.v2.Query;

public interface JdbiSpecification {
  String toSQL();
  Query<Map<String, Object>> bind(Query<Map<String, Object>> query);
}

El método toSQL devolverá el fragmento de un SQL para esa especificación. El método bind se utilizará para enlazar los parámetros en el fragmento de SQL.

Creando una especificación personalizada

Con la interfaz Specification, podemos crear su propia especificación personalizada.

import org.skife.jdbi.v2.Query;

public class PersonIdEquals implements JdbiSpecification{
  private long id;
  public PersonIdEquals(long id){
    this.id = id;
  }

  public String toSQL(){
    return "p.id = :personId";
  }
  public Query<Map<String, Object>> bind(Query<Map<String, Object>> query){
    return query.bind("personId", id);
  }
}

Podemos crear tantas “especificaciones personalizadas” como necesitemos, usando otros campos y otras operaciones como; no igual, IN, menor, mayor que, etc.

import org.skife.jdbi.v2.Query;

public class PersonStatusIn implements JdbiSpecification{
  private List<String> status;
  private final String bindingName = "personaStatus";
  
  public PersonStatusIn(List<String> status){
    this.status = status;
  }

  public String toSQL(){
    var index = 0;
    var preparedInParameters = "";
    var statusSize = status.size();
    while (index < statusSize) {
      preparedInParameters = preparedInParameters + ":"+bindingName+"_"+index;
      index++;
      if (index < statusSize) {
        preparedInParameters = preparedInParameters + ", ";
      }
    }
    return "p.status IN ("+preparedInParameters+")";
  }
  
  public Query<Map<String, Object>> bind(Query<Map<String, Object>> query){
    var boundedQuery = query;
    var index = 0;
    while (index < statusSize.size) {
      boundedQuery = boundedQuery.bind(bindingName+"_"+index, status.get(index));
      index++;
    }
    return boundedQuery;
  }
}

Implementaciones de Operaciones SQL

Para obtener los resultados esperados de una base de datos, usamos diferentes operaciones SQL mientras creamos la oración de consulta SQL, como: AND, OR, GroupBy, OrderBy. Así que proporcionemos apoyo para algunas de esas operaciones. Entonces, creemos algunas especificaciones personalizadas para ese propósito.

Para aquellas operaciones que contendrán solo una especificación, podemos usar la siguiente clase.

public abstract class AbstractClauseSpecification extends JdbiSpecification{
  
  protected JdbiSpecification specification;
  
  public AbstractClauseSpecification(JdbiSpecification specification) {
    this.specification = specification;
  }

  @Override
  public Query<Map<String, Object>> bind(Query<Map<String, Object>> query){
    return specification.bind(query);
  }
}

Para aquellas operaciones que interactúan con 2 especificaciones podemos usar la siguiente clase.

public abstract class AbstractCompositeClauseSpecification extends JdbiSpecification{
  
  protected JdbiSpecification first;
  protected JdbiSpecification second;
  
  public AbstractClauseSpecification(JdbiSpecification first, JdbiSpecification second) {
    this.first = first;
    this.second = second;
  }

  @Override
  public Query<Map<String, Object>> bind(Query<Map<String, Object>> query){
    var newQuery = first.bind(query);
    return second.bind(newQuery);
  }
}

Y ahora, podemos crear las operaciones.

Advertisements

AndSpecification

public class AndSpecification extends AbstractCompositeClauseSpecification {
  public final String AND_OPERATOR = "AND";
  
  public AndSpecification(JdbiSpecification first, JdbiSpecification second){
    super(first, second);
  }
  
  @Override 
  public String toSQL(){
    return first.toSQL+" "+AND_OPERATOR+" "+second.toSQL();
  }
}

OrSpecification

public class OrSpecification extends AbstractCompositeClauseSpecification {
  public final String OR_OPERATOR = "OR";
  
  public OrSpecification(JdbiSpecification first, JdbiSpecification second){
    super(first, second);
  }
  
  @Override 
  public String toSQL(){
    return first.toSQL+" "+OR_OPERATOR+" "+second.toSQL();
  }
}

WhereSpecification

public class WhereSpecification extends AbstractClauseSpecification {
  public final String WHERE_OPERATOR = "WHERE";
  
  public WhereSpecification(JdbiSpecification specification){
    super(specification);
  }
  
  @Override 
  public String toSQL(){
    return " "+WHERE_OPERATOR+" "+specification.toSQL();
  }
}

GroupBySpecification

public class GroupBySpecification extends AbstractClauseSpecification {
  public final String GROUPBY_OPERATOR = "GROUP BY";
  private List<String> groupFields;
  
  public GroupBySpecification(JdbiSpecification specification, List<String> groupFields){
    super(specification);
    this.groupFields = groupFields;
  }
  
  @Override 
  public String toSQL(){
    var groupBy = groupFields.stream()
      .collect(Collectors.joining(", "));
    return specification.toSQL()+" "+GROUPBY_OPERATOR+" "+groupBy";
  }
}

OrderBySpecification

public class OrderBySpecification extends AbstractClauseSpecification {
  public final String ORDERBY_OPERATOR = "ORDER BY";
  private List<SortField> sortFields;
  
  public OrderBySpecification(JdbiSpecification specification, List<SortField> sortFields){
    super(specification);
    this.sortFields = sortFields;
  }
  
  @Override 
  public String toSQL(){
    var orderBy = sortFields.stream()
      .map(sf -> {
        var sortOrder = sf.sortOrder != null ? " "+sf.sortOrder.toString : "";
        return " "+sf.field+sortOrder;
      })
      .collect(Collectors.joining(", "));
    return specification.toSQL()+" "+ORDERBY_OPERATOR+" "+orderBy";
  }
  
  public static class SortField {
    private String field;
    private SortOrder sortOrder;
    
    public SortField(String field){
      this.field = field;
    }
    
    public SortField(String field, SortOrder order){
      this.field = field;
      this.sortOrder = order;
    }
  }
  
  public enum SortOrder {
    ASC, DESC;
  }
}

GroupedSpecification

public class GroupedSpecification extends AbstractClauseSpecification {

  public GroupedSpecification(JdbiSpecification specification){
    super(specification);
  }
  
  @Override 
  public String toSQL(){
   return "("+specification.toSQL+")";
  }
}

Esas son las especificaciones de la cláusula principal que podríamos necesitar para crear la mayoría de las consultas.

El SpecificationExecutor

Porque la idea detrás del patrón de especificación es tener algunos métodos comunes que crearán y ejecutarán las consultas para nosotros, en función de las especificaciones enviadas como parámetro. En Spring Framework, esto se hace a través del JpaSpecificationExecutor, siguiendo el mismo patrón crearemos nuestro JdbiSpecificationExecutor.

Se hace usando una interfaz con algunos métodos predeterminados. ¿Por qué no una clase abstracta? Podríamos usar una clase abstracta para ese propósito, pero estamos proporcionando comportamiento, no herencia, por lo que una interfaz es un mejor enfoque. Además, otra razón es que en JDBI podemos crear los objetos DBI (DAO o Repository) como interfaces o clases abstractas, por lo que este enfoque permitirá usar JdbiSpecificationExecutor en ambos casos.

import org.skife.jdbi.v2.sqlobject.mixins.GetHandle;
import org.skife.jdbi.v2.tweak.ResultSetMapper;

public interface JdbiSpecificationExecutor<T> extends GetHandle {

  protected ResultSetMapper<T> getMapper();
  protected String getSelectQuery();
  
  default List<T> getBy(JdbiSpecification specification) {
      withHandle(
        handle -> {
          var mapper = getMapper();  //Method implemented in the DAO
          var filterClause = specification.toSQL();
          if(sql.trim.startsWith(WhereSpecification.WHERE_OPERATOR){
            filterClause = " WHERE " + sql;
          }
          var sql = getSelectQuery() + filterClause;
          var query = handle.createQuery(sql);
          specification.bind(query)
            .map(mapper)
            .list();
        }
      )
  }

  default Optional<T> findBy(JdbiSpecification specification) {
      withHandle(
        handle => {
          var mapper = getMapper();  //Method implemented in the DAO
          var filterClause = specification.toSQL();
          if(sql.trim.startsWith(WhereSpecification.WHERE_OPERATOR){
            filterClause = " WHERE " + sql;
          }
          var sql = mapper.getSelectQuery() + filterClause;
          var query = handle.createQuery(sql);
          var result = specification.bind(query)
            .map(mapper)
            .first();
          Optional.ofNullable(result)
        }
      )
  }
}

Métodos de utilidad en la interface JdbiSpecification

Al principio creamos la interfaz JdbiSpecification, ese no fue el resultado final en esa interfaz. Haremos un par de métodos default y static en él, para proporcionar algunos métodos de utilidad que nos ayuden a simplificar la creación de la cadena de especificaciones más adelante.

import org.skife.jdbi.v2.Query;

public interface JdbiSpecification {
  String toSQL();
  Query<Map<String, Object>> bind(Query<Map<String, Object>> query);
  
  public static JdbiSpecification where(JdbiSpecification specification){
    return new WhereSpecification(specification);
  }
  
  public static JdbiSpecification grouped(JdbiSpecification specification) {
    return new GroupedSpecification(specification);
  }
  
  default AndSpecification and(JdbiSpecification second){
    return new AndSpecification(this, second);
  }

  default OrSpecification or(JdbiSpecification second){
    return new OrSpecification(this, second);
  }

  default JdbiSpecification grouped() {
    return new GroupedSpecification(this);
  }

  default OrderBySpecification orderBy(List[SortField] sortFields){
    return new OrderBySpecification(this, sortFields);
  }

  default GroupBySpecification groupBy(List[String] groupFields){
    return new GroupBySpecification(this, groupFields);
  }
}

Usando el patrón de Especificación

Ahora, estamos listos para comenzar a usar el patrón de especificación en JDBI.

Comencemos creando nuestro Repositorio o clase DAO, y extendamos o implementemos el JdbiSpecificationExecutor en él. Tendremos que anular e implementar los métodos getMapper y getSelectQuery.

Recuerde que en JDBI podemos crear los objetos DBI como una interfaz o una clase abstracta. Este es un ejemplo de un DBI como interfaz.

public interface PersonRepository extends JdbiSpecificationExecutor<Person>{
  
  @Override 
  default ResultSetMapper<Person> getMapper(){
    return new PersonMapper(); 
  }
  
  @Override 
  default String getSelectQuery(){
    return "SELECT * FROM Person p";
  }
  
  //Still can have the common JDBI annotation approach
  @SqlUpdate("INSERT INTO \"person\" (id, \"name\") VALUES (:id, :name)")
  void insert(@Bind("id") int id, @Bind("name") String name);
}

Este es el equivalente con un DBI como clase abstracta.

public abstract class PersonRepository implements JdbiSpecificationExecutor<Person>{
  
  @Override 
  public ResultSetMapper<Person> getMapper(){
    return new PersonMapper(); 
  }
  
  @Override 
  public String getSelectQuery(){
    return "SELECT * FROM Person p";
  }
  
  //Still can have the common JDBI annotation approach
  @SqlUpdate("INSERT INTO \"person\" (id, \"name\") VALUES (:id, :name)")
  public void insert(@Bind("id") int id, @Bind("name") String name);
}

Finalmente llamemos a los métodos en PersonRepository

//Creating the DBI object somewhere !!
Jdbi jdbi = Jdbi.create...; // Instatiate Jdbi
var personRepository = jdbi.onDemand(PersonRepository.class); //Create an ondeman DBI object

//-------
//Using the findBy method
Optional<Person> person = personRepository.findBy(where(new PersonIdEquals(42)));
Optional<Person> person2 = personRepository.findBy(new PersonIdEquals(83));
Optional<Person> person3 = personRepository.findBy(
  where(new PersonIdEquals(100)
  	.and(new PersonStatusEquals("Active")));
  
//Using the getBy method  
List<Person> persons = personRepository.getBy(new PersonStatusEquals("Active"));
  
//List of person in active status and age greater than 49, or status Inactive or default and age less than 49  
List<Person> persons2 = personRepository.getBy(where(   //this where method is the static method in JdbiSpecification
    grouped(new PersonStatusEquals("Active")
              .and(PersonAgeGreater(49))
    .or(
      grouped(new PersonStatusIn(List.of("Inactive", "Defaulter"))
              .and(PersonAgeLessThan(49)))
    )
  );

Conclusiones

Como podemos ver, siguiendo los pasos anteriores (agregando algunas clases), podemos proporcionar un buen soporte para el patrón de especificación en JDBI. Como he indicado, el patrón de especificación no es el más adecuado para todas las aplicaciones, a veces solo tenemos unas pocas consultas por tabla, el enfoque regular de usar métodos específicos para cada consulta es lo suficientemente bueno.

Sin embargo, para los casos en los que la tabla podría tener varias (decenas o más) de consultas con algunos criterios de filtro repetitivos, el patrón de Especificación es una buena opción para reducir la cantidad de métodos en nuestro DAO o Repositorios. En el caso de que estemos usando JDBI, este tutorial te ayudará a implementarlo.

Referencias

Advertisements

Leave a Reply

Your email address will not be published. Required fields are marked *