В этом документе описаны четыре метода записи данных в вашу Firebase Realtime Database : установка значения, обновление, отправка данных и поддержка транзакций.
Способы сохранения данных
| набор | Запись или замена данных по указанному пути , например, messages/users/<username> |
| обновлять | Обновить некоторые ключи для заданного пути, не заменяя все данные. |
| толкать | Добавить элемент в список данных в базе данных. Каждый раз, когда вы добавляете новый узел в список, ваша база данных генерирует уникальный ключ, например, messages/users/<unique-user-id>/<username> |
| сделка | Используйте транзакции при работе со сложными данными, которые могут быть повреждены в результате одновременных обновлений. |
Сохранение данных
Базовая операция записи в базу данных — это операция `set`, которая сохраняет новые данные в указанную ссылку на базу данных, заменяя все существующие данные по этому пути. Чтобы понять, что такое `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() для сохранения объекта пользователя в базе данных, содержащего имя пользователя, полное имя и дату рождения. В качестве параметра set можно передать строку, число, логическое значение, 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"); 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' } })
Идти
// 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 без изменений.
Обновление сохраненных данных
Если вам необходимо одновременно записывать данные в несколько дочерних узлов одного расположения в базе данных, не перезаписывая другие дочерние узлы, вы можете использовать метод обновления, как показано ниже:
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' })
Идти
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 также поддерживает многопутевые обновления. Это означает, что теперь обновление может одновременно заменять значения в нескольких местах вашей базы данных, что является мощной функцией, помогающей денормализовать ваши данные . Используя многопутевые обновления, вы можете одновременно добавлять псевдонимы как для Грейс, так и для Алана:
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{}{ "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"
}
}
}Добавление функции обратного вызова для завершения
В SDK для администрирования Node.js и Java, если вы хотите узнать, когда ваши данные были зафиксированы, вы можете добавить коллбэк завершения. Методы set и update в этих SDK принимают необязательный коллбэк завершения, который вызывается, когда запись в базу данных была зафиксирована. Если вызов по какой-либо причине был неудачным, в коллбэк передается объект ошибки, указывающий причину сбоя. В 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.'); } });
Сохранение списков данных
При создании списков данных важно учитывать многопользовательский характер большинства приложений и соответствующим образом корректировать структуру списка. Развивая приведенный выше пример, давайте добавим в ваше приложение записи блога. Вашим первым побуждением может быть использование множества для хранения дочерних элементов с автоматически увеличивающимися целочисленными индексами, как показано ниже:
// 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() настолько распространён, что SDK Firebase позволяет комбинировать их, передавая данные для установки непосредственно в 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 используется язык безопасности, позволяющий определять, каким пользователям разрешены доступ на чтение и запись к различным узлам ваших данных. Подробнее об этом можно прочитать в разделе «Защита ваших данных» .