Ler e gravar dados na Web

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:

  1. Para se conectar ao emulador, adicione uma linha de código à configuração de teste do app.
  2. Execute firebase emulators:start na raiz do diretório do projeto local.
  3. 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 uma referência do banco de dados

Para ler ou gravar dados no banco de dados, é necessário uma instância de firebase.database.Reference:

Web

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web

var database = firebase.database();

Gravar dados

Neste documento, você encontra os conceitos básicos sobre como recuperar, ordenar e filtrar dados do Firebase.

Os dados do Firebase são recuperados quando um listener assíncrono é anexado a um firebase.database.Reference. Ele é acionado uma vez no estado inicial dos dados e posteriormente, quando há alterações.

Operações básicas de gravação

Em operações básicas de gravação, use set() para salvar dados em uma referência específica e substitua os dados existentes no caminho. Por exemplo, um aplicativo de blog social pode adicionar um usuário com set() da seguinte maneira:

Web

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

Web

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

O uso de set() substitui os dados no local especificado, incluindo qualquer nó filho.

Ler dados

Detectar eventos de valor

Para ler dados em um caminho e detectar alterações, use onValue() para observar eventos. É possível usar esse evento para ler snapshots estáticos do conteúdo em um determinado caminho, já que eles existiam 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. A chamada de retorno 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 val() nele.

Veja no exemplo a seguir um app de blog em rede social recuperando o número de estrelas de uma postagem do banco de dados:

Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

Web

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

O listener recebe um snapshot que contém os dados no local especificado no banco de dados no momento do evento. É possível recuperar os dados no snapshot com o método val().

Ler dados uma vez

Ler dados uma vez com 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 eventos de valor 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.

Web

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Web

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Ler dados uma vez com um observador

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. Nessas situações, use once() para receber imediatamente os dados do cache do disco local.

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 exemplos anteriores usa este método para carregar o perfil de um usuário quando ele começa a escrever uma nova postagem:

Web

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

Web

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

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 update().

Ao chamar update(), 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 em rede social pode criar uma postagem e atualizá-la simultaneamente no feed de atividades recentes e no feed do autor da postagem usando um código como este:

Web

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

Web

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

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. É possível usar a chave 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 update(), 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 retorno de chamada de conclusão

Se você quiser saber quando seus dados foram confirmados, adicione um retorno de chamada de conclusão. set() e update() recebem um retorno de chamada 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, a chamada de retorno receberá um objeto de erro indicando o motivo da falha.

Web

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

Web

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

Excluir dados

A maneira mais simples de excluir os dados é chamar remove() 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 set() ou update(). Use essa técnica com update() para excluir vários filhos em uma única chamada de API.

Receber um Promise

Para saber quando os dados são confirmados no servidor do Firebase Realtime Database, use Promise. set() e update() podem retornar um Promise, que pode ser usado para descobrir quando a gravação é confirmada no banco de dados.

Remover listeners

Para remover retornos de chamada, chame o método off() na sua referência ao banco de dados do Firebase.

É possível remover um único listener passando-o como um parâmetro para off(). Chamar off() no local sem argumentos remove todos os listeners desse local.

Chamar off() em um listener pai não remove automaticamente os listeners registrados nos nós filhos dele. O off() também precisa ser chamado nos listeners filhos para remover a chamada de retorno.

Salvar dados como transações

Para tratar dados corrompidos por modificações simultâneas, como contadores incrementais, use uma operação de transação. Essa operação aceita uma função de atualização e um retorno de chamada 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:

Web

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Web

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

A transação impede uma contagem incorreta de estrelas se vários usuários adicionam simultaneamente estrelas à mesma postagem ou se o cliente tem 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 repete até que a transação seja aceita ou cancelada.

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.

Web

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

Web

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(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 a própria versão interna de dados ativos. A gravação deles ocorre primeiro nessa versão local. Depois, o cliente do Firebase sincroniza esses dados com os servidores remotos e com outros clientes de acordo com o modelo "melhor esforço".

Consequentemente, todas as gravações no banco de dados acionam eventos locais, antes de qualquer dado ser gravado no servidor, e o app continua 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