Zapisuję dane

Ten dokument obejmuje 4 metody zapisywania danych w Bazie danych czasu rzeczywistego Firebase: ustawianie, aktualizowanie, przekazywanie i obsługa transakcji.

Sposoby oszczędzania danych

ustawianie Zapisz lub zastąp dane zdefiniowaną ścieżką, np. messages/users/<username>
zaktualizowanie Aktualizowanie niektórych kluczy dla zdefiniowanej ścieżki bez zastępowania wszystkich danych
push | wypychanie [in descriptive contexts] Dodaj do listy danych w bazie danych. Za każdym razem, gdy wypchniesz nowy węzeł na listę, baza danych wygeneruje unikalny klucz, taki jak 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 w bazie danych to zbiór, który zapisuje nowe dane we wskazanym odniesieniu do bazy danych, zastępując wszelkie istniejące dane w tej ścieżce. Aby zrozumieć zestaw, stworzymy prostą aplikację do blogowania. Dane Twojej aplikacji są przechowywane w tym odniesieniu 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 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 będzie miał unikalną nazwę użytkownika, dlatego lepiej użyć 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 za pomocą set() / setValue() zapisz w bazie danych obiekt użytkownika z nazwą użytkownika, jego imieniem i nazwiskiem oraz datą urodzenia. Możesz przekazać zestaw znaków, liczbę, wartość logiczną, null, tablicę lub dowolny obiekt JSON. Jeśli przekroczysz null, dane zostaną usunięte z określonej lokalizacji. W tym przypadku należy przekazać do niego 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 zapisany w bazie danych, właściwości obiektu są automatycznie mapowane na lokalizacje podrzędne bazy danych w sposób zagnieżdżony. Jeśli otworzysz adres 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 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 przypadku 2 powyższych przykładów – zapisanie obu wartości w tym samym czasie co obiektu i zapisanie ich oddzielnie w lokalizacjach podrzędnych – spowoduje to 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 aktywuje tylko 1 zdarzenie na klientach, którzy oglądają dane, a drugi – dwa. Należy zauważyć, że jeśli dane istniały już w usersRef, pierwsze podejście spowodowałoby ich zastąpienie, ale druga metoda jedynie zmodyfikuje wartość każdego oddzielnego węzła podrzędnego, pozostawiając bez zmian inne elementy podrzędne instancji 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, dodając jej pseudonim. Gdyby zamiast aktualizacji użyto ustawienia set tutaj, z urządzenia hopperRef zostałyby usunięte zarówno full_name, jak i date_of_birth.

Baza danych czasu rzeczywistego Firebase obsługuje też aktualizacje wielościeżkowe. Oznacza to, że aktualizacja może teraz aktualizować wartości w wielu lokalizacjach w bazie danych jednocześnie. Jest to zaawansowana funkcja, która ułatwia denormalizację danych. Dzięki aktualizacjom z użyciem wielu ścieżek możesz dodać pseudonimy zarówno Grace, jak i Alanowi naraz:

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ą swoje 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 zaktualizowania obiektów przez wpisanie obiektów z uwzględnionymi ścieżkami spowoduje inne zachowanie. Zobaczmy, co się stanie, jeśli zamiast tego 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)
}

Powoduje to inne zachowanie, czyli zastąpienie całego węzła /users:

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

Dodawanie zakończenia wywołania zwrotnego

W Node.js i Java Admin SDK, jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz dodać pełne wywołanie zwrotne. 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 nie uda się z jakiegoś powodu, wywołanie zwrotne zostanie przekazane obiektowi błędu z informacją o przyczynie tego błędu. W pakietach SDK Python i Go Admin wszystkie metody zapisu są blokowane. Oznacza to, że metody zapisu nie są zwracane, 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 w większości aplikacji jest wielu użytkowników, i odpowiednio dostosować strukturę list. 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. Takie rozwiązanie zadziałałoby, jeśli posty dodawała tylko jedna osoba, ale w Twojej aplikacji do wspólnego tworzenia blogów wielu użytkowników może dodawać posty jednocześnie. Jeśli 2 autorów jednocześnie napisze na adres /posts/2, 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 są automatycznie porządkowane chronologicznie. Firebase generuje unikalny klucz dla każdego posta na blogu, więc jeśli wielu użytkowników doda posta w tym samym czasie, nie wystąpią konflikty zapisu. Twoje dane 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 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)
}

Pobieram unikalny klucz wygenerowany przez funkcję push()

Wywołanie push() zwróci odwołanie do nowej ścieżki danych, której możesz użyć, aby uzyskać do niego klucz lub ustawić dla niego dane. 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 widać, wartość unikalnego klucza można pobrać z pliku referencyjnego push().

W następnej sekcji poświęconej pobieraniu danych dowiesz się, jak odczytywać te dane z bazy danych Firebase.

Zapisywanie danych transakcyjnych

W przypadku złożonych danych, które mogą zostać uszkodzone w wyniku równoczesnych modyfikacji, takich jak liczniki przyrostowe, 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 aktualizacji przyjmuje bieżący stan danych jako argument i powinna zwrócić nowy, pożądany stan, który chcesz zapisać. Aby na przykład zwiększyć liczbę głosów za na określonego posta na blogu, musisz napisać transakcję podobną do tej:

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

Ten przykład sprawdza, czy licznik wynosi null, czy nie został jeszcze zwiększony, ponieważ transakcje mogą być wywoływane za pomocą funkcji null, jeśli nie zapisano żadnej wartości domyślnej.

Gdyby powyższy kod został uruchomiony bez funkcji transakcji, a 2 klienty próbowały go zwiększyć jednocześnie, oba klienty zapisałyby jako nową wartość 1, co skutkowało przyrostem zamiast 2.

Połączenia sieciowe i zapisy offline

Klienty Firebase Node.js i Java obsługują własną wewnętrzną wersję wszystkich aktywnych danych. Podczas zapisywania danych są one najpierw zapisywane w tej wersji lokalnej. Następnie klient synchronizuje te dane z bazą danych i innymi klientami zgodnie z zasadą możliwie najlepszej obsługi.

W rezultacie wszystkie zapisy w bazie danych aktywują zdarzenia lokalne natychmiast, jeszcze zanim jakiekolwiek dane zostaną w niej zapisane. Oznacza to, że gdy piszesz aplikację za pomocą Firebase, jest ona responsywna bez względu na opóźnienia w sieci czy połączenie z internetem.

Gdy połączenie zostanie przywrócone, otrzymamy odpowiedni zestaw zdarzeń, dzięki którym klient „dostosuje się” do bieżącego stanu serwera bez konieczności pisania żadnego niestandardowego kodu.

Ochrona danych

Baza danych czasu rzeczywistego Firebase ma język zabezpieczeń umożliwiający określanie, którzy użytkownicy mają uprawnienia do odczytu i zapisu w różnych węzłach Twoich danych. Więcej informacji znajdziesz w artykule Zabezpieczanie danych.