Чтение и запись данных на Android

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

Этот документ охватывает основы чтения и записи данных Firebase.

Данные Firebase записываются в ссылку FirebaseDatabase и извлекаются путем присоединения к ссылке асинхронного прослушивателя. Слушатель запускается один раз для начального состояния данных и снова при каждом изменении данных.

(Необязательно) Создание прототипа и тестирование с помощью Firebase Local Emulator Suite

Прежде чем говорить о том, как ваше приложение читает и записывает в базу данных реального времени, давайте представим набор инструментов, которые вы можете использовать для прототипирования и тестирования функциональности базы данных реального времени: Firebase Local Emulator Suite. Если вы пробуете разные модели данных, оптимизируете правила безопасности или работаете над поиском наиболее экономичного способа взаимодействия с серверной частью, возможность работать локально без развертывания действующих сервисов может оказаться отличной идеей.

Эмулятор базы данных реального времени является частью набора локальных эмуляторов, который позволяет вашему приложению взаимодействовать с содержимым и конфигурацией эмулируемой базы данных, а также, при необходимости, с эмулируемыми ресурсами проекта (функциями, другими базами данных и правилами безопасности).

Использование эмулятора базы данных реального времени включает в себя всего несколько шагов:

  1. Добавление строки кода в тестовую конфигурацию вашего приложения для подключения к эмулятору.
  2. Из корня вашего локального каталога проекта запустите firebase emulators:start .
  3. Выполнение вызовов из кода прототипа вашего приложения с помощью SDK платформы базы данных реального времени, как обычно, или с помощью REST API базы данных реального времени.

Доступно подробное пошаговое руководство с использованием базы данных реального времени и облачных функций . Вам также следует ознакомиться с введением в Local Emulator Suite .

Получить ссылку на базу данных

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

Java

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

Kotlin+KTX

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Запись данных

Основные операции записи

Для основных операций записи вы можете использовать setValue() для сохранения данных по указанной ссылке, заменяя любые существующие данные по этому пути. Вы можете использовать этот метод для:

  • Передавайте типы, соответствующие доступным типам JSON, следующим образом:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Передайте пользовательский объект Java, если класс, который его определяет, имеет конструктор по умолчанию, который не принимает аргументов и имеет общедоступные методы получения для назначаемых свойств.

Если вы используете объект Java, содержимое вашего объекта автоматически сопоставляется с дочерними местоположениями во вложенном виде. Использование объекта Java также обычно делает ваш код более читабельным и простым в обслуживании. Например, если у вас есть приложение с базовым профилем пользователя, ваш объект User может выглядеть следующим образом:

Java

@IgnoreExtraProperties
public class User {

    public String username;
    public String email;

    public User() {
        // Default constructor required for calls to DataSnapshot.getValue(User.class)
    }

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }

}

Kotlin+KTX

@IgnoreExtraProperties
data class User(val username: String? = null, val email: String? = null) {
    // Null default values create a no-argument default constructor, which is needed
    // for deserialization from a DataSnapshot.
}

Вы можете добавить пользователя с помощью setValue() следующим образом:

Java

public void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

Kotlin+KTX

fun writeNewUser(userId: String, name: String, email: String) {
    val user = User(name, email)

    database.child("users").child(userId).setValue(user)
}

Использование setValue() таким образом перезаписывает данные в указанном месте, включая любые дочерние узлы. Однако вы все равно можете обновить дочерний объект, не перезаписывая весь объект. Если вы хотите разрешить пользователям обновлять свои профили, вы можете обновить имя пользователя следующим образом:

Java

mDatabase.child("users").child(userId).child("username").setValue(name);

Kotlin+KTX

database.child("users").child(userId).child("username").setValue(name)

Чтение данных

Чтение данных с постоянными слушателями

Чтобы прочитать данные по пути и прослушать изменения, используйте метод addValueEventListener() , чтобы добавить ValueEventListener в DatabaseReference .

Слушатель Обратный вызов события Типичное использование
ValueEventListener onDataChange() Чтение и прослушивание изменений всего содержимого пути.

Вы можете использовать метод onDataChange() для чтения статического снимка содержимого по заданному пути в том виде, в каком оно существовало во время события. Этот метод запускается один раз, когда прослушиватель подключен, и снова каждый раз, когда данные, включая дочерние, изменяются. Обратному вызову события передается моментальный снимок, содержащий все данные в этом месте, включая дочерние данные. Если данных нет, снимок будет возвращать false , когда вы вызываете exists() , и null , когда вы вызываете getValue() .

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

Java

ValueEventListener postListener = new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        // Get Post object and use the values to update the UI
        Post post = dataSnapshot.getValue(Post.class);
        // ..
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
    }
};
mPostReference.addValueEventListener(postListener);

Kotlin+KTX

val postListener = object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        // Get Post object and use the values to update the UI
        val post = dataSnapshot.getValue<Post>()
        // ...
    }

    override fun onCancelled(databaseError: DatabaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
    }
}
postReference.addValueEventListener(postListener)

Прослушиватель получает DataSnapshot , содержащий данные в указанном месте базы данных на момент события. Вызов getValue() моментального снимка возвращает объектное представление данных Java. Если в этом месте нет данных, вызов getValue() возвращает null .

В этом примере ValueEventListener также определяет метод onCancelled() , который вызывается при отмене чтения. Например, чтение может быть отменено, если у клиента нет разрешения на чтение из базы данных Firebase. Этому методу передается объект DatabaseError , указывающий, почему произошел сбой.

Прочитать данные один раз

Прочитать один раз, используя get()

SDK предназначен для управления взаимодействием с серверами баз данных независимо от того, находится ли ваше приложение в сети или в автономном режиме.

Как правило, вы должны использовать методы ValueEventListener , описанные выше, для чтения данных, чтобы получать уведомления об обновлениях данных из серверной части. Методы прослушивания сокращают использование и оплату счетов и оптимизированы для обеспечения наилучшего взаимодействия с пользователями при работе в сети и в автономном режиме.

Если вам нужны данные только один раз, вы можете использовать get() для получения моментального снимка данных из базы данных. Если по какой-либо причине get() не может вернуть значение сервера, клиент проверит кэш локального хранилища и вернет ошибку, если значение все еще не найдено.

Ненужное использование get() может увеличить использование пропускной способности и привести к потере производительности, которую можно предотвратить, используя прослушиватель в реальном времени, как показано выше.

Java

mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
    @Override
    public void onComplete(@NonNull Task<DataSnapshot> task) {
        if (!task.isSuccessful()) {
            Log.e("firebase", "Error getting data", task.getException());
        }
        else {
            Log.d("firebase", String.valueOf(task.getResult().getValue()));
        }
    }
});

Kotlin+KTX

mDatabase.child("users").child(userId).get().addOnSuccessListener {
    Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
    Log.e("firebase", "Error getting data", it)
}

Прочитать один раз, используя прослушиватель

В некоторых случаях вы можете захотеть, чтобы значение из локального кеша было возвращено немедленно, вместо проверки обновленного значения на сервере. В этих случаях вы можете использовать addListenerForSingleValueEvent для немедленного получения данных из кэша локального диска.

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

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

Обновить определенные поля

Для одновременной записи в определенные дочерние узлы без перезаписи других дочерних узлов используйте метод updateChildren() .

При вызове updateChildren() вы можете обновить дочерние значения более низкого уровня, указав путь для ключа. Если данные хранятся в нескольких местах для лучшего масштабирования, вы можете обновить все экземпляры этих данных с помощью разветвления данных . Например, приложение для ведения социальных блогов может иметь такой класс Post :

Java

@IgnoreExtraProperties
public class Post {

    public String uid;
    public String author;
    public String title;
    public String body;
    public int starCount = 0;
    public Map<String, Boolean> stars = new HashMap<>();

    public Post() {
        // Default constructor required for calls to DataSnapshot.getValue(Post.class)
    }

    public Post(String uid, String author, String title, String body) {
        this.uid = uid;
        this.author = author;
        this.title = title;
        this.body = body;
    }

    @Exclude
    public Map<String, Object> toMap() {
        HashMap<String, Object> result = new HashMap<>();
        result.put("uid", uid);
        result.put("author", author);
        result.put("title", title);
        result.put("body", body);
        result.put("starCount", starCount);
        result.put("stars", stars);

        return result;
    }
}

Kotlin+KTX

@IgnoreExtraProperties
data class Post(
    var uid: String? = "",
    var author: String? = "",
    var title: String? = "",
    var body: String? = "",
    var starCount: Int = 0,
    var stars: MutableMap<String, Boolean> = HashMap()
) {

    @Exclude
    fun toMap(): Map<String, Any?> {
        return mapOf(
                "uid" to uid,
                "author" to author,
                "title" to title,
                "body" to body,
                "starCount" to starCount,
                "stars" to stars
        )
    }
}

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

Java

private void writeNewPost(String userId, String username, String title, String body) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    String key = mDatabase.child("posts").push().getKey();
    Post post = new Post(userId, username, title, body);
    Map<String, Object> postValues = post.toMap();

    Map<String, Object> childUpdates = new HashMap<>();
    childUpdates.put("/posts/" + key, postValues);
    childUpdates.put("/user-posts/" + userId + "/" + key, postValues);

    mDatabase.updateChildren(childUpdates);
}

Kotlin+KTX

private fun writeNewPost(userId: String, username: String, title: String, body: String) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    val key = database.child("posts").push().key
    if (key == null) {
        Log.w(TAG, "Couldn't get push key for posts")
        return
    }

    val post = Post(userId, username, title, body)
    val postValues = post.toMap()

    val childUpdates = hashMapOf<String, Any>(
            "/posts/$key" to postValues,
            "/user-posts/$userId/$key" to postValues
    )

    database.updateChildren(childUpdates)
}

В этом примере push() используется для создания сообщения в узле, содержащем сообщения для всех пользователей в /posts/$postid и одновременного получения ключа с помощью getKey() . Затем ключ можно использовать для создания второй записи в сообщениях пользователя в /user-posts/$userid/$postid .

Используя эти пути, вы можете выполнять одновременные обновления в нескольких местах в дереве JSON с помощью одного вызова updateChildren() , например, как в этом примере создается новая запись в обоих местах. Одновременные обновления, сделанные таким образом, являются атомарными: либо все обновления выполняются успешно, либо все обновления завершаются неудачно.

Добавить обратный вызов завершения

Если вы хотите знать, когда ваши данные были зафиксированы, вы можете добавить прослушиватель завершения. Как setValue() , так и updateChildren() принимают необязательный прослушиватель завершения, который вызывается, когда запись была успешно зафиксирована в базе данных. Если вызов не удался, слушателю передается объект ошибки, указывающий, почему произошел сбой.

Java

mDatabase.child("users").child(userId).setValue(user)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // Write was successful!
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Write failed
                // ...
            }
        });

Kotlin+KTX

database.child("users").child(userId).setValue(user)
        .addOnSuccessListener {
            // Write was successful!
            // ...
        }
        .addOnFailureListener {
            // Write failed
            // ...
        }

Удалить данные

Самый простой способ удалить данные — вызвать removeValue() для ссылки на расположение этих данных.

Вы также можете удалить, указав null в качестве значения для другой операции записи, такой как setValue() или updateChildren() . Вы можете использовать эту технику с updateChildren() для удаления нескольких дочерних элементов в одном вызове API.

Отключить слушателей

Обратные вызовы удаляются путем вызова removeEventListener() в вашей ссылке на базу данных Firebase.

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

Вызов removeEventListener() для родительского прослушивателя не приводит к автоматическому удалению прослушивателей, зарегистрированных на его дочерних узлах; removeEventListener() также должен быть вызван для любых дочерних прослушивателей, чтобы удалить обратный вызов.

Сохранить данные как транзакции

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

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

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @Override
        public Transaction.Result doTransaction(MutableData mutableData) {
            Post p = mutableData.getValue(Post.class);
            if (p == null) {
                return Transaction.success(mutableData);
            }

            if (p.stars.containsKey(getUid())) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1;
                p.stars.remove(getUid());
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1;
                p.stars.put(getUid(), true);
            }

            // Set value and report transaction success
            mutableData.setValue(p);
            return Transaction.success(mutableData);
        }

        @Override
        public void onComplete(DatabaseError databaseError, boolean committed,
                               DataSnapshot currentData) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

Kotlin+KTX

private fun onStarClicked(postRef: DatabaseReference) {
    // ...
    postRef.runTransaction(object : Transaction.Handler {
        override fun doTransaction(mutableData: MutableData): Transaction.Result {
            val p = mutableData.getValue(Post::class.java)
                    ?: return Transaction.success(mutableData)

            if (p.stars.containsKey(uid)) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1
                p.stars.remove(uid)
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1
                p.stars[uid] = true
            }

            // Set value and report transaction success
            mutableData.value = p
            return Transaction.success(mutableData)
        }

        override fun onComplete(
                databaseError: DatabaseError?,
                committed: Boolean,
                currentData: DataSnapshot?
        ) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError!!)
        }
    })
}

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

Атомарные приращения на стороне сервера

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

Java

private void onStarClicked(String uid, String key) {
    Map<String, Object> updates = new HashMap<>();
    updates.put("posts/"+key+"/stars/"+uid, true);
    updates.put("posts/"+key+"/starCount", ServerValue.increment(1));
    updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true);
    updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1));
    mDatabase.updateChildren(updates);
}

Kotlin+KTX

private fun onStarClicked(uid: String, key: String) {
    val updates: MutableMap<String, Any> = hashMapOf(
        "posts/$key/stars/$uid" to true,
        "posts/$key/starCount" to ServerValue.increment(1),
        "user-posts/$uid/$key/stars/$uid" to true,
        "user-posts/$uid/$key/starCount" to ServerValue.increment(1)
    )
    database.updateChildren(updates)
}

Этот код не использует транзакционную операцию, поэтому он не запускается автоматически повторно в случае конфликтующего обновления. Однако, поскольку операция увеличения происходит непосредственно на сервере базы данных, вероятность конфликта исключена.

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

Работайте с данными в автономном режиме

Если клиент потеряет подключение к сети, ваше приложение продолжит работать правильно.

Каждый клиент, подключенный к базе данных Firebase, поддерживает свою собственную внутреннюю версию любых данных, для которых используются прослушиватели или которые помечены для синхронизации с сервером. Когда данные читаются или записываются, эта локальная версия данных используется в первую очередь. Затем клиент Firebase синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами на основе «максимальных усилий».

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

После восстановления подключения ваше приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости писать какой-либо пользовательский код.

Подробнее о поведении в автономном режиме мы поговорим в разделе Дополнительные сведения о возможностях в сети и в автономном режиме .

Следующие шаги