Zapisywanie danych

W tym dokumencie opisano cztery metody zapisywania danych w bazie danych Firebase Realtime Database: ustawianie, aktualizacja, wypychanie i obsługa transakcji.

Sposoby zapisywania danych

ustawić Zapisz lub zamień dane w określonej ścieżce , np. messages/users/<username>
aktualizacja Zaktualizuj niektóre klucze dla określonej ścieżki bez zastępowania wszystkich danych
naciskać Dodaj do listy danych w bazie danych. Za każdym razem, gdy umieszczasz nowy węzeł na liście, Twoja baza danych generuje 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 równoczesne aktualizacje

Zapisywanie danych

Podstawowa operacja zapisu do bazy danych to zbiór, który zapisuje nowe dane do określonego odniesienia do bazy danych, zastępując wszelkie istniejące dane w tej ścieżce. Aby zrozumieć zestaw, zbudujemy prostą aplikację do blogowania. Dane Twojej aplikacji są przechowywane w tym odnośniku do bazy danych:

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

# Get a database reference to our blog.
ref = db.reference('server/saving-data/fireblog')
Iść
// 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 niektórych danych użytkownika. Będziemy przechowywać każdego użytkownika pod unikalną nazwą użytkownika, a także będziemy przechowywać jego pełne imię i nazwisko oraz datę urodzenia. Ponieważ każdy użytkownik będzie miał unikalną nazwę użytkownika, sensowne jest użycie tutaj metody set 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 zawierający nazwę użytkownika, jego imię i nazwisko oraz datę urodzenia. Możesz przekazać set string, number, boolean, null , array lub dowolny obiekt JSON. Przekazanie wartości null spowoduje usunięcie danych z określonej lokalizacji. W tym przypadku przekażesz mu obiekt:

Jawa
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'
  }
});
Pyton
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'
    }
})
Iść

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

Kiedy obiekt JSON jest zapisywany w bazie danych, właściwości obiektu są automatycznie mapowane na lokalizacje podrzędne bazy danych w sposób zagnieżdżony. Teraz, jeśli przejdziesz do adresu URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name , zobaczymy wartość „Alan Turing”. Możesz także zapisać dane bezpośrednio w lokalizacji podrzędnej:

Jawa
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'
});
Pyton
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'
})
Iść
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)
}

Powyższe dwa przykłady - jednoczesne zapisanie obu wartości jako obiektu i zapisanie ich oddzielnie w lokalizacjach podrzędnych - spowodują 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 wyzwoli tylko jedno zdarzenie na klientach oglądających dane, podczas gdy drugi przykład wyzwoli dwa. Należy zauważyć, że jeśli dane już istniały w usersRef , pierwsze podejście spowodowałoby ich nadpisanie, ale druga metoda zmodyfikowałaby jedynie wartość każdego osobnego węzła podrzędnego, pozostawiając inne elementy podrzędne usersRef bez zmian.

Aktualizowanie zapisanych danych

Jeśli chcesz jednocześnie zapisywać dane w wielu elementach podrzędnych lokalizacji bazy danych bez nadpisywania innych węzłów podrzędnych, możesz użyć metody aktualizacji pokazanej poniżej:

Jawa
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'
});
Pyton
hopper_ref = users_ref.child('gracehop')
hopper_ref.update({
    'nickname': 'Amazing Grace'
})
Iść
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 i uwzględnienie jej pseudonimu. Jeśli użyłbyś tutaj set zamiast update, usunęłoby to zarówno full_name , jak i date_of_birth z twojego hopperRef .

Baza danych Firebase Realtime Database obsługuje także aktualizacje wielościeżkowe. Oznacza to, że aktualizacja może teraz aktualizować wartości w wielu lokalizacjach w bazie danych w tym samym czasie, co jest potężną funkcją, która pozwala pomóc w denormalizacji danych . Korzystając z aktualizacji wielościeżkowych, możesz jednocześnie dodawać pseudonimy Grace i Alanowi:

Jawa
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'
});
Pyton
users_ref.update({
    'alanisawesome/nickname': 'Alan The Machine',
    'gracehop/nickname': 'Amazing Grace'
})
Iść
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 pseudonimy zarówno Alanowi, jak 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"
    }
  }
}

Należy pamiętać, że próba aktualizacji obiektów poprzez zapisanie obiektów z dołączonymi ścieżkami spowoduje inne zachowanie. Przyjrzyjmy się, co się stanie, jeśli zamiast tego spróbujesz zaktualizować Grace i Alana w ten sposób:

Jawa
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'
  }
});
Pyton
users_ref.update({
    'alanisawesome': {
        'nickname': 'Alan The Machine'
    },
    'gracehop': {
        'nickname': 'Amazing Grace'
    }
})
Iść
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)
}

Powoduje to inne zachowanie, a mianowicie nadpisanie całego węzła /users :

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

Dodanie wywołania zwrotnego zakończenia

Jeśli chcesz wiedzieć, kiedy Twoje dane zostały zatwierdzone, w pakietach Node.js i Java Admin SDK możesz dodać wywołanie zwrotne zakończenia. Zarówno metody set, jak i update w tych zestawach SDK przyjmują opcjonalne wywołanie zwrotne zakończenia, które jest wywoływane, gdy zapis został zatwierdzony w bazie danych. Jeżeli z jakiegoś powodu wywołanie nie powiodło się, do wywołania zwrotnego przekazywany jest 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ą, dopóki zapisy nie zostaną zapisane w bazie danych.

Jawa
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

Tworząc listy danych, należy pamiętać o wielodostępnym charakterze większości aplikacji i odpowiednio dostosować strukturę listy. Rozwijając powyższy przykład, dodajmy posty na blogu do Twojej aplikacji. Twoim pierwszym odruchem może być użycie set do przechowywania dzieci z automatycznie zwiększającymi się indeksami całkowitymi, jak poniżej:

// 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 . Działałoby to, gdyby posty dodawał tylko jeden autor, ale w aplikacji do wspólnego blogowania wielu użytkowników może dodawać posty w tym samym czasie. Jeśli dwóch autorów napisze jednocześnie do /posts/2 , wówczas 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 dziecka . Używając unikalnych kluczy podrzędnych, kilku klientów może jednocześnie dodawać dzieci do tej samej lokalizacji, nie martwiąc się o konflikty zapisu.

Jawa
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'
});
Pyton
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'
})
Iść

// 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 opiera się na znaczniku czasu, więc elementy listy zostaną automatycznie uporządkowane chronologicznie. Ponieważ Firebase generuje unikalny klucz dla każdego wpisu na blogu, nie wystąpią żadne konflikty podczas zapisu, jeśli wielu użytkowników doda post w tym samym czasie. Dane Twojej bazy 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 metody push() , a następnie natychmiastowego wywoływania set() jest tak powszechny, że pakiet SDK Firebase pozwala je łączyć, przekazując dane, które mają zostać ustawione bezpośrednio do funkcji push() w następujący sposób:

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

Uzyskanie unikalnego klucza wygenerowanego przez push()

Wywołanie metody push() zwróci odwołanie do nowej ścieżki danych, której możesz użyć, aby uzyskać klucz lub ustawić dla niego dane. Poniższy kod da takie same dane jak w powyższym przykładzie, ale teraz będziemy mieli dostęp do wygenerowanego unikalnego klucza:

Jawa
// 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;
Pyton
# 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
Iść
// 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 widać, wartość unikalnego klucza można uzyskać z referencji push() .

W następnej sekcji pobierania danych dowiemy się, jak odczytać te dane z bazy danych Firebase.

Zapisywanie danych transakcyjnych

Podczas pracy ze złożonymi danymi, które mogą zostać uszkodzone przez jednoczesne modyfikacje, takie jak liczniki przyrostowe, zestaw SDK zapewnia operację transakcyjną .

W Javie i Node.js operacji transakcji udostępniasz dwa wywołania zwrotne: funkcję aktualizacji i opcjonalne wywołanie zwrotne zakończenia. W Pythonie i Go operacja transakcji jest blokowana i dlatego akceptuje jedynie funkcję aktualizacji.

Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i powinna zwrócić nowy żądany stan, który chcesz zapisać. Na przykład, jeśli chcesz zwiększyć liczbę głosów pozytywnych dla konkretnego wpisu na blogu, napisz transakcję w następujący sposób:

Jawa
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;
});
Pyton
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')
Iść
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 i czy nie został jeszcze zwiększony, ponieważ transakcje mogą być wywoływane z null , jeśli nie zapisano wartości domyślnej.

Jeżeli powyższy kod zostałby uruchomiony bez funkcji transakcyjnej i dwóch klientów próbowałoby go zwiększyć jednocześnie, obaj zapisaliby 1 jako nową wartość, co dałoby jeden przyrost zamiast dwóch.

Łączność sieciowa i zapisy offline

Klienci Firebase Node.js i Java utrzymują własną wewnętrzną wersję wszelkich aktywnych danych. Kiedy dane są zapisywane, są najpierw zapisywane w tej wersji lokalnej. Następnie klient synchronizuje te dane z bazą danych i innymi klientami, dokładając wszelkich starań.

W rezultacie wszystkie zapisy do bazy danych będą natychmiast wyzwalać zdarzenia lokalne, zanim jakiekolwiek dane zostaną zapisane w bazie danych. Oznacza to, że gdy napiszesz aplikację przy użyciu Firebase, Twoja aplikacja pozostanie responsywna niezależnie od opóźnienia sieci lub łączności z Internetem.

Po przywróceniu łączności otrzymamy odpowiedni zestaw zdarzeń, dzięki czemu klient „dogoni” aktualny stan serwera, bez konieczności pisania żadnego niestandardowego kodu.

Zabezpieczanie Twoich danych

Baza danych Firebase Realtime Database ma język zabezpieczeń, który pozwala określić, którzy użytkownicy mają dostęp do odczytu i zapisu w różnych węzłach danych. Więcej na ten temat przeczytasz w artykule Zabezpiecz swoje dane .