Zapisywanie danych

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>
update 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ć zbiór, zbudujemy prosty 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 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ę, więc lepiej użyć metody set tutaj zamiast metody push, bo 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 element podrzędny bazy danych lokalizacji w sposób zagnieżdżony. Jeśli otworzysz adres URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/pełna_nazwa, zobaczymy 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 wywoła tylko 1 zdarzenie u klientów, którzy oglądają dane, a drugi wywoła dwa. Co ważne, jeśli dane już istniały w usersRef, pierwszym podejściem spowoduje jego zastąpienie, ale druga metoda tylko zmodyfikuje wartość każdego oddzielnego węzła podrzędnego, pozostawiając nie zmieniono innych elementów podrzędnych elementu usersRef.

Aktualizowanie zapisanych danych

Jeśli chcesz jednocześnie zapisywać do wielu elementów podrzędnych lokalizacji bazy danych bez zastępowania innych węzłów podrzędnych, możesz użyć metody aktualizacji 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. Jeśli zamiast aktualizacji była używana opcja Ustaw, spowoduje to usunięcie zarówno full_name, jak i date_of_birth z urządzenia hopperRef.

Baza danych czasu rzeczywistego Firebase obsługuje też aktualizacje wielościeżkowe. Oznacza to, że aktualizacja może teraz być aktualizowana wartości w wielu lokalizacjach w bazie danych jednocześnie. Jest to zaawansowana funkcja, która pozwala pomaga deznormalizować danych. Dzięki aktualizacjom wielościeżkowym możesz dodać pseudonimy zarówno Grace, jak i Alanowi w tym samym czasie. godzina:

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 pakietach Node.js i Java Admin SDK Jeśli chcesz się dowiedzieć, kiedy dane zostały zatwierdzone, możesz dodać wywołanie zwrotne o fakcie zakończenia. Zarówno metody ustawiania, jak i aktualizacji w tych pakietach SDK wykonują opcjonalne wywołanie zwrotne polegające na zakończeniu po zatwierdzeniu zapisu w bazie danych. Jeśli połączenie nie powiodło się w przypadku niektórych w odpowiedzi na wywołanie zwrotne przekazywane jest obiekt błędu wskazujący przyczynę błędu. W pakietach SDK Python i Go Admin wszystkie metody zapisu są blokowane. Oznacza to, że metody zapisu nie zwracają 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ć, że większość aplikacji jest wieloosobowych odpowiednio dostosować strukturę listy. Kontynuując przykład powyżej, dodajmy posty na blogu do Twojej aplikacji. Twoje pierwszym instynktem może być użycie ustawienia do przechowywania elementów podrzędnych z automatycznie przyrostowymi indeksami całkowitymi, np. następujące:

// 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 działać, gdy tylko jeden autor ale w aplikacji do wspólnego tworzenia blogów wielu użytkowników może dodawać posty w tym samym czasie. Jeśli dwóch autorów jednocześnie napisze na adres /posts/2, wówczas 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 konta podrzędnego. Dzięki użyciu unikalnych kluczy podrzędnych kilka klientów może dodawanie dzieci do tej samej lokalizacji w tym samym czasie, 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 są automatycznie porządkowane chronologicznie. Firebase generuje unikalny klucz dla każdego posta na blogu, więc w przypadku wielu użytkowników nie wystąpią konflikty zapisu dodaj wpis. 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łania push(), a następnie natychmiastowego wywołania set() to tak powszechne, że pakiet SDK Firebase umożliwia ich połączenie przez przekazywanie danych ustawionych bezpośrednio jako push() w następujący 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

Podczas pracy ze złożonymi danymi, które mogą ulec uszkodzeniu w wyniku równoczesnych modyfikacji, takich jak liczniki przyrostowe, pakiet SDK udostępnia operację transakcji.

W Javie i Node.js dajesz operacji transakcji dwa wywołania zwrotne: funkcję aktualizacji i opcjonalne wywołanie zwrotne polegające na zakończeniu. W Pythonie i Go operacja transakcji jest blokowana i w związku z tym akceptuje tylko funkcję aktualizacji.

Funkcja aktualizacji przyjmuje bieżący stan danych jako argument, a funkcja powinien zwrócić nowy pożądany stan, który chcesz zapisać. Jeśli na przykład chcesz zwiększyć liczbę głosów za na konkretnego posta na blogu, transakcja będzie podobna 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)
}

W przykładzie powyżej można sprawdzić, czy licznik wynosi null, czy nie został jeszcze zwiększony, ponieważ transakcje mogą być wywoływane za pomocą funkcji null, jeśli nie została zapisana żadna wartość domyślna.

Jeśli powyższy kod został uruchomiony bez funkcji transakcji, a dwa klienty próbowały go zwiększyć jednocześnie zapisywałyby 1 jako nową wartość, co skutkowało zwiększeniem wartości o 1, a nie o 2.

Połączenia sieciowe i zapisy offline

Klienty Firebase Node.js i Java obsługują własną wewnętrzną wersję wszystkich aktywnych danych. Przy zapisywaniu danych zapisane najpierw w tej wersji lokalnej. Następnie klient synchronizuje te dane z bazą danych. oraz z innymi klientami, robiąc, co najlepsze, podstaw.

W rezultacie wszystkie zapisy w bazie danych aktywują zdarzenia lokalne natychmiast, jeszcze zanim dane zostaną zapisane w w bazie danych. Oznacza to, że gdy piszesz aplikację za pomocą Firebase, aplikacja nie ma znaczenia, czy występują opóźnienia w sieci czy połączenie z internetem.

Gdy połączenie zostanie przywrócone, otrzymasz odpowiedni zestaw zdarzeń, aby klient „nadrobił zaległości”. z bieżącym stanem serwera, bez konieczności pisania niestandardowego kodu.

Ochrona danych

Baza danych czasu rzeczywistego Firebase korzysta z języka zabezpieczeń, który pozwala określać, którzy użytkownicy mają uprawnienia do odczytu i zapisu w różnych węzłach danych. Więcej informacji znajdziesz w artykule Zabezpieczanie danych.