Integra Realm Database en Android

Olvida el infierno de gestionar SQLite manualmente con este tutorial

Como ya sabemos Android es un sistema operativo con un ecosistema gigantesco en el que difícilmente alcanzaría un post entero para mencionar todas sus virtudes y las posibilidades que nos provee.

Tanto si eres un experto con muchos años de experiencia en movilidad, como si eres una persona que estás haciendo tus primeros pasos te habrás dado cuenta que una de las partes más tediosas a la hora de construir una app es diseñar y manejar su capa de persistencia, la cual está montada sobre una base de datos SQLite.

Para evitar estar haciendo consultas SQL manualmente y no tener que estar lidiando con cursores podríamos escoger cualquiera de los diversos ORM disponibles en el mercado, esto facilitará el desarrollo, pero ¿por qué no nos vamos un paso más allá y apostamos por algo que aparte de facilitar el desarrollo también nos de un rendimiento brutal a nuestra aplicación?

Esto lo lograremos utilizando Realm Database y en este tutorial os mostraré cuales son los pasos que debeís dar para integrarlo en vuestra aplicación Android.


    ¿Qué es Realm Database?

Realm es una base de datos embebida y open source. Su core está implementado en C++ por lo que se aprovecha la velocidad que este lenguage nos provee. Desde el inicio fue diseñada como una base de datos NoSQL pensada explícitamente para plataformas móviles. Sus principales características son:

  • Rendimiento
  • Thread-safety
  • Relaciones entre entidades
  • Posibilidad de encriptar los datos.
  • Sincronización offline

 

Consta de varios módulos pero nosotros hablaremos específicamente de Realm Java, el cual es el framework que se integra en Android (actualmente es solo compatible para Android dentro del ecosistema Java)


Configurando tu proyecto para utilizar Realm

Lo primero que tenemos que hacer es añadir la siguiente dependencia al build.gradle de la raíz del proyecto:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:5.4.1"
    }
}

A continuación debemos indicarle a nuestra aplicación específica que vamos a usar Realm. Esto lo haremos en el fichero build.gradle de la aplicación:

apply plugin: 'realm-android'

Ya cuando tenemos descargadas las dependencias podremos empezar a la parte divertida, aporrear el teclado cual hacker de película noventera.

Primero configuraremos e iniciaremos la instancia de Realm para tenerla disponible en toda nuestra aplicación (la clase Realm representa una base de datos y está asociada a un archivo físico en nuestro sistema).


public class DecodeApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Realm.init(this);
        RealmConfiguration decodeConfig = new RealmConfiguration.Builder()
                .name("decodeapp.realm")
                .schemaVersion(1)
                .deleteRealmIfMigrationNeeded() //esta es solo para desarrollo para que reconstruya la base de datos cuando cambiemos el schema
                .build();
        Realm.setDefaultConfiguration(decodeConfig);
    }
}
    

Importante, deleteRealmIfMigrationNeeded solo lo usaremos en el entorno de desarrollo para que recree la base de datos cuando cambiemos el esquema de nuestro modelo, si no habría que configurar una migración de los datos.


Extendiendo tu modelo de negocio a RealmObject

No necesitamos preocuparnos de la creación de esquemas, tablas, índices, etc. Solamente tendremos que extender nuestras clases POJO del modelo de negocio a la clase abstracta RealmObject. Con esto y una serie de anotaciones que usaremos Realm será capaz de crear los proxies necesarios en tiempo de compilación.


public class PetOwner extends RealmObject {

    @PrimaryKey
    private String id = UUID.randomUUID().toString();
    private Date createdAt = new Date();

    @Required
    private String firstName;

    private String lastName;

    private Address address; //otro RealmObject

    private RealmList<Pet> pets;
    
    //los getter y setter autogenerados por nuestro IDE
}

Como podemos ver la clase PetOwner tiene una relación "one to many" con la clase Pety es obligatorio que sean de tipo RealmList, que no es más que otra implementación de java.util.List

public class Pet extends RealmObject {

    @PrimaryKey
    private String id = UUID.randomUUID().toString();
    private Date createdAt = new Date();
    String nickname;


    //los getter y setter autogenerados por nuestro IDE
}

También especificar que el id siempre lo generaremos con un UUID debido a que Realm no aconseja una estrategia auto-incremental debido a que está preparado para trabajar entre varios hilos y en entornos distribuidos. El atributo createdAt lo inicializaremos solamente en el constructor para garantizar el orden de inserción.


Manejando nuestros datos con el patrón Repositorio

Como nuestra intención siempre tiene que ser crear código reusable, desacoplado y testeable, y que con suerte el compañero que nos sustituya el día de mañana no nos quiera matar cuando le toque modificar nuestro código. Siguiendo esta directiva utilizaremos Realm junto con el patrón repositorio, esto nos permitirá que nuestra aplicación no dependa explícitamente de una capa de persistencia específica, y si el día del mañana queremos regresar a SQLite, lo podamos hacer sin tener que refactorizar cientos de clases.

 

 

Este patrón(si queréis más detalles podéis ver la definición especificada por Martin Fowler), básicamente consiste en utilizar nuestros datos como si de una colección se tratara, abstrayéndonos de la implementación específica y normalmente lo definiremos con una interfaz similar a esta:


public interface Repository<T> {

    void add(T item);

    void add(Iterable<T> items);

    void update(T item);

    void remove(T item);

    List<T> query(Specification specification);

}

Las implementaciones esta interfaz y de Specification serán específicas para  la capa de persistencia que usemos en nuestra app. Vamos a ver cómo podemos crear nuestro repositorio para Realm.


public class GeneralRealmRepository implements Repository<RealmObject> {

    private RealmConfiguration realmConf;

    public GeneralRealmRepository(RealmConfiguration realmConf) {
        this.realmConf = realmConf;
    }

    @Override
    public void add(final RealmObject item) {
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                realm.copyToRealm(item);
            }
        });
    }

    @Override
    public void add(final Iterable<RealmObject> items) {
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                realm.copyToRealm(items);
            }
        });
        realm.close();
    }

    @Override
    public void update(final RealmObject item) {
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                realm.copyToRealmOrUpdate(item);
            }
        });
        realm.close();
    }

    @Override
    public void remove(final RealmObject item) {
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                item.deleteFromRealm();
            }
        });
        realm.close();
    }

    @Override
    public List<RealmObject> query(Specification specification) {
        if (specification instanceof RealmSpecifications) {
            final RealmSpecifications specs = (RealmSpecifications) specification;
            RealmResults<? extends RealmObject> results = specs.toRealmResults(realm);
            return new ArrayList<>(results);
        } else {
            throw new IllegalArgumentException("Specification has to be a RealmSpecifications instance");
        }
    }
}
    

Ahora que ya tenemos nuestro implementación del repositorio para Realm tendremos que crear una "especificación" por cada petición específica que necesitemos en nuestra aplicación. Por ejemplo si quisiéramos obtener las mascotas(Pet) que tengan un determinado id:


public class PetByIdSpecification implements RealmSpecifications {

    private static final String ID_FIELD = "id";
    private final String id;

    public PetByIdSpecification(String id) {
        this.id = id;
    }

    @Override
    public RealmResults<? extends RealmObject> toRealmResults(Realm realm) {
        // usamos un findFirst porque siempre el ID va a ser único
        return realm.where(Pet.class).equalTo(ID_FIELD, id).findFirst();
    }
}

O por ejemplo si quisiéramos obtener los dueños de mascotas(PetOwner) que tengan un determinado nombre sería algo así:


public class PetOwnersByFirstNameSpecification implements RealmSpecifications {

    private static final String FIRST_NAME_FIELD = "firstName";
    private final String firstName;

    public PetOwnersByFirstNameSpecification(String firstName) {
        this.firstName = firstName;
    }

    @Override
    public RealmResults<? extends RealmObject> toRealmResults(Realm realm) {
        return realm.where(PetOwner.class)
                    .contains(FIRST_NAME_FIELD, firstName, Case.INSENSITIVE)
                    .findAll();
    }
}

Como véis es mucho más fácil tener nuestras consultas separadas en clases separadas con lo que así podremos testearlas independientemente a cada una, obteniendo un código más limpio y robusto ¿Quién no ha visto las típicas clases "Dios" con todas las funcionalidades mezcladas e imposibles de leer con tantas líneas de código?


Vayamos a la acción

Ya basta de tanto patrón y tanta teoría, vamos a ver como usar todo lo que hemos definido.


private List<PetOwner> getPetOwnersByName(String name){
    Repository repository = new GeneralRealmRepository(Realm.getDefaultInstance());
    Specification byNameSpec = new PetOwnersByFirstNameSpecification(name);
    return repository.query(byNameSpec);
}

private void giftPetToPeopleWithSpecificName(String name){
    Repository repository = new GeneralRealmRepository(Realm.getDefaultInstance());
    List<PetOwner> owners = getPetOwnersByName(name);
    for(PetOwner owner : owners)
        owner.getPets().add(new Pet(getRandomNickname()));
}

Perdón por los nombres tan tontos que les he puesto en los ejemplos (demasiados videos de Youtube sobre gatitos en mi cabeza), el objetivo es que cojan la idea y las buenas prácticas de cómo gestionar Realm.

Bueno, así termina este post. Espero que te haya ayudado y te agradezco por leerlo. Hemos visto una ínfima parte de la potencia de Realm, en próximo post nos adentraremos más en la utilización asíncrona de Realm, la encriptación y securización, las diferentes querys que existen y los modelos autoactualizables mediante Rx. Tampoco se piensen que todo es color de rosa, por ejemplo, no estoy de acuerdo en que tengamos que extender nuestras clases obligatoriamente de RealmObject ni que las listas tengan que ser RealmList, pero creo que las cosas positivas compensan las negativas.

No olvides seguirme vía RSS o vía Feedly para no perderte ninguna novedad y juntos recorrer el camino infinito del Clean Code.

 

Más entradas de blog

Google apadrina Kotlin en el I/O 2017

Si eres un desarrollador Android debes estar de enhorabuena. Google ha anunciado que a partir...

Curso Intensivo Liferay 7 Digital Experience I

Hola! Aquí comienza una serie de tutoriales en los cuales aprenderemos todos los conceptos...

Añadir comentarios