Zapisuję dane

W tym dokumencie opisujemy 4 metody zapisywania danych w Bazie danych czasu rzeczywistego Firebase: ustawianie, aktualizowanie, przekazywanie i obsługa transakcji.

Sposoby oszczędzania danych

ustawianie Wpisz lub zastąp dane zdefiniowaną ścieżką, np. messages/users/<username>
zaktualizowanie Aktualizacja niektórych kluczy 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 przenosisz nowy węzeł na listę, baza danych generuje unikalny klucz, taki jak messages/users/<unique-user-id>/<username>
transakcja Korzystanie z transakcji podczas pracy ze złożonymi danymi, które mogą zostać uszkodzone przez równoczesne aktualizacje

Zapisuję dane

Podstawowa operacja zapisu w bazie danych to zbiór, który zapisuje nowe dane w określonym odniesieniu do bazy danych, zastępując wszystkie dane istniejące w tej ścieżce. Aby zrozumieć zbiór, utworzymy prostą aplikację do blogowania. Dane 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 kilku danych użytkownika. Przechowujemy dane każdego użytkownika z unikalną nazwą użytkownika, a także z jego imieniem i nazwiskiem oraz datą urodzenia. Każdy użytkownik ma unikalną nazwę użytkownika, dlatego warto użyć tutaj metody zamiast metody push, ponieważ już masz klucz i nie musisz go tworzyć.

Najpierw utwórz odniesienie do danych użytkownika w bazie danych. Następnie użyj set() / setValue(), aby zapisać w bazie danych obiekt użytkownika z nazwą użytkownika, jego imieniem i nazwiskiem oraz datą urodzenia. Możesz przekazać ciąg znaków, liczbę, wartość logiczną, null, tablicę lub dowolny obiekt JSON. Przekazanie parametru null spowoduje usunięcie danych w określonej lokalizacji. W tym przypadku przekazujesz 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, jego właściwości są automatycznie mapowane na lokalizacje podrzędne bazy danych w sposób zagnieżdżony. 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ż 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)
}

Powyższe 2 przykłady – zapisanie obu wartości w tym samym czasie jako obiektu i osobne zapisywanie ich w lokalizacjach podrzędnych – spowoduje 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 wywoła tylko 1 zdarzenie w przypadku klientów, którzy oglądają dane, a drugi – 2. Warto zauważyć, że jeśli dane istnieją już w lokalizacji usersRef, pierwsza metoda spowoduje ich zastąpienie, a druga tylko zmieni wartość każdego osobnego węzła podrzędnego, pozostawiając bez zmian pozostałe elementy podrzędne obiektu usersRef.

Aktualizowanie zapisanych danych

Jeśli chcesz zapisać dane w wielu węzłach podrzędnych lokalizacji bazy danych jednocześnie bez zastępowania innych węzłów podrzędnych, możesz użyć metody aktualizacji w sposób przedstawiony 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 o jej pseudonim. Gdyby zamiast aktualizacji użyć tej funkcji, zostałaby usunięta zarówno full_name, jak i date_of_birth z hopperRef.

Baza danych czasu rzeczywistego Firebase obsługuje również 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 umożliwia denormalizację danych. Dzięki aktualizacji z wieloma ścieżkami możesz dodać pseudonimy zarówno Grace, jak i Andrzeju jednocześnie:

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 otrzymali 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 zapisanie obiektów z uwzględnionymi ścieżkami może spowodować inne działanie. Zobaczmy, co się stanie, jeśli spróbujesz zaktualizować aplikację Grace i Alan 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 odmienne zachowanie, a mianowicie powoduje zastąpienie całego węzła /users:

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

Dodawanie wywołania zwrotnego w celu ukończenia

Jeśli chcesz wiedzieć, kiedy Twoje dane zostały zatwierdzone, możesz dodać wywołanie zwrotne o zakończeniu działania w pakietach Node.js i Java Admin SDK. Zarówno metody ustawiania i aktualizacji w tych pakietach SDK odbierają opcjonalne wywołanie zwrotne zakończenia, które jest wywoływane po zatwierdzeniu zapisu w bazie danych. Jeśli z jakiegoś powodu wywołanie się nie udało, wywołanie zwrotne zostanie przekazane obiektowi błędu z informacją o przyczynie niepowodzenia. W pakietach Admin SDK Python i Go 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

Tworząc listy danych, pamiętaj o tym, że większość aplikacji ma wielu użytkowników i odpowiednio dostosuj strukturę listy. Nawiązując do powyższego przykładu, dodajmy do aplikacji posty na blogu. Być może najpierw wybierzesz opcję przechowywania elementów podrzędnych z automatycznie rosnącymi indeksami liczb całkowitych, 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. Byłoby to możliwe, gdyby posty dodawał tylko 1 autor, ale w aplikacji do wspólnego blogowania wielu użytkowników może dodawać posty w tym samym czasie. Jeśli 2 autorów napisze jednocześnie w usłudze /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 jednocześnie 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 bazuje na sygnaturze czasowej, więc elementy listy są automatycznie uporzą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. 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 przypadku JavaScriptu, Pythona i Go wzorzec wywoływania push(), a następnie natychmiastowego wywoływania funkcji set() jest tak powszechny, że pakiet SDK Firebase umożliwia ich połączenie przez przekazanie danych ustawianych 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)
}

Uzyskiwanie unikalnego klucza wygenerowanego przez push()

Wywołanie push() zwróci odwołanie do nowej ścieżki danych, za pomocą którego możesz uzyskać klucz lub ustawić dla niego dane. Ten kod zwróci te same dane, co powyższy przykład, ale teraz mamy 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żesz uzyskać z pliku referencyjnego push().

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

Zapisywanie danych transakcyjnych

W przypadku pracy ze złożonymi danymi, które mogą zostać uszkodzone przez równoczesne modyfikacje (np. liczniki przyrostowe), pakiet SDK udostępnia operację transakcji.

W Javie i Node.js operacji transakcji przypisujesz 2 wywołania zwrotne: funkcji update i opcjonalnego wywołania zwrotnego ukończenia. W Pythonie i Go operacja transakcji blokuje operację i dlatego akceptuje tylko funkcję update.

Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i powinna zwracać nowy stan, który chcesz zapisać. Jeśli na przykład chcesz zwiększyć liczbę głosów za konkretnym postem na blogu, możesz utworzyć 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)
}

Powyższy przykład pozwala sprawdzić, czy licznik ma wartość null i czy jeszcze nie został zwiększony, ponieważ transakcje można wywoływać za pomocą funkcji null, jeśli nie została zapisana żadna wartość domyślna.

Gdyby powyższy kod został uruchomiony bez funkcji transakcji, a 2 klienty próbowały jednocześnie zwiększyć tę wartość, obydwie zapisaliby 1 jako nową wartość, co dałoby jeden przyrost zamiast 2.

Połączenia sieciowe i zapisy w trybie offline

Klienty Firebase Node.js i Java mają własną wewnętrzną wersję wszystkich aktywnych danych. Kiedy dane są zapisywane, 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 związku z tym wszystkie zapisy w bazie danych będą aktywować zdarzenia lokalne natychmiast, zanim jakiekolwiek dane zostaną w niej zapisane. Oznacza to, że gdy piszesz aplikację za pomocą Firebase, będzie ona reagować niezależnie od opóźnień sieci czy połączenia z internetem.

Po przywróceniu połączenia otrzymasz odpowiedni zestaw zdarzeń, aby klient „dostosował się” do bieżącego stanu serwera bez konieczności pisania niestandardowego kodu.

Ochrona danych

Baza danych czasu rzeczywistego Firebase ma język zabezpieczeń, który pozwala definiować, którzy użytkownicy mają uprawnienia do odczytu i zapisu w różnych węzłach Twoich danych. Więcej informacji na ten temat znajdziesz w artykule Zabezpieczanie danych.