Salvando dados

Este documento aborda os quatro métodos para gravar dados no Firebase Realtime Database: configuração, atualização, push e suporte a transações.

Maneiras de salvar dados

definir Escreva ou substitua dados em um caminho definido , como messages/users/<username>
atualizar Atualize algumas das chaves para um caminho definido sem substituir todos os dados
empurrar Adicione a uma lista de dados no banco de dados. Cada vez que você envia um novo nó para uma lista, seu banco de dados gera uma chave exclusiva, como messages/users/<unique-user-id>/<username>
transação Use transações ao trabalhar com dados complexos que podem ser corrompidos por atualizações simultâneas

Salvando dados

A operação básica de gravação do banco de dados é um conjunto que salva novos dados na referência de banco de dados especificada, substituindo quaisquer dados existentes nesse caminho. Para entender o conjunto, construiremos um aplicativo de blog simples. Os dados do seu aplicativo são armazenados nesta referência de banco de dados:

Java
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog");
Node.js
// Import Admin SDK
const { getDatabase } = require('firebase-admin/database');

// Get a database reference to our blog
const db = getDatabase();
const ref = db.ref('server/saving-data/fireblog');
Pitão
# Import database module.
from firebase_admin import db

# Get a database reference to our blog.
ref = db.reference('server/saving-data/fireblog')
Ir
// Create a database client from App.
client, err := app.Database(ctx)
if err != nil {
	log.Fatalln("Error initializing database client:", err)
}

// Get a database reference to our blog.
ref := client.NewRef("server/saving-data/fireblog")

Vamos começar salvando alguns dados do usuário. Armazenaremos cada usuário com um nome de usuário exclusivo e também armazenaremos seu nome completo e data de nascimento. Como cada usuário terá um nome de usuário exclusivo, faz sentido usar o método set aqui em vez do método push, pois você já possui a chave e não precisa criar uma.

Primeiro, crie uma referência de banco de dados para os dados do seu usuário. Em seguida, use set() / setValue() para salvar um objeto de usuário no banco de dados com o nome de usuário, nome completo e data de nascimento do usuário. Você pode passar set uma string, número, booleano, null , array ou qualquer objeto JSON. Passar null removerá os dados no local especificado. Neste caso você passará um objeto:

Java
public static class User {

  public String date_of_birth;
  public String full_name;
  public String nickname;

  public User(String dateOfBirth, String fullName) {
    // ...
  }

  public User(String dateOfBirth, String fullName, String nickname) {
    // ...
  }

}

DatabaseReference usersRef = ref.child("users");

Map<String, User> users = new HashMap<>();
users.put("alanisawesome", new User("June 23, 1912", "Alan Turing"));
users.put("gracehop", new User("December 9, 1906", "Grace Hopper"));

usersRef.setValueAsync(users);
Node.js
const usersRef = ref.child('users');
usersRef.set({
  alanisawesome: {
    date_of_birth: 'June 23, 1912',
    full_name: 'Alan Turing'
  },
  gracehop: {
    date_of_birth: 'December 9, 1906',
    full_name: 'Grace Hopper'
  }
});
Pitão
users_ref = ref.child('users')
users_ref.set({
    'alanisawesome': {
        'date_of_birth': 'June 23, 1912',
        'full_name': 'Alan Turing'
    },
    'gracehop': {
        'date_of_birth': 'December 9, 1906',
        'full_name': 'Grace Hopper'
    }
})
Ir

// User is a json-serializable type.
type User struct {
	DateOfBirth string `json:"date_of_birth,omitempty"`
	FullName    string `json:"full_name,omitempty"`
	Nickname    string `json:"nickname,omitempty"`
}

usersRef := ref.Child("users")
err := usersRef.Set(ctx, map[string]*User{
	"alanisawesome": {
		DateOfBirth: "June 23, 1912",
		FullName:    "Alan Turing",
	},
	"gracehop": {
		DateOfBirth: "December 9, 1906",
		FullName:    "Grace Hopper",
	},
})
if err != nil {
	log.Fatalln("Error setting value:", err)
}

Quando um objeto JSON é salvo no banco de dados, as propriedades do objeto são mapeadas automaticamente para locais filho do banco de dados de forma aninhada. Agora, se você navegar até a URL https://docs-examples.firebaseio.com/server/ saving-data/fireblog/users/alanisawesome/full_name , veremos o valor "Alan Turing". Você também pode salvar dados diretamente em um local filho:

Java
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing"));
usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
Node.js
const usersRef = ref.child('users');
usersRef.child('alanisawesome').set({
  date_of_birth: 'June 23, 1912',
  full_name: 'Alan Turing'
});
usersRef.child('gracehop').set({
  date_of_birth: 'December 9, 1906',
  full_name: 'Grace Hopper'
});
Pitão
users_ref.child('alanisawesome').set({
    'date_of_birth': 'June 23, 1912',
    'full_name': 'Alan Turing'
})
users_ref.child('gracehop').set({
    'date_of_birth': 'December 9, 1906',
    'full_name': 'Grace Hopper'
})
Ir
if err := usersRef.Child("alanisawesome").Set(ctx, &User{
	DateOfBirth: "June 23, 1912",
	FullName:    "Alan Turing",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

if err := usersRef.Child("gracehop").Set(ctx, &User{
	DateOfBirth: "December 9, 1906",
	FullName:    "Grace Hopper",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

Os dois exemplos acima - escrever ambos os valores ao mesmo tempo como um objeto e gravá-los separadamente em locais filhos - resultarão no salvamento dos mesmos dados em seu banco de dados:

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper"
    }
  }
}

O primeiro exemplo acionará apenas um evento em clientes que estão monitorando os dados, enquanto o segundo exemplo acionará dois. É importante observar que se os dados já existissem em usersRef , a primeira abordagem os substituiria, mas o segundo método apenas modificaria o valor de cada nó filho separado, deixando os outros filhos de usersRef inalterados.

Atualizando dados salvos

Se quiser gravar em vários filhos de um local de banco de dados ao mesmo tempo sem substituir outros nós filhos, você pode usar o método de atualização conforme mostrado abaixo:

Java
DatabaseReference hopperRef = usersRef.child("gracehop");
Map<String, Object> hopperUpdates = new HashMap<>();
hopperUpdates.put("nickname", "Amazing Grace");

hopperRef.updateChildrenAsync(hopperUpdates);
Node.js
const usersRef = ref.child('users');
const hopperRef = usersRef.child('gracehop');
hopperRef.update({
  'nickname': 'Amazing Grace'
});
Pitão
hopper_ref = users_ref.child('gracehop')
hopper_ref.update({
    'nickname': 'Amazing Grace'
})
Ir
hopperRef := usersRef.Child("gracehop")
if err := hopperRef.Update(ctx, map[string]interface{}{
	"nickname": "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating child:", err)
}

Isso atualizará os dados de Grace para incluir seu apelido. Se você tivesse usado set aqui em vez de update, teria excluído full_name e date_of_birth do seu hopperRef .

O Firebase Realtime Database também oferece suporte a atualizações de vários caminhos. Isso significa que a atualização agora pode atualizar valores em vários locais do seu banco de dados ao mesmo tempo, um recurso poderoso que ajuda a desnormalizar seus dados . Usando atualizações de vários caminhos, você pode adicionar apelidos para Grace e Alan ao mesmo tempo:

Java
Map<String, Object> userUpdates = new HashMap<>();
userUpdates.put("alanisawesome/nickname", "Alan The Machine");
userUpdates.put("gracehop/nickname", "Amazing Grace");

usersRef.updateChildrenAsync(userUpdates);
Node.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome/nickname': 'Alan The Machine',
  'gracehop/nickname': 'Amazing Grace'
});
Pitão
users_ref.update({
    'alanisawesome/nickname': 'Alan The Machine',
    'gracehop/nickname': 'Amazing Grace'
})
Ir
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome/nickname": "Alan The Machine",
	"gracehop/nickname":      "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

Após esta atualização, Alan e Grace tiveram seus apelidos adicionados:

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing",
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper",
      "nickname": "Amazing Grace"
    }
  }
}

Observe que tentar atualizar objetos escrevendo objetos com os caminhos incluídos resultará em um comportamento diferente. Vamos dar uma olhada no que acontece se você tentar atualizar Grace e Alan desta forma:

Java
Map<String, Object> userNicknameUpdates = new HashMap<>();
userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine"));
userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace"));

usersRef.updateChildrenAsync(userNicknameUpdates);
Node.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome': {
    'nickname': 'Alan The Machine'
  },
  'gracehop': {
    'nickname': 'Amazing Grace'
  }
});
Pitão
users_ref.update({
    'alanisawesome': {
        'nickname': 'Alan The Machine'
    },
    'gracehop': {
        'nickname': 'Amazing Grace'
    }
})
Ir
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome": &User{Nickname: "Alan The Machine"},
	"gracehop":      &User{Nickname: "Amazing Grace"},
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

Isso resulta em um comportamento diferente, ou seja, sobrescrevendo todo o nó /users :

{
  "users": {
    "alanisawesome": {
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "nickname": "Amazing Grace"
    }
  }
}

Adicionando um retorno de chamada de conclusão

Em Node.js e Java Admin SDKs, se quiser saber quando seus dados foram confirmados, você pode adicionar um retorno de chamada de conclusão. Os métodos set e update nesses SDKs recebem um retorno de chamada de conclusão opcional que é chamado quando a gravação é confirmada no banco de dados. Se a chamada não tiver êxito por algum motivo, o retorno de chamada receberá um objeto de erro indicando o motivo da falha. Nos SDKs Admin Python e Go, todos os métodos de gravação são bloqueadores. Ou seja, os métodos de gravação não retornam até que as gravações sejam confirmadas no banco de dados.

Java
DatabaseReference dataRef = ref.child("data");
dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() {
  @Override
  public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
    if (databaseError != null) {
      System.out.println("Data could not be saved " + databaseError.getMessage());
    } else {
      System.out.println("Data saved successfully.");
    }
  }
});
Node.js
dataRef.set('I\'m writing data', (error) => {
  if (error) {
    console.log('Data could not be saved.' + error);
  } else {
    console.log('Data saved successfully.');
  }
});

Salvando listas de dados

Ao criar listas de dados, é importante ter em mente a natureza multiusuário da maioria dos aplicativos e ajustar a estrutura da lista de acordo. Expandindo o exemplo acima, vamos adicionar postagens de blog ao seu aplicativo. Seu primeiro instinto pode ser usar set para armazenar filhos com índices inteiros com incremento automático, como o seguinte:

// NOT RECOMMENDED - use push() instead!
{
  "posts": {
    "0": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "1": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

Se um usuário adicionar uma nova postagem, ela será armazenada como /posts/2 . Isso funcionaria se apenas um único autor adicionasse postagens, mas em seu aplicativo de blog colaborativo muitos usuários podem adicionar postagens ao mesmo tempo. Se dois autores escreverem para /posts/2 simultaneamente, um dos posts será deletado pelo outro.

Para resolver isso, os clientes Firebase fornecem uma função push() que gera uma chave exclusiva para cada novo filho . Ao usar chaves filhas exclusivas, vários clientes podem adicionar filhos ao mesmo local ao mesmo tempo, sem se preocupar com conflitos de gravação.

Java
public static class Post {

  public String author;
  public String title;

  public Post(String author, String title) {
    // ...
  }

}

DatabaseReference postsRef = ref.child("posts");

DatabaseReference newPostRef = postsRef.push();
newPostRef.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language"));

// We can also chain the two calls together
postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
Node.js
const newPostRef = postsRef.push();
newPostRef.set({
  author: 'gracehop',
  title: 'Announcing COBOL, a New Programming Language'
});

// we can also chain the two calls together
postsRef.push().set({
  author: 'alanisawesome',
  title: 'The Turing Machine'
});
Pitão
posts_ref = ref.child('posts')

new_post_ref = posts_ref.push()
new_post_ref.set({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})

# We can also chain the two calls together
posts_ref.push().set({
    'author': 'alanisawesome',
    'title': 'The Turing Machine'
})
Ir

// Post is a json-serializable type.
type Post struct {
	Author string `json:"author,omitempty"`
	Title  string `json:"title,omitempty"`
}

postsRef := ref.Child("posts")

newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

if err := newPostRef.Set(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

// We can also chain the two calls together
if _, err := postsRef.Push(ctx, &Post{
	Author: "alanisawesome",
	Title:  "The Turing Machine",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

A chave exclusiva é baseada em um carimbo de data/hora, portanto os itens da lista serão automaticamente ordenados cronologicamente. Como o Firebase gera uma chave exclusiva para cada postagem do blog, nenhum conflito de gravação ocorrerá se vários usuários adicionarem uma postagem ao mesmo tempo. Os dados do seu banco de dados agora ficam assim:

{
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "-JRHTHaKuITFIhnj02kE": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

Em JavaScript, Python e Go, o padrão de chamar push() e chamar imediatamente set() é tão comum que o SDK do Firebase permite combiná-los passando os dados a serem definidos diretamente para push() da seguinte forma:

Java
// No Java equivalent
Node.js
// This is equivalent to the calls to push().set(...) above
postsRef.push({
  author: 'gracehop',
  title: 'Announcing COBOL, a New Programming Language'
});;
Pitão
# This is equivalent to the calls to push().set(...) above
posts_ref.push({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})
Ir
if _, err := postsRef.Push(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

Obtendo a chave exclusiva gerada por push()

Chamar push() retornará uma referência ao novo caminho de dados, que você pode usar para obter a chave ou definir dados para ela. O código a seguir resultará nos mesmos dados do exemplo acima, mas agora teremos acesso à chave exclusiva que foi gerada:

Java
// Generate a reference to a new location and add some data using push()
DatabaseReference pushedPostRef = postsRef.push();

// Get the unique ID generated by a push()
String postId = pushedPostRef.getKey();
Node.js
// Generate a reference to a new location and add some data using push()
const newPostRef = postsRef.push();

// Get the unique key generated by push()
const postId = newPostRef.key;
Pitão
# Generate a reference to a new location and add some data using push()
new_post_ref = posts_ref.push()

# Get the unique key generated by push()
post_id = new_post_ref.key
Ir
// Generate a reference to a new location and add some data using Push()
newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

// Get the unique key generated by Push()
postID := newPostRef.Key

Como você pode ver, você pode obter o valor da chave exclusiva na sua referência push() .

Na próxima seção sobre Recuperação de dados , aprenderemos como ler esses dados de um banco de dados Firebase.

Salvando dados transacionais

Ao trabalhar com dados complexos que podem ser corrompidos por modificações simultâneas, como contadores incrementais, o SDK fornece uma operação de transação .

Em Java e Node.js, você fornece à operação de transação dois retornos de chamada: uma função de atualização e um retorno de chamada de conclusão opcional. Em Python e Go, a operação de transação está bloqueando e portanto aceita apenas a função de atualização.

A função de atualização toma o estado atual dos dados como argumento e deve retornar o novo estado desejado que você gostaria de escrever. Por exemplo, se você quiser aumentar o número de votos positivos em uma postagem específica do blog, você escreveria uma transação como esta:

Java
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes");
upvotesRef.runTransaction(new Transaction.Handler() {
  @Override
  public Transaction.Result doTransaction(MutableData mutableData) {
    Integer currentValue = mutableData.getValue(Integer.class);
    if (currentValue == null) {
      mutableData.setValue(1);
    } else {
      mutableData.setValue(currentValue + 1);
    }

    return Transaction.success(mutableData);
  }

  @Override
  public void onComplete(
      DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) {
    System.out.println("Transaction completed");
  }
});
Node.js
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes');
upvotesRef.transaction((current_value) => {
  return (current_value || 0) + 1;
});
Pitão
def increment_votes(current_value):
    return current_value + 1 if current_value else 1

upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes')
try:
    new_vote_count = upvotes_ref.transaction(increment_votes)
    print('Transaction completed')
except db.TransactionAbortedError:
    print('Transaction failed to commit')
Ir
fn := func(t db.TransactionNode) (interface{}, error) {
	var currentValue int
	if err := t.Unmarshal(&currentValue); err != nil {
		return nil, err
	}
	return currentValue + 1, nil
}

ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes")
if err := ref.Transaction(ctx, fn); err != nil {
	log.Fatalln("Transaction failed to commit:", err)
}

O exemplo acima verifica se o contador é null ou ainda não foi incrementado, pois as transações podem ser chamadas com null se nenhum valor padrão tiver sido gravado.

Se o código acima tivesse sido executado sem uma função de transação e dois clientes tentassem incrementá-lo simultaneamente, ambos escreveriam 1 como o novo valor, resultando em um incremento em vez de dois.

Conectividade de rede e gravações offline

Os clientes Firebase Node.js e Java mantêm sua própria versão interna de quaisquer dados ativos. Quando os dados são gravados, eles são gravados primeiro nesta versão local. O cliente então sincroniza esses dados com o banco de dados e com outros clientes com base no “melhor esforço”.

Como resultado, todas as gravações no banco de dados acionarão eventos locais imediatamente, antes mesmo de qualquer dado ter sido gravado no banco de dados. Isso significa que quando você escreve um aplicativo usando o Firebase, ele permanecerá responsivo, independentemente da latência da rede ou da conectividade com a Internet.

Assim que a conectividade for restabelecida, receberemos o conjunto apropriado de eventos para que o cliente "alcance" o estado atual do servidor, sem precisar escrever nenhum código customizado.

Protegendo seus dados

O Firebase Realtime Database possui uma linguagem de segurança que permite definir quais usuários terão acesso de leitura e gravação aos diferentes nós dos seus dados. Você pode ler mais sobre isso em Proteja seus dados .