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

Оптимизируйте свои подборки Сохраняйте и классифицируйте контент в соответствии со своими настройками.

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