Zapisywanie danych

Ten dokument opisuje 4 metody zapisywania danych w bazie danych Firebase Realtime Database: ustawianie, aktualizowanie, push i transakcje.

Sposoby zapisywania danych

ustawianie Zapisywanie lub zastępowanie danych w zdefiniowanej ścieżce, np. messages/users/<username>
update Aktualizowanie niektórych kluczy w zdefiniowanej ścieżce bez zastępowania wszystkich danych
push Dodawanie do listy danych w bazie danych. Za każdym razem, gdy dodajesz nowy węzeł do listy, baza danych generuje unikalny klucz, np. messages/users/<unique-user-id>/<username>
transakcja Używaj transakcji, gdy pracujesz ze złożonymi danymi, które mogą zostać uszkodzone przez jednoczesne aktualizacje.

Zapisywanie danych

Podstawową operacją zapisu w bazie danych jest ustawianie, które zapisuje nowe dane w określonym odwołaniu do bazy danych, zastępując wszystkie istniejące dane w tej ścieżce. Aby zrozumieć ustawianie, utworzymy prostą aplikację do blogowania. Dane aplikacji są przechowywane w tym odwołaniu do 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żytkownika. Każdego użytkownika będziemy przechowywać pod unikalną nazwą użytkownika, a także jego pełne imię i nazwisko oraz datę urodzenia. Ponieważ każdy użytkownik będzie miał unikalną nazwę użytkownika, warto użyć tutaj metody ustawiania zamiast metody push, ponieważ masz już klucz i nie musisz go tworzyć.

Najpierw utwórz odwołanie do bazy danych do danych użytkownika. Następnie użyj set() / setValue(), aby zapisać obiekt użytkownika w bazie danych z nazwą użytkownika, pełnym imieniem i nazwiskiem oraz datą urodzenia. Możesz przekazać ciąg znaków, liczbę, wartość logiczną, null, tablicę lub dowolny obiekt JSON. Przekazanie null spowoduje usunięcie danych w określonej lokalizacji. W tym przypadku przekażesz 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, właściwości obiektu są automatycznie mapowane na podrzędne lokalizacje bazy danych w sposób zagnieżdżony. Jeśli teraz przejdziesz do adresu URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, zobaczysz wartość „Alan Turing”. Możesz też zapisywać dane bezpośrednio w podrzędnej lokalizacji:

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

Dwa powyższe przykłady – zapisywanie obu wartości jednocześnie jako obiektu i zapisywanie ich oddzielnie w podrzędnych lokalizacjach – spowodują zapisanie tych samych danych w 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 jednego zdarzenia na klientach, którzy obserwują dane, a drugi spowoduje wywołanie dwóch. Ważne jest, aby pamiętać, że jeśli dane już istniały w usersRef, pierwsze podejście spowoduje ich zastąpienie, ale druga metoda zmodyfikuje tylko wartość każdego oddzielnego węzła podrzędnego, pozostawiając inne węzły podrzędne usersRef bez zmian.

Aktualizowanie zapisanych danych

Jeśli chcesz jednocześnie zapisywać dane w wielu węzłach podrzędnych w 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 uwzględnić jej pseudonim. Gdybyś użył tutaj metody ustawiania zamiast aktualizacji, spowodowałoby to usunięcie zarówno full_name, jak i date_of_birth z hopperRef.

Firebase Realtime Database obsługuje też aktualizacje wielościeżkowe. Oznacza to, że aktualizacja może teraz jednocześnie aktualizować wartości w wielu lokalizacjach w bazie danych. Jest to zaawansowana funkcja, która umożliwia denormalizację danych. Za pomocą aktualizacji wielościeżkowych możesz jednocześnie dodać pseudonimy do Grace i Alana:

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 zarówno Alan, jak i Grace mają dodane pseudonimy:

{
  "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 aktualizacji obiektów przez zapisywanie obiektów ze ścieżkami spowoduje inne zachowanie. Zobaczmy, co się stanie, jeśli spróbujesz zaktualizować Grace i Alana 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 zachowanie, a mianowicie zastąpienie całego węzła /users:

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

Dodawanie wywołania zwrotnego zakończenia

Jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz dodać wywołanie zwrotne zakończenia w pakietach SDK administratora w Node.js i Javie. Zarówno metody ustawiania, jak i aktualizacji w tych pakietach SDK przyjmują opcjonalne wywołanie zwrotne zakończenia, które jest wywoływane gdy zapis zostanie zatwierdzony w bazie danych. Jeśli wywołanie z jakiegoś powodu się nie powiedzie, do wywołania zwrotnego zostanie przekazany obiekt błędu wskazujący przyczynę niepowodzenia. W pakietach SDK administratora w Pythonie i Go wszystkie metody zapisu są blokujące. Oznacza to, że metody zapisu nie zwracają wartości dopóki zapisy nie zostaną zatwierdzone 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ć o tym, że większość aplikacji jest używana przez wielu użytkowników, i odpowiednio dostosować strukturę listy. Rozwijając powyższy przykład, dodajmy do aplikacji posty na blogu. Twoim pierwszym odruchem może być użycie metody ustawiania do przechowywania węzłów podrzędnych z automatycznie zwiększającymi się indeksami całkowitymi, takimi jak te:

// 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. Będzie to działać tylko wtedy, gdy posty dodaje tylko jeden autor , ale w aplikacji do blogowania, w której współpracuje wiele osób, wielu użytkowników może dodawać posty jednocześnie. Jeśli 2 autorów jednocześnie zapisze dane w /posts/2, 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 węzła podrzędnego. Dzięki użyciu unikalnych kluczy podrzędnych kilku klientów może jednocześnie dodawać węzły podrzędne do tej samej lokalizacji bez obaw 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. Ponieważ Firebase generuje unikalny klucz dla każdego posta na blogu, nie wystąpią żadne konflikty zapisu, jeśli kilku użytkowników doda post jednocześnie. 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 wzorzec wywoływania push() i natychmiastowego wywoływania set() jest tak powszechny, że pakiet SDK Firebase umożliwia ich łączenie przez przekazywanie danych do ustawienia bezpośrednio do 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 push() zwróci odwołanie do nowej ścieżki danych, za pomocą której możesz pobrać klucz lub ustawić dane. Poniższy kod spowoduje utworzenie tych samych danych co w powyższym przykładzie, ale teraz będziemy mieć 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 odwołania push().

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

Zapisywanie danych transakcyjnych

Podczas pracy ze złożonymi danymi, które mogą zostać uszkodzone przez jednoczesne modyfikacje, np. liczniki przyrostowe, pakiet SDK udostępnia operację transakcji.

W Javie i Node.js operacja transakcji przyjmuje 2 wywołania zwrotne: funkcję aktualizacji i opcjonalne wywołanie zwrotne zakończenia. W Pythonie i Go operacja transakcji jest blokująca, dlatego akceptuje tylko funkcję aktualizacji.

Funkcja aktualizacji przyjmuje jako argument bieżący stan danych i powinna zwracać nowy stan, który chcesz zapisać. Jeśli na przykład chcesz zwiększyć liczbę głosów na konkretny post na blogu, napisz transakcję w ten sposób:

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

Powyższy przykład sprawdza, czy licznik ma wartość null lub nie został jeszcze zwiększony, ponieważ transakcje można wywoływać z wartością null, jeśli nie zapisano wartości domyślnej.

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

Połączenia sieciowe i zapisywanie offline

Klienci Firebase w Node.js i Javie utrzymują własną wewnętrzną wersję aktywnych danych. Gdy dane są zapisywane, są zapisywane w tej lokalnej wersji. Następnie klient synchronizuje te dane z bazą danych i innymi klientami na zasadzie „najlepszych starań”.

W rezultacie wszystkie zapisy w bazie danych spowodują natychmiastowe wywołanie zdarzeń lokalnych, zanim jakiekolwiek dane zostaną zapisane w bazie danych. Oznacza to, że gdy piszesz aplikację za pomocą Firebase, Twoja aplikacja będzie reagować niezależnie od opóźnienia sieci i połączenia z internetem.

Po przywróceniu połączenia otrzymamy odpowiedni zestaw zdarzeń, dzięki czemu klient „nadrobi” bieżący stan serwera bez konieczności pisania niestandardowego kodu.

Zabezpieczanie danych

Firebase Realtime Database ma język zabezpieczeń, który umożliwia określenie, którzy użytkownicy mają dostęp do odczytu i zapisu w różnych węzłach danych. Więcej informacji znajdziesz w artykule Zabezpieczanie danych.