debug_mode=ON

Buscar en

 
 

Siena: persistencia en Java sencilla y escalable

Escrito por gimenete hace 11 meses bajo una licencia de Creative Commons Creative Commons License
2931 visitas. Etiquetas: persistencia, java, siena, active-record, bases-de-datos, jdbc

Principios de diseño

Siena es un sencillo motor de persistencia diseñado con los siguientes principios:

  • Sencillo. Siena no tiene dependencias, ocupa 25Kb y el API está diseñada para ser lo más sencilla posible.
  • Intrusivo. Para persistir objetos es necesario extender de una clase, de modo que el código depende de Siena. El motivo es que se ha utilizado el patrón ActiveRecrod, para hacer el API lo más sencilla posible. Otros frameworks de persistencia optan por hacer que las clases persistentes no tengan ninguna dependencia en el framework, pero en la práctica es absolutamente infrecuente que una aplicación cambie de motor de persistencia.
  • Limitado. El API se ha diseñado para que todas las consultas puedan ser ejecutadas usando sólo un índice. Todas las consultas se realizan sólo sobre una tabla, no se pueden hacer subconsultas ni JOINs y no se puede utilizar OR en una cláusula WHERE. De este modo se consigue que todas las consultas puedan ser realizadas utilizando sólo un índice y así sean lo más rápidas posible. Esta estrategia es la que viene siendo utilizada por las aplicaciones que necesitan ser altamente escalables. Las bases de datos "en la nube" Big Table de Google, SimpleDB de Amazon, utilizan esta aproximación.
  • Extensible. La implementación actual utiliza JDBC y ha sido probada en MySQL. No obstante la funcionalidad principal de Siena se basa en dos interfaces que pueden ser implementadas utilizando otros mecanismos de persistencia. En este artículo me centraré en la implementación actual que transforma las consultas a SQL.

Mapeo de clases

Para hacer que los objetos de una clase sean persistentes utilizando Siena es necesario seguir estos pasos:

  1. Extender de la clase siena.Model
  2. Implementer el método estático all(). Esto no es obligatorio pero es altamente recomendable. Este método será utilizado como base para realizar cualquier consulta a los objetos de esta clase.
  3. Anotar la clase con @Table. Esto sólo es necesario si el nombre de la clase es diferente al nombre de la tabla en la base de datos.
  4. Anotar los atributos de la clase con @Column si el nombre del atributo es diferente al nombre de la columna en la tabla de la base de datos.
  5. Anotar con @Id los campos que sean clave primaria. Si la clave primaria es generada por la base de datos de forma autoincremental se debe anotar como @Id(Generator.AUTO_INCREMENT)
  6. Los atributos de la clase pueden tener cualquier nivel de visibilidad. En los ejemplos de este artículo se han declarado como públicos por sencillez.

Un ejemplo completo:

import siena.Column;
import siena.Generator;
import siena.Id;
import siena.Model;
import siena.Query;
import siena.Table;

@Table("people")
public class Person extends Model {

        @Id(Generator.AUTO_INCREMENT)
        public long id;
        @Column("first_name")
        public String firstName;
        @Column("last_name")
        public String lastName;
        public String city;

        public static Query<Person> all() {
                return Model.all(Person.class);
        }

        public String toString() {
                return "id: "+id+", firstName: "+firstName+", lastName: "+lastName+", city: "+city;
        }

}

Configurar la conexión a la base de datos

Para configurar la conexión a la base de datos solamente es necesario crear un fichero de propiedades llamado siena.properties situado en el mismo paquete que la clase que se quiere persistir. En este fichero se indicará el PersistenceManager y los parámetros que este necesite. Por ejemplo:

implementation = siena.jdbc.JdbcPersistenceManager
driver = com.mysql.jdbc.Driver
url = jdbc:mysql://localhost/siena-example
user = root
password = 1234

Siena se encargará de establecer la conexión a la base de datos cuando sea necesario.

Realizar consultas

Primero voy a exponer un ejemplo y a partir de él explicaré los métodos utilizados:

List<Person> people = Person.all().filter("firstName", "Mark").order("-lastName").fetch(10);

El método all() será la raíz de cualquier consulta. El método all() no realiza ninguna consulta a la base de datos, simplemente devuelve un objeto Query. Un objeto Query contiene los criterios de la consulta. El método all() devolverá un objeto todavía sin criterios.

Llamando a filter() se añaden criterios a la consulta. Este método necesita dos parámetros. El primer parámetro es el nombre del atributo de la clase sobre el que imponer la condición, opcionalmente concatenado con un operador. Si no se define un operador el operador por defecto es "=". Otros operadores disponbiles son: <, >, <=, >=. El segundo parámetro es el valor de la restricción. Es decir

filter("firstName", "Mark")

se traduce a SQL como WHERE first_name='Mark'. Y

filter("age>", 18)

se traduce a SQL como WHERE age>18. Nótese que el nombre del atributo del objeto no tiene por qué ser igual al nombre de la columna. En el método filter() se debe usar el nombre del campo de la clase.

Para ordenar los resultados de una consulta se utiliza el método order() que recibe un parámetro. Este parámetro es el nombre de un atributo de la clase sobre el que ordenar. Por defecto la ordenación es ascendente. Para hacer una ordenación descendente debe concatenarse el signo "-" a la izquierda del nombre del atributo. Por ejemplo:

query.order("-lastName")

Hasta este punto no se ha realizado ninguna consulta a la base de datos. Simplemente se ha construido un objeto Query con la información de la consulta. Para obtener los resultados de la consulta se puede utilizar el método fetch(). Este método tiene tres variantes:

  • fetch(). Devuelve todos los resultados. Se desaconseja el uso de este método a no ser que se sepa que la consulta devolerá un número de resultados no muy grande.
  • fetch(int limit). Limita la consulta a un número máximo de resultados.
  • fetch(int limit, int offset). Limita la consulta a un número máximo de resultados e indica un offset. Este método permite paginar los resultados de una consulta.

Tanto los métodos filter() como order() pueden ser utilizados varias veces en una misma consulta.

List<Person> people = Person.all().filter("firstName", "Mark").filter("age>", 18).order("-lastName").order("postal_code").fetch(10);

Esta consulta será traducida a SQL aproximadamente de esta forma:

SELECT * FROM people WHERE first_name='Mark' AND age>18 ORDER BY last_name DESC, postal_code ASC LIMIT 10

Para consultar un objeto del cual conocemos sus claves primarias podemos hacer dos cosas:

Person p = new Person();
p.id = 123; // establecemos el valor de las claves primarias, que previamente conocemos
p.get(); // cargamos el objeto
System.out.println(p);

El método get() como puede verse permite cargar un objeto que tenga definidas las claves primarias. Si el objeto no existe se lanzará una SienaException. Esta forma de leer un objeto es especialmente útil si estamos navegando por las relaciones de un objeto. Por ejemplo si tenemos un objeto "articulo" que tiene un "autor" podemos hacer:

Articulo articulo = ...;
articulo.autor.get();
System.out.println(articulo.autor.nombreCompleto);

Otra forma de leer un objeto es crear una consulta, filtrar por las claves primarias y utilizar get(). Ejemplo:

Person p = Person.all().filter("id", 123).get();

Esta forma tiene dos ventajas. La primera es que se puede escribir en una línea, y la segunda es que en vez de lanzar una excepción devolverá null si el objeto no se ha encontrado.

Modificar la base de datos

Las clases que son persistentes en Siena deben extender de Model. Esto significa que todos los objetos persistentes heredan los métodos de Model que son:

  • insert(). Inserta un nuevo registro en la base de datos con los valores contenidos en los atributos del objeto. Si el objeto tiene una clave primaria autoincremental esta puede ser consultada inmediatamente después de llamar a insert().
  • update(). Actualiza el registro de la base de datos correspondiente a este objeto.
  • delete(). Elimina el objeto de la base de datos. Ejecuta un DELETE de SQL.
  • get(). Como se ha visto anteriormente carga los campos del objeto haciendo una consulta a la base de datos.

Relaciones

Siena permite manejar relaciones entre tablas. Si por ejemplo tenemos una clase Articulo y una clase Usuario de la siguiente forma:

class Articulo extends Model {
    @Column("autor")
    public Usuario autor;
    public String titulo;
}

class Usuario extends Model {
    @Id
    public String nickname;
    public String email;
}

Cuando carguemos de la base de datos un objeto Articulo Siena cargará el objeto estableciendo la propiedad "autor" con un objeto Usuario que sólo contendrá el nickname del usuario (su clave primaria).

List<Articulo> articulos = Articulo.all().fetch(10);
for (Articulo a : articulos) {
    System.out.println("El artículo '"+a.title+"' fue escrito por "+a.autor.nickname);
}

Si quisiéramos obtener el email del usuario tendríamos que hacer a.autor.get(), lo que cargaría el objeto completo, y ya podríamos acceder a ese dato. Sin embargo si esto lo hacemos dentro del bucle para cada objeto estaríamos haciendo una consulta en cada iteración del bucle, lo cual es muy desaconsejable. En una consulta SQL esto se solucionaría utilizando JOINs, pero Siena no permite hacer JOINs. Esta es una funcionalidad crítica para decidir si utilizar o no Siena.

También se pueden hacer consultas en base a relaciones. Si por ejemplo queremos obtener los artículos escritos por un usuario...

Usuario usuario = ...;
List<Articulo> articulos = Articulo.all().filter("autor", usuario).fetch(10);

Escalabilidad

Las limitaciones en funcionalidades en Siena se deben a su propósito: ser un API de persistencia para aplicaciones altamente escalables. Si no necesitas una aplicación altamente escalable posiblemente te sea más cómodo utilizar otro API de persistencia sin las limitaciones de Siena.

Si tu aplicación necesita ser altamente escalable o simplemente muy rápida, desnormaliza la base de datos y utiliza Siena :)

Para terminar recomiendo leer: Maybe Normalizing Isn't Normal y los artículos que se enlazan desde el artículo.

 

¡Votalo! 9 votos
¡Compártelo!

        

&nbps;

&nbps;

gimenete

Sobre gimenete

Gimenete es un tipo al que le encanta programar. Lleva media vida programando en Java, y ahora le da bastante también a Python. No le hace ascos a JavaScript. Su tema de investigación favorito ahora es el cloud computing.

 
Regístrate o haz login para participar.
¿Todavía no conoces debugmodeon?
debugmodeon es la red social para profesionales de la informática
descubre debugmodeon
 

4 comentarios en "Siena: persistencia en Java sencilla y escalable"

jsanca
jsanca escribió
hace 11 meses

#1   

Hola Gimienete,
Ya le había hechado un vistaso a Siena; en realidad me gusta la idea que sea pequeño y simple, el unico punto que no me gusta es que sea "intrusivo".
Pues por ejemplo, en el caso que tengo objetos que ya extienden de otro objetos (una jerarquía propia), ya ahi me encuentro con un problema para introducir Siena.

Me pregunto que tan complicado sería implementar la funcionalidad mediante la implementación de una interface en vez de una extensión.

Saludos,
J

 

gimenete
gimenete escribió
hace 11 meses

#2   

Hola jsanca. Me alegra que te guste.

Es intrusivo por dos motivos:

  • Los objetos implementan el patrón ActiveRecord. Es decir, los objetos tienen update(), delete(), insert(),... Estos métodos son implementados por el framework, por la clase Model. Tener que implementar una interfaz para implementar esos métodos rompería la filosofía de sencillez del framework. Podría hacerse, pero habría que hacer muchos cambios y el framework sería más complicado.
  • No se soporta herencia en la base de datos relacional. Los mapeos a las clases serían más complicados. Ahora son simples: una clase -> una tabla. Extendiendo de la clase Model también se impone esta sencillez.

En definitiva, la obligatoriedad de extender de una clase se va a mantener. Si quieres utilizar siena tendrías que refactorizar tus clases.

Un saludo!

 

jsanca
jsanca escribió
hace 11 meses

#3   

Entiendo tu punto, me parece que en un desarrollo desde cero, puede ser util, pero por ejemplo a mi me hubiera gustado algo así.

class DomainObject implement Serializable {

private int id;
}

class User extends DomainObject {
// ... + propiedades
}

class UserDR extends User implements Model {

// Ahora si este tiene todo lo que se ocupa para la persistencia.
}

No es que me importe extender o no, el asunto mas que todo que a veces seria util tener clases base ya establecidas, aunque pensandolo bien DomainObject podria extender de model, de paso una pregunta, con JPA cuando intento hacer esto; poner el id en una clase padre y anotarla encuentro un problema, pues al parecer Hibernate, que es la implementación que uso, no puede obtener los annotations de clases padres, Siena funciona de forma similar?

 

nel124
nel124 escribió
hace 10 meses

#4   

Hola que tal, la api me parece interesante, por la cual tenemos pensado utilizarla, solo que tenemos un pequeño problema, el cual es no conseguimos correr un ejemplo, la verdad hemos probado el codigo que proporcionas aqui siguiendo tus intrucciones, bueno no se si se deba al IDE que estamos utilizando, nosotros ocupamos Eclipse 3.2.2,el conector jdbc es el 3.0.17, con lammp version 1.7.1, ojala si se pudiera nos pudieras facilitar el ejercicio de ejemplo.

De antemano Gracias

 
 
 
 

© Copyright 2008-2009 debug_mode=ON | Aviso legal | Contacto | FAQ | ¿Quiénes somos? |