Android Jetpack — Room, o novo jeito de usar SQL

by Rhett Noonan on Unsplash

Olá pessoal! Novamente, Jetpack aqui na área :)

Hoje nós vamos falar de um dos componentes mais “velhos” (e mais completos) do jetpack, o Room. O Room foi lançado no ano passado, junto com os outros Arch Components como sendo uma alternativa de ORM suportada pelo próprio Android. Usualmente quando resolvemos utilizar as APIs de comunicação com SQL no Android nós recorremos a uma variedade de bibliotecas que ajudam nessa tarefa, já que o jeito “raw” é um tanto indigesto:

Algumas das bibliotecas utilizadas:

Essas bibliotecas (todas third-party) ajudam a comunicar-se com as APIs SQL (ou utilizar uma outra estrutura de dados por baixo, no caso do Realm) com mais facilidade sem ter que lidar muito com queries SQL ou Cursors.

Mas então, qual o problema de utilizar o jeito “raw” proposto pelo próprio Android?

O jeito antigo

O jeito antigo é utilizado pelo Room por “baixo dos panos”. As APIs de acesso ao SQLite envolvem muitos passos:

1 — Criar um contrato

2 — Criar uma subclasse de SQLiteOpenHelper

Primeiro criamos um arquivo com as constantes (queries SQL que serão usadas)

Depois definimos uma subclasse de SQLiteOpenHelper

3 — Escrever no banco:

Para escrever no banco, é “bem fácil”:

Utiliza-se uma instância do seu OpenHelper:

PostDbHelper dbHelper = new PostDbHelper(getContext());

Depois, usa-se uma versão “writable” do seu banco:

SQLiteDatabase db = dbHelper.getWritableDatabase();

Preenche-se o ContentValue (um Map especial que mapeia colunas para valores):

ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_DESCRIPTION, description);

Chame o método insert no seu banco:

long rowId = db.insert(PostEntry.TABLE_NAME, null, values);

4 — Ler do banco:

Se você achou a escrita fácil, vai se surpreender com a leitura:

Usa-se uma versão “readable” do seu banco:

SQLiteDatabase db = dbHelper.getReadableDatabase();

Defina uma “projection” que é basicamente as colunas que serão retornadas depois da query. Em termos SQL seria “SELECT <projection> FROM…”:

String[] projection = {
BaseColumns._ID,
PostEntry.COLUMN_NAME_TITLE,
PostEntry.COLUMN_NAME_DESCRIPTION
};

Defina uma “selection”, que é basicamente a parte da query onde você define qual a coluna que você que que seja “matched”:

String selection = PostEntry.COLUMN_NAME_TITLE + " = ?";

Defina os argumentos da query, ou seja, aquilo que você está buscando no banco:

String[] queryArgs = { "A post title" };

Finalmente você usa o método query do seu banco para realizar a query:

Cursor cursor = db.query(
PostEntry.TABLE_NAME, // tabela
projection, // colunas que serão retornadas
selection, // coluna da busca
queryArgs, // argumentos da busca
null, // group by
null, // having
null // sort
);

Como retorno você recebe um Cursor que deve utilizar para finalmente acessar os valores. Pelo amor de Deus, não se esquessa de fechar o cursor no fim da sua utilização:

Ah, já ia esquecendo. Ao fazer as queries, lembrem-se que elas podem ser demoradas e não devem ser feitas na Main Thread (Good luck managing that on top of everything else ;0)

Bem, acho que vou parar por aqui. Já deu para dar uma ideia do problemão que é trabalhar com essas APIs. Elas são extremamente poderosas, com certeza, e são a base do Room, mas para a maioria das aplicações não é necessário descer tão baixo no nível de abstração. Algumas das desvantagens de utilizar essa aboardagem:

  • O código que usamos acima é apenas um exemplo, mas tem bastante boirlerplate para apenas 1 tabela com 2 colunas. Imaginem só umas 5 tabelas com 5 colunas cada. É bastante coisa para administrar.
  • Mesmo depois de fazer a query você não recebe o objeto desejado (Post), mas sim uma instância de Cursor que você usa para “montar” o objeto.
  • Como eu disse, acesso ao banco de dados é algo que deve ser feito fora da Main Thread, o que complica ainda mais as coisas.
  • Vocês viram que em muitos casos usamos sintaxe SQL para “formar” as queries, não há nenhuma garantia que esses pedaços de SQL que escrevemos estão realmente corretos.
  • Não vou nem entrar no mérito de relacionamentos entre entidades…

Enfim, o jeito antigo é interessante para aqueles que querem ter completo domínio sobre a interface com o banco de dados. Repito, é um jeito poderoso de fazer, mas você muitas vezes não precisa de tanto. As vezes você quer apenas salvar umas entidades de forma facilitada para dar uma experiência mais interessante para seu usuário (quando ele estiver temporariamente offline, por exemplo). Vamos então ao jeito “Room” de fazer as coisas.

O jeito novo

A utilização do Room adiciona uma camada de abstração ao jeito raw anterior. Começemos primeiramente com as dependências:

Eu adicionei as dependências do LiveData e RxJava por que como veremos, nós poderemos fazer uso dos dois para melhorar o fluxo de leitura e escrita no banco sem se preocupar com mover o processamento para outra thread. Todo o código e exemplos utilizados nesse post estarão nesse projeto no Github.

Os passos do Room também são muitos, mas diferente do jeito aterior eles são mais “organizados”:

1 — Criar Entidade

No Room entidades são definidas sob a forma de objetos com certas Annotations (por isso uma das dependências era um annotationProcessor).

As entidades são definidas com a annotation @Entity passando o nome que queremos dar a tabela. Eu criei um companion object por que depois usaremos para acessar esses valores em outras classes e poderemos usar a sintaxe Person.TABLE_NAME para isso.

A annotation @PrimaryKey é necessária para indicar qual vai ser a chave primária dessa tabela. @ColumnInfo não é obrigatória. Se você estiver usando Java os atributos da classe devem ser públicos para que o Room possa persití-los, ou você deve definir setters e getters púplicos para aquele atributo. Caso não queira persistir um campo, você pode usar a annotation @Ignore.

2 — Criar um Dao (Data Access Object)

Os dados são recuperados ou adicionados no banco de dados através de Daos. Usualmente cada tabela/entidade persistida no seu banco deve ter um Dao para administrar as operações comuns de CRUD (Criar, Ler, Atualizar e Deletar).

Um Dao é geralmente definido por uma classe abastrata ou interface. Utiliza-se a annotation @Dao para indicar a criação. Dentro da interface nós definimos métodos convenientes que poderemos utilizar. Aqui eu criei 4 métodos, listar, inserir, ler e excluir.

A definição do Dao é um dos lugares onde o Room se destaca. Primeiro nós definimos queries SQL que representam as funções. O Room é capaz de detectar erros nessas queries em tempo de compilação, o que é excelente para tratar potenciais bugs. Além disso o retorno das queries é feito sob a forma de objetos puros e não Cursors como anteriormente. Então a query de ler a lista retorna uma Lista de objetos: List<Person>.

No exemplo de Dao acima eu estou utilizando RxJava para resolver um outro problema que tinhamos no jeito antigo de fazer as coisas. Lá tinhamos que administrar as operações de banco de dados em uma thread secundária, mas ao utilizarmos RxJava (ou LiveData) a administração disso é feita por essas bibliotecas automaticamente para nós. Se você estiver utilizando o Room sem RxJava ou LiveData saiba que ele irá lançar uma exceção caso você tente fazer operações na Main Thread.

3 — Criando o banco de dados

Finalmente criamos o AppDatabase que recebe todos os Daos para criar e interagir com as tabelas do SQL:

Para inicializar o banco você só precisa fazer:

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "MyDatabase.db").build();

Normalmente vocé vai querer recorrer a injeção de dependência para criar essa instância do seu banco e injetar ela onde você precisar. É recomendado que ela seja um Singleton.

4 — Relacionamentos e entidades aninhadas

Para terminar essa overview sobre o Room vamos falar com fazer relacionamentos entre entidades com ele.

4.1 — Relacionamento One-Many

O primeiro tipo de relacionamento que podemos ilustrar é o 1 para muitos com o uso de ForeignKeys.

No exemplo acima definimos uma nova entidade com uma coluna que representa um relacionamento entre Business e Person. Dessa forma, poderíamos ter no nosso BusinessDao uma query para relacionar os Business de uma Person:

@Query("SELECT * FROM " + Business.TABLE_NAME + " WHERE " + COLUMN_PERSON + " = :personId")
fun businessFromPerson(personId: Long): Single<List<Business>>

4.2 — Relacionamento Many-Many

Um outro relacionamento, esse bem mais complexo, é o de muito para muitos. Quando falamos em SQL, o relacionamento de muitos para muitos é representado por uma tabela intermediária com as duas foreign keys, uma para cada tabela do relacionamento. O mesmo acontece no Room, devemos definir uma entidade intermediária que contém duas ForeignKeys:

Parece bem assustador, mas depois de um tempo você se acostuma. Através desse modelo podemos criar Queries que retornam todas as Person de um determinado Business ou todos os Business de uma determinada Person:

4.3 — Objetos aninhados

Uma outra opcão que o Room nos dar para representar relacionamento de objetos é o aninhamento deles. Então, retornando a nossa entidade Person, podemos definir um objeto aninhado dessa forma:

Note que não existe um relacionamento propriamente dito entre as tabelas Address e Person, o que o Room vai fazer aqui é simplesmente acrescentar os campos do Address dentro da tabela Person e reconstruir o objeto Address quando as queries forem realizadas nessa tabela.

É basicamente isso. Para resumir as vantagens do Room:

  • Funciona em comunhão com os outros Arch Components
  • Nada de cursores (você pode até usar se quiser, mas não é recomendado), utilize objetos reais.
  • Adminstração da thread de execução por conta do RxJava/LiveData
  • Código mais fácil de manter
  • As queries SQL são verificadas em tempo de compilação.

Então isso pessoal. O Room tem muitas outras ferramentas inclusas que eu não discuti aqui, isso foi só um pouco do que ele é capaz. Dá próxima vez que você pensar em guardar dados localmente, dá uma olhada nele aqui.

If you like it and you know it clap your hands. Clap, clap, clap.