В этом документе рассматриваются четыре метода записи данных в 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, это удалило бы и full_name
, и date_of_birth
из вашего hopperRef
.
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 имеет язык безопасности, который позволяет вам определять, какие пользователи имеют доступ на чтение и запись к различным узлам ваших данных. Вы можете прочитать об этом подробнее в разделе Secure Your Data .