В этом документе рассматриваются четыре метода записи данных в базу данных Firebase Realtime: установка, обновление, отправка и поддержка транзакций.
Способы сохранения данных
поставил | Запишите или замените данные по определенному пути , например 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 , мы увидим значение «Алан Тьюринг». Вы также можете сохранить данные непосредственно в дочернем расположении:
Джава
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 также поддерживает многоканальные обновления. Это означает, что update теперь может обновлять значения в нескольких местах в вашей базе данных одновременно, мощная функция, которая позволяет вам денормализировать ваши данные . Используя обновления с несколькими путями, вы можете одновременно добавлять псевдонимы и Грейс, и Алану:
Джава
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" } } }
Обратите внимание, что попытка обновить объекты путем записи объектов с включенными путями приведет к другому поведению. Давайте посмотрим, что произойдет, если вместо этого вы попытаетесь обновить Grace и Alan следующим образом:
Джава
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.'); } });
Сохранение списков данных
При создании списков данных важно помнить о многопользовательском характере большинства приложений и соответствующим образом корректировать структуру списка. В дополнение к приведенному выше примеру давайте добавим записи блога в ваше приложение. Первым вашим побуждением может быть использование 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()
, которая генерирует уникальный ключ для каждого нового дочернего элемента . Используя уникальные дочерние ключи, несколько клиентов могут одновременно добавлять дочерние элементы в одно и то же расположение, не беспокоясь о конфликтах записи.
Джава
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 имеет язык безопасности, который позволяет вам определить, какие пользователи имеют доступ для чтения и записи к различным узлам ваших данных. Подробнее об этом можно прочитать в статье «Защитите свои данные ».