Сохранение данных

В этом документе описаны четыре метода записи данных в базу данных 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 также поддерживает многопутевые обновления. Это означает, что обновление теперь может обновлять значения в нескольких местах вашей базы данных одновременно. Эта мощная функция позволяет денормализовать ваши данные . Используя многопутевые обновления, вы можете одновременно добавлять псевдонимы Грейс и Алану:

Джава
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 принимают необязательный обратный вызов завершения, который вызывается, когда запись зафиксирована в базе данных. Если вызов по какой-то причине оказался неудачным, обратному вызову передается объект ошибки, указывающий, почему произошел сбой. В SDK Python и Go Admin все методы записи блокируются. То есть методы записи не возвращаются до тех пор, пока запись не будет зафиксирована в базе данных.

Джава
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(&currentValue); 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 имеет язык безопасности, который позволяет вам определить, какие пользователи имеют доступ для чтения и записи к различным узлам ваших данных. Подробнее об этом можно прочитать в статье Защитите свои данные .