Risparmio di dati

Questo documento illustra i quattro metodi per scrivere dati in Firebase Realtime Database: set, update, push e il supporto delle transazioni.

Modi per risparmiare dati

set Scrivere o sostituire i dati in un percorso definito, ad esempio messages/users/<username>
update Aggiornare alcune delle chiavi per un percorso definito senza sostituire tutti i dati
eseguire il push Aggiungi a un elenco di dati nel database. Ogni volta che inserisci un nuovo nodo in un elenco, il database genera una chiave univoca, ad esempio messages/users/<unique-user-id>/<username>
transazione Utilizza le transazioni quando lavori con dati complessi che potrebbero essere danneggiati da aggiornamenti simultanei

Salvataggio dei dati

L'operazione di scrittura di base del database è un insieme che salva i nuovi dati nel riferimento del database specificato, sostituendo eventuali dati esistenti in quel percorso. Per comprendere l'insieme, creeremo un'app di blogging semplice. I dati della tua app vengono archiviati in questo riferimento del database:

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')
Vai
// 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")

Iniziamo salvando alcuni dati utente. Memorizzeremo ogni utente tramite un nome utente univoco, nonché il nome completo e la data di nascita. Poiché ogni utente avrà un nome utente univoco, ha senso utilizzare il metodo set anziché il metodo push, poiché disponi già della chiave e non è necessario crearne una.

Innanzitutto, crea un riferimento a un database per i dati utente. Quindi, utilizza set() / setValue() per salvare un oggetto utente nel database con il nome utente, il nome completo e la data di nascita dell'utente. Puoi impostare una stringa, un numero, un valore booleano, null, un array o qualsiasi oggetto JSON. Se passi null, i dati nella posizione specificata verranno rimossi. In questo caso, dovrai passare un oggetto:

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'
    }
})
Vai
// 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 un oggetto JSON viene salvato nel database, le proprietà dell'oggetto vengono mappate automaticamente alle posizioni secondarie del database in modo nidificato. Ora, se accedi all'URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, vedrai il valore "Alan Turing". Puoi anche salvare i dati direttamente in una posizione del bambino:

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'
})
Vai
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)
}

I due esempi precedenti, che scrivono entrambi i valori contemporaneamente come oggetto e separatamente nelle posizioni secondarie, fanno sì che gli stessi dati vengano salvati nel database:

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

Il primo esempio attiverà un solo evento sui client che monitorano i dati, mentre il secondo ne attiverà due. È importante notare che se i dati esistevano già in usersRef, il primo approccio li sovrascriverebbe, ma il secondo metodo modificherebbe solo il valore di ogni nodo secondario separato lasciando invariati gli altri elementi secondari di usersRef.

Aggiornamento dei dati salvati

Se vuoi scrivere contemporaneamente in più nodi figlio di una posizione di database senza sovrascrivere altri nodi figlio, puoi utilizzare il metodo di aggiornamento come mostrato di seguito:

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

I dati di Grace verranno aggiornati in modo da includere il nickname. Se avessi utilizzato set qui anziché update, sarebbero stati eliminati sia full_name sia date_of_birth da hopperRef.

Firebase Realtime Database supporta anche gli aggiornamenti multipath. Ciò significa che ora l'aggiornamento può aggiornare contemporaneamente i valori in più posizioni del database, una funzionalità potente che consente di denormalizzare i dati. Con gli aggiornamenti multipath, puoi aggiungere contemporaneamente i nickname a Grace e Alan:

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'
})
Vai
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)
}

Dopo questo aggiornamento, sono stati aggiunti i nickname di Alan e 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"
    }
  }
}

Tieni presente che il tentativo di aggiornare gli oggetti scrivendoli con i percorsi inclusi avrà un comportamento diverso. Vediamo cosa succede se provi ad aggiornare Grace e Alan in questo modo:

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'
    }
})
Vai
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)
}

Il risultato è un comportamento diverso, ovvero l'intera sovrascrittura del nodo /users:

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

Aggiunta di una chiamata di ritorno di completamento

Negli SDK Node.js e Java Admin, se vuoi sapere quando è stato eseguito il commit dei dati, puoi aggiungere un callback di completamento. Entrambi i metodi set e update in questi SDK accettano un callback di completamento facoltativo che viene chiamato quando la scrittura è stata eseguita nel database. Se la chiamata non è andata a buon fine per qualche motivo, al callback viene passato un oggetto errore che indica il motivo dell'errore. Negli SDK amministrativi Python e Go, tutti i metodi di scrittura sono bloccati. In altre parole, i metodi di scrittura non resituiscono finché le scritture non vengono applicate al database.

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

Salvare elenchi di dati

Quando crei elenchi di dati, è importante tenere in considerazione la natura multiutente della maggior parte delle applicazioni e modificare di conseguenza la struttura degli elenchi. Ampliando l'esempio precedente, aggiungiamo i post del blog alla tua app. Il primo istinto potrebbe essere quello di utilizzare set per memorizzare gli elementi secondari con indici interi con incremento automatico, come segue:

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

Se un utente aggiunge un nuovo post, questo viene archiviato come /posts/2. Questo funzionerebbe se solo un autore avesse aggiunto i post, ma nella tua applicazione di blogging collaborativo molti utenti possono aggiungerli contemporaneamente. Se due autori scrivono in /posts/2 contemporaneamente, uno dei post viene eliminato dall'altro.

Per risolvere il problema, i client Firebase forniscono una funzione push() che genera una chiave unica per ogni nuovo asset figlio. Utilizzando chiavi secondarie univoche, più client possono aggiungere elementi secondari alla stessa posizione contemporaneamente senza preoccuparsi di conflitti di scrittura.

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'
})
Vai
// 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 chiave univoca è basata su un timestamp, quindi gli elementi dell'elenco verranno ordinati automaticamente in ordine cronologico. Poiché Firebase genera una chiave univoca per ogni post del blog, non si verificano conflitti di scrittura se più utenti aggiungono un post contemporaneamente. I dati del database ora hanno il seguente aspetto:

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

In JavaScript, Python e Go, lo schema di chiamata di push() e poi immediatamente di set() è così comune che l'SDK Firebase ti consente di combinarli passando i dati da impostare direttamente a push() come segue:

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'
})
Vai
if _, err := postsRef.Push(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

Ottenere la chiave univoca generata da push()

La chiamata a push() restituirà un riferimento al nuovo percorso dei dati, che puoi utilizzare per recuperare la chiave o impostare i dati. Il codice seguente produrrà gli stessi dati dell'esempio precedente, ma ora avremo accesso alla chiave univoca generata:

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
Vai
// 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

Come puoi vedere, puoi ottenere il valore della chiave univoca dal riferimento push().

Nella sezione successiva sul recupero dei dati, impareremo a leggere questi dati da un database Firebase.

Salvataggio dei dati transazionali

Quando si utilizzano dati complessi che potrebbero essere danneggiati da modifiche simultanee, come i contatori incrementali, l'SDK fornisce un'operazione di transazione.

In Java e Node.js, fornisci all'operazione di transazione due callback: una funzione di aggiornamento e un callback di completamento facoltativo. In Python e Go, l'operazione di transazione è bloccante e pertanto accetta solo la funzione di aggiornamento.

La funzione di aggiornamento prende come argomento lo stato attuale dei dati e deve restituire il nuovo stato desiderato che vuoi scrivere. Ad esempio, se vuoi aumentare il numero di upvote su un determinato post del blog, devi scrivere una transazione come la seguente:

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')
Vai
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)
}

L'esempio riportato sopra verifica se il contatore è null o non è ancora stato incrementato, poiché le transazioni possono essere chiamate con null se non è stato scritto alcun valore predefinito.

Se il codice riportato sopra fosse stato eseguito senza una funzione di transazione e due client avessero tentato di incrementarlo contemporaneamente, entrambi scriverebbero 1 come nuovo valore, con un solo incremento anziché due.

Connettività di rete e scritture offline

I client Firebase Node.js e Java mantengono la propria versione interna di tutti i dati attivi. Quando i dati vengono scritti, vengono prima scritti in questa versione locale. Il client sincronizza quindi i dati con il database e con altri client secondo il criterio del "best effort".

Di conseguenza, tutte le scritture nel database attiveranno immediatamente gli eventi locali, prima che i dati vengano scritti nel database. Ciò significa che quando scrivi un'applicazione utilizzando Firebase, la tua app rimane reattiva indipendentemente dalla latenza della rete o dalla connettività a internet.

Una volta stabilita nuovamente la connettività, riceveremo l'insieme appropriato di eventi in modo che il client "si aggiorni" con lo stato corrente del server, senza dover scrivere codice personalizzato.

Protezione dei dati

Firebase Realtime Database ha un linguaggio di sicurezza che ti consente di definire quali utenti hanno accesso in lettura e scrittura a diversi nodi dei tuoi dati. Puoi scoprire di più in Proteggi i tuoi dati.