Guarda datos

Este documento abarca los cuatro métodos para escribir datos en Firebase Realtime Database: set, update, push y la función transactions.

Maneras de guardar datos

set Escribir o reemplazar datos en una ruta de acceso definida, como messages/users/<username>
update Actualiza algunas de las claves de una ruta de acceso definida sin reemplazar todos los datos
push Agregar datos a una lista de datos en la base de datos. Cada vez que envías un nodo nuevo a una lista, tu base de datos genera una clave única, como messages/users/<unique-user-id>/<username>
transaction Usa transacciones cuando trabajes con datos complejos que podrían dañarse con las actualizaciones simultáneas

Guarda datos

La operación de escritura básica de la base de datos es un "set" que guarda datos nuevos en la referencia de base de datos especificada y reemplaza cualquier dato existente en esa ruta de acceso. Para comprender el método set, vamos a compilar una app de blog simple. Los datos de ella se almacenan en esta referencia de base de datos:

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');
Python
# Import database module.
from firebase_admin import db

# Get a database reference to our blog.
ref = db.reference('server/saving-data/fireblog')
Go
// 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")

Para comenzar, guardaremos algunos datos de usuarios. Almacenaremos cada usuario con un nombre de usuario único junto con su nombre completo y su fecha de nacimiento. Como cada usuario tendrá un nombre de usuario único, tiene sentido usar el método set aquí en lugar del método push, debido a que ya disponemos de la clave y no necesitamos crearla.

Primero, crea una referencia a la base de datos para los datos de los usuarios. A continuación, usa set() o setValue() para guardar un objeto de usuario en la base de datos con el nombre de usuario, su nombre completo y fecha de nacimiento. Se puede pasar una string, un número, un valor booleano, el valor null, una matriz o cualquier objeto JSON al método set. Si pasas null, se borrarán los datos de la ubicación especificada. En este caso, pasaremos un 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'
  }
});
Python
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'
    }
})
Go
// 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)
}

Cuando se guarda un objeto JSON en la base de datos, las propiedades del objeto se asignan automáticamente a ubicaciones secundarias en la base de datos de forma anidada. Ahora, si vamos a la URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, veremos el valor "Alan Turing". También podemos guardar datos directamente en una ubicación secundaria:

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'
});
Python
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'
})
Go
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)
}

En los dos ejemplos anteriores (escribir ambos valores al mismo tiempo que un objeto y escribirlos por separado en ubicaciones secundarias), los mismos datos se guardarán en tu base de datos:

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

El primer ejemplo solo activará un evento en clientes que miren los datos, mientras que el segundo activará dos. Es importante tener en cuenta que, si ya existían datos en usersRef, la primera opción los reemplazará, pero el segundo método solo modificará el valor de cada nodo secundario por separado y dejará otros elementos secundarios de usersRef sin modificar.

Cómo actualizar datos guardados

Si quieres escribir en varios campos secundarios de una ubicación de la base de datos al mismo tiempo, pero sin sobrescribir otros nodos secundarios, puedes usar el método update, como se muestra a continuación:

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'
});
Python
hopper_ref = users_ref.child('gracehop')
hopper_ref.update({
    'nickname': 'Amazing Grace'
})
Go
hopperRef := usersRef.Child("gracehop")
if err := hopperRef.Update(ctx, map[string]interface{}{
	"nickname": "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating child:", err)
}

Esto actualizará los datos de Grace para incluir su sobrenombre. Si hubieras usado set aquí en lugar de update, se habrían borrado tanto full_name como date_of_birth de tu hopperRef.

Firebase Realtime Database también es compatible con actualizaciones en varias rutas de acceso. Esto significa que el método update ahora puede actualizar valores en varias ubicaciones de tu base de datos a la vez, una potente función que te permite desnormalizar tus datos. Cuando se usan actualizaciones en varias rutas de acceso, es posible agregar sobrenombres a Alan y a Grace al mismo tiempo:

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'
});
Python
users_ref.update({
    'alanisawesome/nickname': 'Alan The Machine',
    'gracehop/nickname': 'Amazing Grace'
})
Go
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)
}

Después de esta actualización, se agregaron apodos a Alan y Grace:

{
  "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"
    }
  }
}

Ten en cuenta que, si intentas actualizar objetos escribiéndolos con las rutas de acceso incluidas, se generará un comportamiento diferente. Veamos lo que sucede si en lugar de esto intentas actualizar los datos de Grace y de Alan de la siguiente manera:

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'
  }
});
Python
users_ref.update({
    'alanisawesome': {
        'nickname': 'Alan The Machine'
    },
    'gracehop': {
        'nickname': 'Amazing Grace'
    }
})
Go
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)
}

Esto genera un comportamiento diferente; en particular, se reemplaza el nodo /users:

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

Cómo agregar una devolución de llamada de finalización

En los SDK de Admin de Node.js y Java, si deseas saber si se han confirmado los datos, puedes agregar una devolución de llamada de finalización. Tanto el método set como update en estos SDK aceptan una devolución de llamada de finalización opcional que se llama cuando la escritura se confirma en la base de datos. Si por alguna razón la llamada no funciona correctamente, la devolución de llamada recibirá un objeto de error que indicará el motivo. En los SDK de Admin de Python y Go, se bloquean todos los métodos de escritura. Es decir, los métodos de escritura no funcionarán hasta que las escrituras se confirmen en la base de datos.

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.');
  }
});

Cómo guardar listas de datos

Cuando se crean listas de datos, es importante tener en mente la naturaleza multiusuario de la mayoría de las apps y ajustar la estructura de tu lista según corresponda. Para ampliar la explicación del ejemplo anterior, agreguemos entradas de blog a tu app. Es posible que tu primer instinto sea usar el método set para almacenar campos secundarios con índices enteros de incremento automático, como el siguiente:

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

Si un usuario agrega una entrada nueva, se almacenaría como /posts/2. Esto funcionaría si un único autor agrega entradas, pero en tu aplicación de blog colaborativa muchos usuarios pueden agregar entradas al mismo tiempo. Si dos autores escriben en /posts/2 de manera simultánea, una de las entradas borrará la otra.

A fin de solucionar esto, los clientes de Firebase proporcionan una función push() que genera una clave única para cada elemento secundario nuevo. Puedes usar estas claves únicas secundarias para permitir que varios clientes agreguen elementos secundarios a la misma ubicación y al mismo tiempo sin conflictos de escritura.

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'
});
Python
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'
})
Go
// 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)
}

La clave única se basa en una marca de tiempo. Por lo tanto, los elementos de listas se ordenan cronológicamente de forma automática. Debido a que Firebase genera una clave única para cada entrada de blog, no se producen conflictos de escritura si varios usuarios agregan una entrada al mismo tiempo. Los datos de tu base de datos ahora tienen la siguiente apariencia:

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

En JavaScript, Python y Go, el patrón de llamadas push() y de llamadas inmediatas set() es tan común que el SDK de Firebase te permite combinarlos y pasar los datos que se configurarán directamente en push() de la siguiente manera:

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'
});;
Python
# This is equivalent to the calls to push().set(...) above
posts_ref.push({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})
Go
if _, err := postsRef.Push(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

Obtén la clave única que genera push()

Si llamas a push(), se mostrará una referencia a la ruta de acceso de datos nueva, que puedes usar para obtener la clave o configurar datos. El siguiente código generará los mismos datos que el ejemplo anterior, pero ahora tendremos acceso a la clave única que se creó.

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;
Python
# 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
Go
// 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 puedes ver, puedes obtener el valor de la clave única de la referencia a push().

En la siguiente sección sobre Recuperación de datos, aprenderemos a leer estos datos desde una base de datos de Firebase.

Cómo guardar datos de transacción

Cuando se trabaja con datos complejos que pueden dañarse con actualizaciones simultáneas, como los contadores incrementales, el SDK proporciona una operación de transacción.

En Java y Node.js, debes asignar dos devoluciones de llamada a esta operación de transacción: una función de actualización y una devolución de llamada de finalización opcional. En Python y Go, la operación de transacción se bloquea y, por lo tanto, solo acepta la función de actualización.

La función de actualización toma el estado actual de los datos como un argumento y debe devolver el nuevo estado deseado que quieres escribir. Por ejemplo, si quieres incrementar el número de votos a favor de una entrada específica de blog, debes escribir una transacción como la siguiente:

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;
});
Python
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')
Go
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)
}

En el ejemplo anterior, se verifica si el contador es null o si aún no se incrementó, dado que las transacciones se pueden llamar con null si no se escribió un valor predeterminado.

Si el código anterior se hubiese ejecutado sin una función de transacción y dos clientes intentan incrementarlo simultáneamente, ambos escribirían 1 como el valor nuevo, lo que genera solo un incremento en lugar de dos.

Conectividad de red y operaciones de escritura sin conexión

Los clientes Node.js y Java de Firebase mantienen su propia versión interna de los datos activos. Cuando se escriben datos, primero se hace en esta versión local. Luego, el cliente sincroniza esos datos con la base de datos y con otros clientes según el “mejor esfuerzo”.

Como resultado, todas las operaciones de escritura en la base de datos activan eventos locales de inmediato, antes de que se escriban datos en la base de datos. Esto significa que cuando escribes una aplicación con Firebase, tu app mantiene su capacidad de respuesta independientemente de la latencia de la red o la conexión a Internet.

Una vez que se restablece la conectividad, recibimos el conjunto de eventos que corresponda para que el cliente “se actualice” al estado actual del servidor, sin necesidad de escribir un código personalizado.

Protege tus datos

Firebase Realtime Database tiene un lenguaje de seguridad que te permite definir los usuarios que tienen acceso de lectura y escritura a diferentes nodos de tus datos. Obtén más información en el artículo Protege tus datos.