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

В этом документе рассматриваются четыре метода записи данных в базу данных 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');
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() , чтобы сохранить пользовательский объект в базу данных с именем пользователя, полное имя и день рождения. Вы можете пройти множество строки, числа, булев, 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'
  }
});
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». Вы также можете сохранить данные прямо в дочернем расположении:

Джава
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 без изменений.

Обновление сохраненных данных

Если вы хотите одновременно писать в несколько дочерних узлов базы данных, не перезаписывая другие дочерние узлы, вы можете использовать метод обновления, как показано ниже:

Джава
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)
}

Это обновит данные Грейс, включив в нее ее псевдоним. Если вы использовали набор здесь вместо обновления, было бы удалено как full_name и date_of_birth от вашего hopperRef .

База данных 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'
});
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"
    }
  }
}

Обратите внимание, что попытка обновить объекты путем записи объектов с включенными путями приведет к другому поведению. Давайте посмотрим, что произойдет, если вы вместо этого попытаетесь обновить Грейс и Алана следующим образом:

Джава
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"
    }
  }
}

Добавление обратного вызова о завершении

В 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'
});
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() настолько часто , что 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'
});;
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() возвращает ссылку на новый канал передачи данных, которые вы можете использовать , чтобы получить данные ключа или набора к нему. Следующий код приведет к тем же данным, что и в приведенном выше примере, но теперь у нас будет доступ к сгенерированному уникальному ключу:

Джава
// 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 операция транзакции блокируется и поэтому принимает только функцию обновления.

Функция обновления принимает текущее состояние данных в качестве аргумента и должна возвращать новое желаемое состояние, которое вы хотите записать. Например, если вы хотите увеличить количество голосов за конкретное сообщение в блоге, вы должны написать транзакцию, подобную следующей:

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