Este documento aborda as noções básicas de leitura e gravação de dados do Firebase.
Esses dados são gravados em uma referência FirebaseDatabase
e recuperados com
um listener assíncrono anexado à referência. Ele é acionado
uma vez no estado inicial dos dados, e posteriormente, quando há alterações.
Criar protótipos e fazer testes com Firebase Local Emulator Suite (opcional)
Antes de compreender como o app lê e grava no Realtime Database, é importante conhecer um conjunto de ferramentas que podem ser usadas para criar protótipos e testar a funcionalidade do Realtime Database: Firebase Local Emulator Suite. Conseguir trabalhar localmente sem precisar implantar serviços existentes será uma ótima ideia se você estiver testando diferentes modelos de dados, otimizando suas regras de segurança ou procurando a maneira mais econômica de interagir com o back-end.
Um emulador do Realtime Database faz parte do Local Emulator Suite, que permite que o app interaja com o conteúdo e a configuração do banco de dados emulado e também com os recursos do projeto emulado (funções, outros bancos de dados e regras de segurança).
O uso do emulador do Realtime Database envolve apenas algumas etapas:
- Para se conectar ao emulador, adicione uma linha de código à configuração de teste do app.
- Execute
firebase emulators:start
na raiz do diretório do projeto local. - Faça chamadas pelo código de protótipo do app usando um SDK da plataforma do Realtime Database, como de costume, ou a API REST do Realtime Database.
Confira um tutorial detalhado sobre o Realtime Database e o Cloud Functions. Consulte também a introdução do Local Emulator Suite.
Receber um DatabaseReference
Para ler ou gravar dados no banco de dados, é necessário uma instância de
DatabaseReference
:
Kotlin
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Gravar dados
Operações básicas de gravação
Em operações básicas de gravação, use setValue()
para salvar dados em uma referência
específica e substituir os dados no caminho. Use esse método para as seguintes ações:
- Transmitir os tipos que correspondam aos tipos JSON disponíveis:
String
Long
Double
Boolean
Map<String, Object>
List<Object>
- Transmita um objeto Java personalizado se a classe que o define tiver um construtor padrão que não recebe argumentos e tem getters públicos para as propriedades a serem atribuídas.
Se você usar um objeto Java, o conteúdo desse objeto será automaticamente associado
a locais filhos de maneira aninhada. Usar um objeto Java normalmente torna seu
código mais legível e fácil de manter. Por exemplo, se você tiver um app
com um perfil básico de usuário, é possível que o objeto User
tenha a aparência a seguir:
Kotlin
@IgnoreExtraProperties data class User(val username: String? = null, val email: String? = null) { // Null default values create a no-argument default constructor, which is needed // for deserialization from a DataSnapshot. }
Java
@IgnoreExtraProperties public class User { public String username; public String email; public User() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } public User(String username, String email) { this.username = username; this.email = email; } }
É possível adicionar um usuário com setValue()
como mostrado a seguir:
Kotlin
fun writeNewUser(userId: String, name: String, email: String) { val user = User(name, email) database.child("users").child(userId).setValue(user) }
Java
public void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
O uso de setValue()
dessa maneira substitui os dados no local especificado,
incluindo qualquer nó filho. No entanto, ainda é possível atualizar um filho sem
substituir o objeto inteiro. Para que os usuários atualizem os próprios perfis,
atualize o nome de usuário deles como mostrado a seguir:
Kotlin
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Ler dados
Ler dados com listeners permanentes
Para ler dados em um caminho e detectar alterações, use o método
addValueEventListener()
para adicionar um ValueEventListener
a uma DatabaseReference
.
Listener | Callback do evento | Uso normal |
---|---|---|
ValueEventListener |
onDataChange() |
Ler e detectar alterações em todo o conteúdo de um caminho. |
É possível usar o método onDataChange()
para ler um snapshot estático do
conteúdo de um determinado caminho no momento do evento. Esse método
será acionado uma vez quando o listener for anexado e sempre que os dados
forem alterados, incluindo os filhos. O callback do evento recebe um snapshot que contém
todos os dados no local, incluindo dados filhos. Se não houver dados,
o snapshot retornará false
quando você chamar exists()
e null
e quando chamar
getValue()
nele.
O exemplo a seguir mostra um aplicativo de blog social recuperando detalhes de uma postagem do banco de dados:
Kotlin
val postListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { // Get Post object and use the values to update the UI val post = dataSnapshot.getValue<Post>() // ... } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) } } postReference.addValueEventListener(postListener)
Java
ValueEventListener postListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // Get Post object and use the values to update the UI Post post = dataSnapshot.getValue(Post.class); // .. } @Override public void onCancelled(DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); } }; mPostReference.addValueEventListener(postListener);
O listener recebe um DataSnapshot
que contém os dados no local especificado no
banco de dados no momento do evento. Chamar getValue()
em um snapshot retorna a representação do objeto Java dos dados. Se não houver dados no local, chamar getValue()
retornará null
.
Neste exemplo, ValueEventListener
também define o método onCancelled()
que será chamado se a leitura for cancelada. Por exemplo, uma leitura poderá ser cancelada se o cliente não tiver permissão para ler esse local no banco de dados do Firebase. Este método recebe um objeto DatabaseError
indicando por que a falha ocorreu.
Ler dados uma vez
Ler uma vez usando get()
O SDK foi projetado para gerenciar interações com servidores de banco de dados, independentemente do seu app estar on-line ou off-line.
Geralmente, é necessário usar as técnicas de ValueEventListener
descritas acima
para ler dados e receber notificações sobre as atualizações dos dados do back-end. As
técnicas de listener reduzem o uso e o faturamento, além de serem otimizadas para
oferecer aos usuários a melhor experiência on-line e off-line.
Se você precisar dos dados apenas uma vez, poderá usar get()
para acessar um snapshot dos
dados do banco de dados. Se, por algum motivo, get()
não conseguir retornar o valor
do servidor, o cliente procurará no cache de armazenamento local e retornará um erro se o
valor não for encontrado.
O uso desnecessário de get()
pode aumentar a utilização da largura de banda e causar uma perda
de desempenho. É possível evitar isso usando um listener em tempo real, como mostrado acima.
Kotlin
mDatabase.child("users").child(userId).get().addOnSuccessListener {
Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
Log.e("firebase", "Error getting data", it)
}
Java
mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (!task.isSuccessful()) {
Log.e("firebase", "Error getting data", task.getException());
}
else {
Log.d("firebase", String.valueOf(task.getResult().getValue()));
}
}
});
Ler uma vez usando um listener
Em alguns casos, é melhor que o valor do cache local seja retornado
imediatamente, em vez de você ter que verificar um valor atualizado no servidor. Nesses
casos, use addListenerForSingleValueEvent
para receber os dados do
cache do disco local imediatamente.
Isso é útil para dados que só precisam ser carregados uma vez, não são alterados com frequência nem exigem detecção ativa. Por exemplo, o app de blog dos casos anteriores usou esse método para carregar o perfil de um usuário quando ele começou a escrever uma nova postagem.
Atualizar ou excluir dados
Atualizar campos específicos
Para gravar simultaneamente em filhos específicos de um nó sem substituir outros
nós filhos, use o método updateChildren()
.
Ao chamar updateChildren()
, atualize valores de filhos de nível inferior ao
especificar um caminho para a chave. Se os dados estiverem armazenados em vários locais para aprimorar a escalabilidade, atualize todas as instâncias usando a distribuição de dados. Por exemplo, um
app de blog social poderia ter uma classe Post
como a mostrada a seguir:
Kotlin
@IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", var body: String? = "", var starCount: Int = 0, var stars: MutableMap<String, Boolean> = HashMap(), ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, "body" to body, "starCount" to starCount, "stars" to stars, ) } }
Java
@IgnoreExtraProperties public class Post { public String uid; public String author; public String title; public String body; public int starCount = 0; public Map<String, Boolean> stars = new HashMap<>(); public Post() { // Default constructor required for calls to DataSnapshot.getValue(Post.class) } public Post(String uid, String author, String title, String body) { this.uid = uid; this.author = author; this.title = title; this.body = body; } @Exclude public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("uid", uid); result.put("author", author); result.put("title", title); result.put("body", body); result.put("starCount", starCount); result.put("stars", stars); return result; } }
Para criar uma postagem e atualizá-la simultaneamente no feed de atividades recentes e no feed de atividades do usuário que fez a postagem, o aplicativo de blog usa códigos como estes:
Kotlin
private fun writeNewPost(userId: String, username: String, title: String, body: String) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously val key = database.child("posts").push().key if (key == null) { Log.w(TAG, "Couldn't get push key for posts") return } val post = Post(userId, username, title, body) val postValues = post.toMap() val childUpdates = hashMapOf<String, Any>( "/posts/$key" to postValues, "/user-posts/$userId/$key" to postValues, ) database.updateChildren(childUpdates) }
Java
private void writeNewPost(String userId, String username, String title, String body) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously String key = mDatabase.child("posts").push().getKey(); Post post = new Post(userId, username, title, body); Map<String, Object> postValues = post.toMap(); Map<String, Object> childUpdates = new HashMap<>(); childUpdates.put("/posts/" + key, postValues); childUpdates.put("/user-posts/" + userId + "/" + key, postValues); mDatabase.updateChildren(childUpdates); }
Esse exemplo usa push()
para criar uma postagem no nó que armazena as postagens para
todos os usuários em /posts/$postid
e, simultaneamente, recuperar a chave com
getKey()
. Depois, a chave pode ser usada para criar uma segunda entrada nas postagens
do usuário em /user-posts/$userid/$postid
.
Com esses caminhos, você faz atualizações simultâneas em vários locais
da árvore JSON com uma única chamada ao updateChildren()
, da mesma forma que esse exemplo
cria a nova postagem nos dois locais. Essas atualizações
são atômicas: ou todas funcionam ou todas falham.
Adicionar um callback de conclusão
Para saber quando seus dados foram confirmados, adicione um listener de conclusão. Tanto setValue()
quanto updateChildren()
recebem um listener de conclusão opcional, que será chamado quando a gravação for confirmada no banco de dados. Se a chamada não for bem-sucedida, o listener vai
receber um objeto de erro indicando o motivo da falha.
Kotlin
database.child("users").child(userId).setValue(user) .addOnSuccessListener { // Write was successful! // ... } .addOnFailureListener { // Write failed // ... }
Java
mDatabase.child("users").child(userId).setValue(user) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Write was successful! // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Write failed // ... } });
Excluir dados
A maneira mais simples de excluir os dados é chamar removeValue()
em uma referência ao
local desses dados.
Também é possível fazer a exclusão ao especificar null
como o valor de outra operação
de gravação, como setValue()
ou updateChildren()
. Você pode usar essa técnica com updateChildren()
para excluir vários filhos em uma única chamada de API.
Remover listeners
Para remover callbacks, chame o método removeEventListener()
na sua referência ao banco de dados do Firebase.
Se um listener tiver sido adicionado várias vezes a um local de dados, ele será chamado diversas vezes para cada evento e precisará ser removido o mesmo número de vezes para ser completamente excluído.
Chamar removeEventListener()
em um listener pai não
remove automaticamente os listeners registrados nos nós filhos dele.
removeEventListener()
também precisa ser chamado nos listeners filhos
para remover a callback.
Salvar dados como transações
Ao trabalhar com dados que podem ser corrompidos por modificações simultâneas, como contadores incrementais, use uma operação de transação. Essa operação aceita dois argumentos: uma função de atualização e um callback de conclusão opcional. A função de atualização usa o estado atual dos dados como um argumento e retorna o novo estado desejado de acordo com suas preferências. Se outro cliente fizer uma gravação no local antes que seu novo valor seja gravado com sucesso, sua função de atualização será chamada novamente com o novo valor atual e a gravação será repetida.
Por exemplo, os usuários do app de blog social podem adicionar ou remover estrelas de postagens e acompanhar quantas foram recebidas da seguinte maneira:
Kotlin
private fun onStarClicked(postRef: DatabaseReference) { // ... postRef.runTransaction(object : Transaction.Handler { override fun doTransaction(mutableData: MutableData): Transaction.Result { val p = mutableData.getValue(Post::class.java) ?: return Transaction.success(mutableData) if (p.stars.containsKey(uid)) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1 p.stars.remove(uid) } else { // Star the post and add self to stars p.starCount = p.starCount + 1 p.stars[uid] = true } // Set value and report transaction success mutableData.value = p return Transaction.success(mutableData) } override fun onComplete( databaseError: DatabaseError?, committed: Boolean, currentData: DataSnapshot?, ) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError!!) } }) }
Java
private void onStarClicked(DatabaseReference postRef) { postRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData mutableData) { Post p = mutableData.getValue(Post.class); if (p == null) { return Transaction.success(mutableData); } if (p.stars.containsKey(getUid())) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1; p.stars.remove(getUid()); } else { // Star the post and add self to stars p.starCount = p.starCount + 1; p.stars.put(getUid(), true); } // Set value and report transaction success mutableData.setValue(p); return Transaction.success(mutableData); } @Override public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot currentData) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
A transação impede uma contagem incorreta de estrelas caso vários usuários adicionem simultaneamente estrelas à mesma postagem ou se o cliente tiver dados desatualizados. Se a transação for rejeitada, o servidor retornará o valor atual ao cliente, que executará a transação novamente com o valor atualizado. Isso se repetirá até que a transação seja aceita ou até que muitas tentativas sejam realizadas.
Incrementos atômicos do lado do servidor
No caso de uso acima, estamos gravando dois valores no banco de dados: o ID do usuário que marca a publicação com estrela ou remove a marcação e a contagem de estrelas incrementada. Se já soubermos que o usuário está marcando a postagem com estrela, poderemos usar uma operação de incremento atômico em vez de uma transação.
Kotlin
private fun onStarClicked(uid: String, key: String) { val updates: MutableMap<String, Any> = hashMapOf( "posts/$key/stars/$uid" to true, "posts/$key/starCount" to ServerValue.increment(1), "user-posts/$uid/$key/stars/$uid" to true, "user-posts/$uid/$key/starCount" to ServerValue.increment(1), ) database.updateChildren(updates) }
Java
private void onStarClicked(String uid, String key) { Map<String, Object> updates = new HashMap<>(); updates.put("posts/"+key+"/stars/"+uid, true); updates.put("posts/"+key+"/starCount", ServerValue.increment(1)); updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true); updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1)); mDatabase.updateChildren(updates); }
Este código não usa uma operação de transação. Portanto, ele não será executado automaticamente se houver uma atualização conflitante. No entanto, como a operação de incremento acontece diretamente no servidor de banco de dados, não há possibilidade de conflito.
Se você quiser detectar e rejeitar conflitos específicos do aplicativo, como um usuário marcando uma postagem que já havia marcado antes, escreva regras de segurança personalizadas para esse caso de uso.
Trabalhar com dados off-line
Se um cliente perder a conexão de rede, o app continuará funcionando.
Todos os clientes conectados a um banco de dados do Firebase mantêm uma versão interna própria de quaisquer dados em que os listeners estão sendo usados ou que foram sinalizados para manterem a sincronia com o servidor. Quando os dados são lidos ou gravados, a versão local dos dados é usada primeiro. Depois, o cliente do Firebase sincroniza esses dados com os servidores de banco de dados remotos e com outros clientes de acordo com o modelo "melhor esforço".
Por causa disso, todas as gravações no banco de dados acionarão eventos locais imediatamente antes de qualquer interação com o servidor. Isso significa que o app continuará responsivo, independentemente da conectividade ou da latência da rede.
Assim que a conectividade for restabelecida, seu app receberá o conjunto apropriado de eventos para que o cliente faça a sincronização com o estado atual do servidor sem precisar de um código personalizado.
Confira Saiba mais sobre recursos on-line e off-line se você quiser ver detalhes sobre esse assunto.
Próximas etapas
- Como trabalhar com listas de dados
- Saiba como estruturar dados
- Saiba mais sobre recursos on-line e off-line