quinta-feira, 28 de junho de 2018

Rumo ao Certificado Android: Banco de Dados SQLite com Room

O que é Room?
Room é uma biblioteca do Android para mapeamento das tabelas de um banco SQLite para Objetos, em outras palavras um ORS (Object-relational mapping - Mapeamento objeto-relacional).

Ajustando dependências
Como qualquer outra biblioteca facultativa do Android, o Room deve ser importado no Gradle para poder ser usado. Para isso, vamos criar uma dependência:

dependencies{
    implementation "android.arch.persistence.room:runtime:1.0.0"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
}


Criando uma Entity
Entities são objetos que representam uma tabela no banco de dados. 
No código abaixo, nas linhas 1 e 2, vemos que Room utiliza classes com a anotação @Entity para criar as tabela antes da declaração da classe. Caso o nome da classe é diferente do nome da tabela, podemos indicar o nome da tabela que queremos no parâmetro da anotação.

Nas linhas 3-9 temos os campos do objeto, que por padrão vão ser as colunas da nossa tabela. Para criar um campo que não deva ser mapeado, devemos usar a anotação @Ignore (linha 8). Para definirmos uma chave primaria, usamos a anotação @PrimaryKey com parâmetro autoGenerate para deixarmos o banco de dados gerar os valores ou nós gerarmos. Não esqueça de criar os Getters e Setters (linha 22-26).

Também vale lembrar que o Room não permite que nossa classe tenha dois construtores (linha 10-21). Isso porque o Room usará construtores para popular os dados. Caso você queira ter um segundo construtor, você pode usar a anotação @Ignore no construtor (ou construtores) que você não deseja que o Room utilize (linha 15-21).

01. @Entity(tableName="name")
02. public class NameEntry{

03.   @PrimaryKey(autoGenerate=true)
04.   private int id;
05.   private String name;
06.   private int priority;
07.   private Date update;
08.   @Ignore
09.   private int dontIncludeMe;

10.   public NameEntry(String name,int priority,Date update){
11.       this.name = name;
12.       this.priority = priority;
13.       this.update = update;
14.   }

15.   @Ignore;
16.   public NameEntry(int id, String name,int priority,Date update){
17.       this.id = id;
18.       this.name = name;
19.       this.priority = priority;
20.       this.update = update;
21.   }


22.  //Getters e Setters
23.  public int getId() {return id;}
24.  public void setId(int id){ this.id = id; }
25.  public String getName() {return name; }
26.  public void setName(String name) {this.name = name; }
23.  public int getPriority() {return priority;}
24.  public void setPriority(int priority){ this.priority = priority; }
25.  public int getUpdate() {return update;}
26.  public void setUpdate(int update){ this.updateupdate; }

27. }

Criando uma DAO
DAO (Data Access Object) é o objeto responsável por permitir o acesso aos dados no banco de dados. Cada Entity criada vai possuir um DAO. 

Para criar um DAO usando Room, você deve implementar uma interface com a anotação @Dao (linha 1-2).
Um método que retorna uma lista da Entity na qual o DAO implementa o acesso deve ser criada para consulta, juntamente com sua anotação @Query, contendo a consulta SQL para obter todas as Entities (linha 3-4).
Consulta com parâmetros podem ser feitas também como mostra a linha 11 e 12.
Também precisamos inserir as operações INSERT (linha 5-6), UPDATE (linha 07-08) e DELETE (linha 09-10), juntamente com suas respectivas anotações @Insert, @Update e @Delete.
Uma observação no @Update do código abaixo (linha 07) é que usamos o parâmetro onConflict para especificar uma estratégia de conflito, que no caso seria um REPLACE. Mas o que seria isso? Conflitos são gerados quando definimos restrições, como a não repetição de valores em uma coluna (UNIQUE), chaves primárias ou chaves estrangeiras. No caso do exemplo, usamos o REPLACE como parâmetro, o que significa que se houver um conflito, os registros conflitantes serão deletados e, neste caso como se trata de um Update, o registro a ser alterado será atualizado.



01. @Dao
02. public interface NameDao{

03.   @Query("SELECT * FROM name ORDER BY priority")
04.   List<NameEntry> loadAllNames();

05.   @Insert
06.   void insertName(NameEntry nameEntry);

07.   @Update(onConflict=OnConflictStrategy.REPLACE)
08.   void updateName(NameEntry nameEntry);

09.   @Delete
10.   void deleteName(NameEntry nameEntry);

11.   @Query("SELECT * FROM name WHERE id=:id")
12.   NameEntry loadNameById(int id);
13. }

Criando um Type Converters
SQLite possui limitações quantos aos tipos. Ele não possui nativamente uma estrutura de dados para datas, valores booleanos e qualquer outra classe que não seja Null, Integer, Real, Text ou Blob. Se mesmo assim, nós queremos utilizar uma classe específica para este dado, então precisamos criar um Type Converter. Vamos olhar por exemplo, que a nossa classe NameEntry possui um campo de data. Então como fazemos?

Criaremos dois métodos, neste caso, um vai receber um valor do tipo long (do banco de dados) para um Objeto Date (linhas 02-05). Enquanto o outro vai fazer o contrário, vai receber um objeto Date e converter para um long (linhas 06-09). Note que ambos métodos possuem uma anotação @TypeConverter (linhas 02 e 06).



01. public class DateConverter{

02.     @TypeConverter
03.     public static Date toDate(long timestamp){
04.         return timestamp==null?null:new Date(timestamp);
05.     }

06.     @TypeConverter
07.     public static long toTimeStamp(Date date){
08.        return date==null?null:date.getTime();
09.     }
10. }


Criando o banco de dados
Para criar um banco de dados, vamos criar uma classe abstrata que herdará a classe RoomDatabase (linha 03). Também usaremos a anotação @Database, que levará como parâmetros, uma lista de todas as classes com a notação @Entity, a versão do banco de dados e se é para exportar um schema do BD (linha 01). Se tiver Type Converters, então também vamos declará-los aqui (linha 02).

Aqui usaremos um Singleton para criar/instanciar o banco de dados: Se já não houver uma instância, criaremos usando um databaseBuilder, que utiliza de um contexto para ser criado. (linhas 07-16)

Por fim, criaremos um método abstrato para cada DAOs que tivermos criado (linha 17).  

01. @Database(entities={NameEntry.class}, version=1, exportSchema=false) 
02. @TypeConverters(DateConverter.class);
03. public abstract class AppDatabase extends RoomDatabase{

04.   private static final Object LOCK = new Object();
05.   private static final String DATABASE_NAME = "NameList";
06.   private static AppDatabase instance;

07.   public static AppDatabase getInstance(Context context){
08.       if(instance == null){
09.         synchronized(LOCK){
10.            instance = Room.databaseBuilder(context.getApplicationContext(),
11.                       AppDatabase.class,
12.                       AppDatabase.DATABASE_NAME).build();
13.         }
14.       }
15.       return instance;
16.   }
  
17.   public abstract NameDao nameDao();
18. }

Como usar tudo isso?
Vamos pegar um exemplo de método para inserir um novo dado:

01.  public void insert(String name){
02.      NameEntry newName = new NameEntry(name, 1, new Date());
03.      AppDatabase.getInstance(this).taskDao().insertName(newName);
04.  } 

Basicamente, pegamos uma instancia do banco de dados, pegamos o DAO da nossa tabela e invocamos o método responsável por inserir. Simples assim.

Notas Importantes
- Nunca execute queries em threads principais de uma atividade. Apesar de poder desativar a proteção, por padrão, o mesmo não permitirá que utilize em threads principais. 



Nenhum comentário:

Postar um comentário