В этом документе рассматриваются четыре метода записи данных в Firebase Realtime Database : установка, обновление, отправка и поддержка транзакций.
Способы сохранения данных
набор | Записать или заменить данные по указанному пути , например messages/users/<username> |
обновлять | Обновить некоторые ключи для определенного пути без замены всех данных |
толкать | Добавление в список данных в базе данных. Каждый раз, когда вы добавляете новый узел в список, ваша база данных генерирует уникальный ключ, например messages/users/<unique-user-id>/<username> |
сделка | Используйте транзакции при работе со сложными данными, которые могут быть повреждены одновременными обновлениями. |
Сохранение данных
Базовая операция записи в базу данных — это набор данных, который сохраняет новые данные по указанной ссылке базы данных, заменяя любые существующие данные по этому пути. Чтобы разобраться с набором данных, создадим простое приложение для ведения блога. Данные вашего приложения хранятся по этой ссылке базы данных:
Ява
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');
Питон
# 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()
чтобы сохранить объект пользователя в базе данных, содержащий имя пользователя, полное имя и дату рождения. В качестве set можно передать строку, число, логическое значение, null
, массив или любой JSON-объект. Передача null
удалит данные в указанном месте. В данном случае вы передадите объект:
Ява
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' } });
Питон
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». Вы также можете сохранить данные непосредственно в дочернем расположении:
Ява
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' });
Питон
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
без изменений.
Обновление сохраненных данных
Если вы хотите одновременно записать данные в несколько дочерних узлов базы данных, не перезаписывая другие дочерние узлы, вы можете использовать метод обновления, как показано ниже:
Ява
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' });
Питон
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, из вашего hopperRef
были бы удалены и full_name
, и date_of_birth
.
Firebase Realtime Database также поддерживает многопутевые обновления. Это означает, что теперь обновление может обновлять значения в нескольких местах базы данных одновременно — мощная функция, которая помогает денормализовать данные . Используя многопутевые обновления, вы можете добавлять псевдонимы одновременно и для Грейс, и для Алана:
Ява
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' });
Питон
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" } } }
Обратите внимание, что попытка обновить объекты путём записи объектов с указанием путей приведёт к иному результату. Давайте посмотрим, что произойдёт, если вместо этого попытаться обновить Грейс и Алана следующим образом:
Ява
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' } });
Питон
users_ref.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } })
Идти
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) }
Это приводит к разному поведению, а именно к перезаписи всего узла /users
:
{ "users": { "alanisawesome": { "nickname": "Alan The Machine" }, "gracehop": { "nickname": "Amazing Grace" } } }
Добавление обратного вызова завершения
В Node.js и Java Admin SDK, если вы хотите узнать, когда данные были зафиксированы, можно добавить обратный вызов завершения. Методы set и update в этих SDK принимают необязательный обратный вызов завершения, который вызывается после фиксации записи в базу данных. Если по какой-либо причине вызов оказался неудачным, обратному вызову передаётся объект ошибки, указывающий причину сбоя. В Python и Go Admin SDK все методы записи являются блокирующими. То есть, методы записи не возвращают управление, пока запись не будет зафиксирована в базе данных.
Ява
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.'); } });
Сохранение списков данных
При создании списков данных важно учитывать многопользовательский характер большинства приложений и соответствующим образом корректировать структуру списка. Продолжая приведённый выше пример, давайте добавим записи блога в ваше приложение. Первым побуждением может быть использование набора для хранения дочерних элементов с автоматически увеличивающимися целочисленными индексами, например:
// 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()
, которая генерирует уникальный ключ для каждого нового дочернего элемента . Используя уникальные ключи дочерних элементов, несколько клиентов могут добавлять дочерние элементы в одно и то же место одновременно, не беспокоясь о конфликтах записи.
Ява
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' });
Питон
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()
следующим образом:
Ява
// 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' });;
Питон
# 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()
вернёт ссылку на новый путь к данным, который можно использовать для получения ключа или сохранения данных в нём. Следующий код вернёт те же данные, что и в примере выше, но теперь у нас будет доступ к сгенерированному уникальному ключу:
Ява
// 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;
Питон
# 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 транзакционная операция блокируется и, следовательно, принимает только функцию обновления.
Функция обновления принимает текущее состояние данных в качестве аргумента и должна возвращать новое желаемое состояние, которое вы хотите записать. Например, если вы хотите увеличить количество положительных голосов для определённой записи в блоге, вы можете написать транзакцию следующего вида:
Ява
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; });
Питон
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 используется язык безопасности, позволяющий определить, какие пользователи имеют доступ на чтение и запись к различным узлам ваших данных. Подробнее об этом можно узнать в статье «Защита данных» .