В этом документе рассматриваются четыре метода записи данных в Firebase Realtime Database: set, update, push и поддержка транзакций.
Как экономить трафик
| set | Запись или замена данных по определенному пути, например messages/users/<username>. |
| update | Обновить некоторые ключи для определенного пути, не заменяя все данные. |
| push | Добавить в список данных в базе данных. Каждый раз, когда вы добавляете новый узел в список, база данных генерирует уникальный ключ, например messages/users/<unique-user-id>/<username>. |
| транзакция | Используйте транзакции при работе со сложными данными, которые могут быть повреждены одновременными обновлениями. |
Сохранение данных
Базовая операция записи в базу данных – это набор, который сохраняет новые данные в указанной ссылке на базу данных, заменяя любые существующие данные по этому пути. Чтобы понять, как работает метод set, мы создадим простое приложение для ведения блога. Данные для приложения хранятся по следующей ссылке на базу данных:
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')
Проложить маршрут
// 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")
Для начала сохраним некоторые пользовательские данные. Мы будем хранить данные каждого пользователя по уникальному имени пользователя, а также его полное имя и дату рождения. Поскольку у каждого пользователя будет уникальное имя, здесь имеет смысл использовать метод set вместо метода push, так как у вас уже есть ключ и вам не нужно его создавать.
Сначала создайте ссылку на базу данных с пользовательскими данными. Затем используйте set() / setValue(), чтобы сохранить объект пользователя в базе данных с именем пользователя, полным именем и датой рождения. Вы можете передать строку, число, логическое значение, null, массив или любой объект JSON. Если передать null, данные в указанном местоположении будут удалены. В этом случае вы передадите ему объект:
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&qu<ot;); MapSt>ring, User users = n<>ew 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' } })
Проложить маршрут
// 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) }
При сохранении объекта JSON в базе данных его свойства автоматически сопоставляются с дочерними местоположениями в базе данных в виде вложенной структуры. Теперь, если вы перейдете по URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, вы увидите значение "Alan Turing". Вы также можете сохранять данные непосредственно в дочернем местоположении:
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' })
Проложить маршрут
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) }
Оба приведенных выше примера (запись обоих значений одновременно в виде объекта и их раздельная запись в дочерние местоположения) приведут к сохранению одних и тех же данных в вашей базе данных:
{
"users": {
"alanisawesome": {
"date_of_birth": "June 23, 1912",
"full_name": "Alan Turing"
},
"gracehop": {
"date_of_birth": "December 9, 1906",
"full_name": "Grace Hopper"
}
}
}
В первом примере будет активировано только одно событие для клиентов, которые отслеживают данные, а во втором – два. Важно отметить, что если данные уже существовали в usersRef, первый подход перезапишет их, а второй метод изменит только значение каждого отдельного дочернего узла, оставив другие дочерние узлы usersRef без изменений.
Обновление сохраненных данных
Если вы хотите одновременно записать данные в несколько дочерних узлов базы данных, не перезаписывая другие дочерние узлы, используйте метод update, как показано ниже.
Java
DatabaseReference hopperRef = usersRef.child("gracehop&q<uot;); MapStri>ng, Object hopperUpdates = n<>ew 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' })
Проложить маршрут
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
В результате в данные Грейс будет добавлен ее псевдоним. Если бы вы использовали здесь set вместо update, то удалили бы и full_name, и date_of_birth из hopperRef.
Firebase Realtime Database также поддерживает многопутевые обновления. Это означает, что метод update теперь может одновременно обновлять значения в нескольких местах базы данных. Эта мощная функция позволяет денормализовать данные. Используя многопутевые обновления, вы можете одновременно добавить псевдонимы как Грейс, так и Алану:
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' })
Проложить маршрут
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) }
После этого обновления у Алана и Грейс появились псевдонимы:
{
"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"
}
}
}Обратите внимание, что попытка обновить объекты путем записи объектов с включенными путями приведет к другому поведению. Давайте посмотрим, что произойдет, если вы попытаетесь обновить данные Грейс и Алана следующим образом:
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' } })
Проложить маршрут
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisaweso&me": User{Nickname: "Alan The Machine"}&, "gracehop": User{Nickname: "Amazing Grace"}, }); err != nil { log.Fatalln("Error updating children:", err) }
Это приводит к другому поведению, а именно к перезаписи всего узла /users:
{
"users": {
"alanisawesome": {
"nickname": "Alan The Machine"
},
"gracehop": {
"nickname": "Amazing Grace"
}
}
}Добавление обратного вызова по завершении
В Node.js и Java Admin SDK вы можете добавить обратный вызов завершения, чтобы узнать, когда данные были зафиксированы. Оба метода set и update в этих SDK принимают необязательный обратный вызов завершения, который вызывается, когда запись была зафиксирована в базе данных. Если вызов по какой-либо причине не удался, обратному вызову передается объект ошибки с указанием причины сбоя. В Admin SDK для Python и Go все методы записи являются блокирующими. Это означает, что методы записи не возвращают результат, пока запись не будет зафиксирована в базе данных.
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.'); } });
Сохранение списков данных
При создании списков данных важно помнить о многопользовательском характере большинства приложений и соответствующим образом корректировать структуру списка. Расширяя приведенный выше пример, давайте добавим в ваше приложение сообщения блога. Вашим первым инстинктом может быть использование set для хранения дочерних элементов с автоматически увеличивающимися целочисленными индексами, например:
// NOT RECOMMENDED - use push() instead! { "posts": { "0": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "1": { "author": "alanisawesome", "title": "The Turing Machine" } } }
Если пользователь добавляет новую запись, она сохраняется как /posts/2. Это сработало бы, если бы записи добавлял только один автор, но в вашем приложении для совместного ведения блогов многие пользователи могут добавлять записи одновременно. Если два автора одновременно пишут в /posts/2, то одна из записей будет удалена другой.
Чтобы решить эту проблему, клиенты Firebase предоставляют функцию push(), которая генерирует уникальный ключ для каждого нового дочернего элемента. Используя уникальные дочерние ключи, несколько клиентов могут одновременно добавлять дочерние элементы в одно и то же место, не беспокоясь о конфликтах записи.
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' })
Проложить маршрут
// 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) }
Уникальный ключ основан на временной метке, поэтому элементы списка будут автоматически упорядочены в хронологическом порядке. Поскольку Firebase генерирует уникальный ключ для каждого сообщения в блоге, конфликтов записи не возникнет, если несколько пользователей добавят сообщение одновременно. Теперь данные в базе выглядят так:
{
"posts": {
"-JRHTHaIs-jNPLXOQivY": {
"author": "gracehop",
"title": "Announcing COBOL, a New Programming Language"
},
"-JRHTHaKuITFIhnj02kE": {
"author": "alanisawesome",
"title": "The Turing Machine"
}
}
}
В JavaScript, Python и Go вызов push() и последующий вызов set() настолько распространены, что Firebase SDK позволяет объединить их, передав данные, которые нужно задать, непосредственно в push(), как показано ниже.
Java
// No Java equivalentNode.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' })
Проложить маршрут
if _, err := postsRef.Push(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error pushing child node:", err) }
Получение уникального ключа, сгенерированного методом push()
Вызов push() возвращает ссылку на новый путь к данным, которую можно использовать для получения ключа или установки данных. Следующий код приведет к тому же результату, что и в примере выше, но теперь у нас будет доступ к сгенерированному уникальному ключу:
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
Проложить маршрут
// 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
Как видите, значение уникального ключа можно получить из ссылки push().
В следующем разделе, Получение данных, мы расскажем, как считывать эти данные из базы данных Firebase.
Сохранение данных о транзакциях
При работе со сложными данными, которые могут быть повреждены одновременными изменениями, например с инкрементными счетчиками, SDK предоставляет операцию транзакции.
В Java и Node.js вы передаете операции транзакции два обратных вызова: функцию обновления и необязательный обратный вызов завершения. В Python и Go операция транзакции блокируется и поэтому принимает только функцию обновления.
Функция обновления принимает текущее состояние данных в качестве аргумента и должна возвращать новое желаемое состояние, которое вы хотите записать. Например, если вы хотите увеличить количество голосов за определенный пост в блоге, вы можете написать транзакцию, подобную следующей:
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')
Проложить маршрут
fn := func(t db.TransactionNode) (interface{}, error) { var currentValue int if err := t.Unmarshal(¤tValue); 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) }
В примере выше проверяется, равен ли счетчик null или он ещё не увеличивался, поскольку транзакции могут вызываться с null, если не было записано значение по умолчанию.
Если бы приведенный выше код был запущен без функции транзакции и два клиента попытались бы увеличить его одновременно, они оба записали бы 1 в качестве нового значения, что привело бы к одному увеличению вместо двух.
Подключение к сети и запись в офлайн-режиме
Клиенты Firebase Node.js и Java поддерживают собственную внутреннюю версию всех активных данных. При записи данных они сначала сохраняются в локальной версии. Затем клиент синхронизирует эти данные с базой данных и другими клиентами по мере возможности.
В результате все записи в базу данных будут немедленно вызывать локальные события, даже до того, как какие-либо данные будут записаны в базу данных. Это означает, что при написании приложения с помощью Firebase оно будет оставаться отзывчивым независимо от задержки сети или подключения к Интернету.
После восстановления подключения мы получим соответствующий набор событий, чтобы клиент "догнал" текущее состояние сервера без необходимости писать какой-либо пользовательский код.
Защита данных
В Firebase Realtime Database есть язык безопасности, который позволяет определить, у каких пользователей есть доступ на чтение и запись к разным узлам ваших данных. Подробнее о защите данных…