Zapisywanie danych

W tym dokumencie omawiamy 4 metody zapisywania danych na urządzeniu Firebase Realtime Database: set, update, push i obsługa transakcji.

Sposoby oszczędzania danych

ustawianie zapisywać lub zastępować dane w określonej ścieżce, np. messages/users/<username>
update aktualizować niektóre klucze na zdefiniowanej ścieżce bez zastępowania wszystkich danych;
push | wypychanie [in descriptive contexts] Dodaj do listy danych w bazie danych. Za każdym razem, gdy dodasz nowy węzeł do listy, baza danych wygeneruje unikalny klucz, np. messages/users/<unique-user-id>/<username>
transakcja Używaj transakcji podczas pracy ze złożonymi danymi, które mogą zostać uszkodzone przez jednoczesne aktualizacje.

Zapisywanie danych

Podstawowa operacja zapisu bazy danych to zbiór, który zapisuje nowe dane w określonym odwołaniu do bazy danych, zastępując wszystkie istniejące dane na tej ścieżce. Aby lepiej zrozumieć zestawy, utworzymy prostą aplikację do blogowania. Dane aplikacji są przechowywane w tej referencji bazy danych:

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")

Zacznijmy od zapisania danych użytkowników. Każdego użytkownika będziemy przechowywać pod unikalną nazwą użytkownika, a także jego imię i nazwisko oraz datę urodzenia. Każdy użytkownik ma unikalną nazwę użytkownika, więc warto użyć tutaj metody set zamiast metody push, ponieważ masz już klucz i nie musisz go tworzyć.

Najpierw utwórz odwołanie do bazy danych z danymi użytkownika. Następnie użyj set() / setValue(), aby zapisać obiekt użytkownika w bazie danych z nazwą użytkownika, imieniem i nazwiskiem oraz datą urodzenia. Możesz przekazać ciąg znaków, liczbę, wartość logiczną, null, tablicę lub dowolny obiekt JSON. Podanie wartości null spowoduje usunięcie danych z określonej lokalizacji. W tym przypadku przekazujesz mu obiekt:

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

Gdy obiekt JSON jest zapisywany w bazie danych, jego właściwości są automatycznie mapowane na elementy podrzędne bazy danych w postaci zagnieżdżonej. Jeśli teraz otworzysz adres URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, zobaczysz wartość „Alan Turing”. Możesz też zapisać dane bezpośrednio w lokalizacji podrzędnej:

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

W obu powyższych przykładach – zapisywanie obu wartości jednocześnie w ramach obiektu i oddzielnie w miejscach podrzędnych – spowoduje zapisanie tych samych danych w Twojej bazie danych:

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

Pierwszy przykład spowoduje wywołanie tylko 1 zdarzenia na klientach, którzy obserwują dane, a drugi – 2 zdarzeń. Pamiętaj, że jeśli w węźle usersRef istnieją już dane, pierwsze podejście spowoduje ich zastąpienie, ale drugie tylko zmodyfikuje wartość każdego osobnego węzła podrzędnego, nie zmieniając pozostałych podrzędnych węzłów węzła usersRef.

Aktualizowanie zapisanych danych

Jeśli chcesz zapisać dane w kilku elementach podrzędnych lokalizacji bazy danych jednocześnie, bez zastępowania innych węzłów podrzędnych, możesz użyć metody aktualizacji w ten sposób:

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

Spowoduje to zaktualizowanie danych Grace, aby zawierały jej pseudonim. Jeśli zamiast set here użyjesz update, zostanie usunięta zarówno full_name, jak i date_of_birthhopperRef.

Firebase Realtime Database obsługuje też aktualizacje wielościeżkowe. Oznacza to, że instrukcja update może teraz aktualizować wartości w wielu miejscach w bazie danych jednocześnie. Ta zaawansowana funkcja pomaga denormalizować dane. Korzystając z aktualizacji wielościeżkowych, możesz dodać przezwiska do Grace i Alana w tym samym czasie:

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

Po tej aktualizacji dodano do niego pseudonimy Alana i 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"
    }
  }
}

Pamiętaj, że próba zaktualizowania obiektów przez zapisanie obiektów z dołączonymi ścieżkami spowoduje inne działanie. Zobaczmy, co się stanie, jeśli zamiast tego spróbujesz zaktualizować informacje o Grace i Alanie w ten sposób:

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

Spowoduje to inne działanie, a mianowicie zastąpienie całego węzła /users:

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

Dodawanie wywołania zwrotnego po zakończeniu

Jeśli chcesz wiedzieć, kiedy Twoje dane zostały zapisane, możesz dodać funkcję zwrotną po zakończeniu w Node.js i Java Admin SDK. Zarówno metoda set, jak i update w tych pakietach SDK przyjmują opcjonalną funkcję zwrotną po zakończeniu, która jest wywoływana po zapisaniu danych w bazie danych. Jeśli wywołanie zakończy się niepowodzeniem z jakiegoś powodu, do funkcji wywołania zwrotnego zostanie przekazany obiekt błędu wskazujący przyczynę niepowodzenia. W pakietach Admin SDK w Pythonie i Go wszystkie metody zapisu są blokujące. Oznacza to, że metody zapisu nie zwracają danych, dopóki nie zostaną zapisane w bazie danych.

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

Zapisywanie list danych

Podczas tworzenia list danych należy pamiętać, że większość aplikacji jest przeznaczona dla wielu użytkowników, i odpowiednio dostosować strukturę listy. Rozwijając przykład powyżej, dodajmy do aplikacji wpisy na blogu. Pomysłem, który może nasunąć się jako pierwszy, jest użycie zbioru do przechowywania elementów potomnych z automatycznie zwiększającymi się indeksami całkowitymi, takimi jak:

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

Jeśli użytkownik doda nowy post, zostanie on zapisany jako /posts/2. Ta metoda działałaby, gdyby posty dodawał tylko jeden autor, ale w przypadku aplikacji do blogowania wiele osób może dodawać posty w tym samym czasie. Jeśli 2 autorzy piszą do /posts/2 jednocześnie, jeden z postów zostanie usunięty przez drugiego.

Aby rozwiązać ten problem, klienci Firebase udostępniają funkcję push(), która generuje unikalny klucz dla każdego nowego podrzędnego. Dzięki unikatowym kluczom podrzędnym kilka klientów może dodawać podrzędne do tego samego miejsca w tym samym czasie bez obaw o konflikty podczas zapisu.

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

Unikalny klucz jest oparty na sygnaturze czasowej, więc elementy listy będą automatycznie uporządkowane chronologicznie. Firebase generuje unikalny klucz dla każdego wpisu na blogu, więc jeśli wielu użytkowników doda wpis w tym samym czasie, nie wystąpi konflikt zapisu. Dane w bazie danych wyglądają teraz tak:

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

W JavaScript, Pythonie i Go wzór wywołania funkcji push(), a następnie natychmiast wywołania funkcji set() jest tak powszechny, że pakiet SDK Firebase umożliwia ich połączenie przez przekazanie danych do ustawienia bezpośrednio do funkcji push() w następujący sposób:

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

Pobieranie unikalnego klucza wygenerowanego przez push()

Wywołanie funkcji push() zwróci odwołanie do nowej ścieżki danych, której możesz użyć do uzyskania klucza lub ustawienia danych. Ten kod spowoduje uzyskanie tych samych danych co w przykładzie powyżej, ale teraz będziemy mieć dostęp do wygenerowanego niepowtarzalnego klucza:

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

Jak widzisz, możesz pobrać wartość unikalnego klucza z elementu odniesienia push().

W następnej sekcji Pobieranie danych dowiesz się, jak odczytywać te dane z bazy danych Firebase.

Zapisywanie danych transakcyjnych

Podczas pracy z zaawansowanymi danymi, które mogą zostać uszkodzone przez jednoczesne modyfikacje, np. z licznikami przyrostowymi, pakiet SDK udostępnia operację transakcji.

W Java i Node.js operacja transakcji ma 2 funkcje wywołania zwrotnego: funkcję aktualizacji i opcjonalną funkcję wywołania zwrotnego po zakończeniu. W Pythonie i Go operacja transakcji jest blokująca i dlatego akceptuje tylko funkcję aktualizacji.

Funkcja update przyjmuje jako argument bieżący stan danych i powinna zwrócić nowy pożądany stan, który chcesz zapisać. Jeśli np. chcesz zwiększyć liczbę głosów pozytywnych pod określonym postem na blogu, musisz napisać taką transakcję:

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

W powyższym przykładzie sprawdzamy, czy licznik ma wartość null, czy też nie został jeszcze zwiększony. Transakcje mogą być wywoływane z wartością null, jeśli nie została zapisana żadna wartość domyślna.

Gdyby powyższy kod został uruchomiony bez funkcji transakcji, a 2 klienci próbowali zwiększyć jego wartość jednocześnie, obaj zapisaliby wartość 1 jako nową wartość, co spowodowałoby zwiększenie o 1 zamiast o 2.

Połączenia sieciowe i zapisywanie offline

Klienci Firebase Node.js i Java utrzymują własną wewnętrzną wersję wszystkich aktywnych danych. Gdy dane są zapisywane, są najpierw zapisywane w tej wersji lokalnej. Następnie klient synchronizuje te dane z bazą danych oraz z innymi klientami według zasady „najlepszego wysiłku”.

W rezultacie wszystkie zapisy w bazie danych będą wywoływać zdarzenia lokalne natychmiast, jeszcze przed zapisaniem jakichkolwiek danych w bazie danych. Oznacza to, że gdy tworzysz aplikację za pomocą Firebase, Twoja aplikacja będzie reagować niezależnie od opóźnień w sieci lub połączenia z internetem.

Gdy połączenie zostanie przywrócone, otrzymamy odpowiedni zestaw zdarzeń, dzięki czemu klient „nadrobi zaległości” w stosunku do bieżącego stanu serwera bez konieczności pisania kodu niestandardowego.

Zabezpieczanie danych

Firebase Realtime Database ma język zabezpieczeń, który pozwala określić, którzy użytkownicy mają dostęp do odczytu i zapisu do różnych węzłów danych. Więcej informacji znajdziesz w artykule Bezpieczeństwo danych.