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

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

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

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

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

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

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

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

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

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

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

Ява

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

Котлин + 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 объект может выглядеть следующим образом :

Ява

@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;
    }

}

Котлин + 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() следующим образом :

Ява

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

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

Котлин + KTX

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

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

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

Ява

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

Котлин + KTX

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

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

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

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

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

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

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

Ява

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);

Котлин + 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() может увеличить использование пропускной способности и приводит к потере производительности, который может быть предотвращен с помощью слушателя в режиме реального времени , как показано выше.

Ява

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()));
        }
    }
});

Котлин + 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 класс вроде этого:

Ява

@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;
    }
}

Котлин + 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
        )
    }
}

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

Ява

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);
}

Котлин + 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() принимают необязательные завершающие слушателя , который вызывается , когда запись была успешно выполнена в базу данных. Если вызов был неудачным, слушателю передается объект ошибки, указывающий, почему произошел сбой.

Ява

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
                // ...
            }
        });

Котлин + KTX

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

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

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

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

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

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

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

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

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

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

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

Ява

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);
        }
    });
}

Котлин + 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!!)
        }
    })
}

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

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

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

Ява

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);
}

Котлин + KTX

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

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

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

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

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

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

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

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

Мы больше о автономном поведении говорить Узнайте больше о онлайн и оффлайн возможностей .

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