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 Zapisz lub zastąp dane zdefiniowaną ścieżką, 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 Transakcje możesz wykorzystać podczas pracy ze złożonymi danymi, które mogą ulec uszkodzeniu w wyniku równoczesnych aktualizacji.

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 trochę danych użytkowników. Przechowujemy dane każdego użytkownika pod unikalną nazwą użytkownika, a także przechowujemy jego pełne 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 do swoich danych 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 otworzysz adres URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, zobaczymy 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 jednocześnie zapisywać dane w wielu węzłach podrzędnych lokalizacji bazy danych bez zastępowania innych węzłów podrzędnych, możesz użyć metody aktualizacji, jak pokazano poniżej:

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 zakończenia wywołania zwrotnego

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 metody ustawiania, jak i aktualizacji w tych pakietach SDK wykonują opcjonalne wywołanie zwrotne ukończenia, które jest wywoływane po zatwierdzeniu zapisu w bazie danych. Jeśli wywołanie zakończyło się niepowodzeniem, callbackowi zostanie przekazany obiekt błędu wskazujący przyczynę niepowodzenia. W pakietach SDK Python i Go Admin wszystkie metody zapisu są blokowane. Oznacza to, że metody zapisu nie zwracają żadnych 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. Kontynuując poprzedni przykład, dodajmy posty na blogu do Twojej aplikacji. Na początek możesz użyć ustawienia przechowywania elementów podrzędnych z automatycznie rosnącymi indeksami całkowitymi, jak w tym przykładzie:

// 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, klienty Firebase udostępniają funkcję push(), która generuje unikalny klucz dla każdego nowego elementu podrzędnego. Dzięki użyciu unikalnych kluczy podrzędnych kilka klientów może w tym samym czasie dodawać elementy podrzędne do tej samej lokalizacji, nie martwiąc się o konflikty 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 językach JavaScript, Python i Go wzorzec wywoływania push(), a następnie natychmiastowego wywołania set(), jest tak powszechny, że pakiet SDK Firebase umożliwia ich połączenie przez przekazanie danych, które można ustawić bezpośrednio na push() w ten 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. Poniższy kod zwróci te same dane co z przykładu powyżej, ale uzyskamy dostęp do wygenerowanego unikalnego 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 poświęconej pobieraniu 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 Javie i Node.js dajesz operacji transakcji 2 wywołania zwrotne: funkcję aktualizacji i opcjonalne wywołanie zwrotne ukończenia. W Pythonie i Go operacja transakcji jest blokowana 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

Klienty Firebase Node.js i Java obsługują 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. 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ń umożliwiający określenie, którzy użytkownicy mają uprawnienia do odczytu i zapisu w różnych węzłach Twoich danych. Więcej informacji znajdziesz w artykule Bezpieczeństwo danych.