قراءة وكتابة البيانات على Android

يغطي هذا المستند أساسيات قراءة بيانات Firebase وكتابتها.

تتم كتابة بيانات FirebaseDatabase في مرجع قاعدة بيانات Firebase ويتم استردادها عن طريق إرفاق مستمع غير متزامن بالمرجع. يتم تشغيل المستمع مرة واحدة للحالة الأولية للبيانات ومرة ​​أخرى في أي وقت تتغير فيه البيانات.

(اختياري) النموذج الأولي والاختبار باستخدام Firebase Local Emulator Suite

قبل التحدث عن كيفية قراءة تطبيقك من قاعدة بيانات Realtime والكتابة إليها ، دعنا نقدم مجموعة من الأدوات التي يمكنك استخدامها لتصميم واختبار وظيفة Realtime Database: Firebase Local Emulator Suite. إذا كنت تجرب نماذج بيانات مختلفة ، أو تقوم بتحسين قواعد الأمان الخاصة بك ، أو تعمل على إيجاد الطريقة الأكثر فعالية من حيث التكلفة للتفاعل مع النهاية الخلفية ، فإن القدرة على العمل محليًا دون نشر الخدمات المباشرة يمكن أن تكون فكرة رائعة.

يعد محاكي Realtime Database جزءًا من Local Emulator Suite ، والذي يمكّن تطبيقك من التفاعل مع محتوى قاعدة البيانات التي تمت محاكاتها وتكوينها ، بالإضافة إلى موارد المشروع التي تمت محاكاتها اختياريًا (الوظائف وقواعد البيانات الأخرى وقواعد الأمان).

يتضمن استخدام محاكي Realtime Database بضع خطوات فقط:

  1. إضافة سطر من التعليمات البرمجية إلى التكوين التجريبي لتطبيقك للاتصال بالمحاكي.
  2. من جذر دليل المشروع المحلي ، قم بتشغيل firebase emulators:start .
  3. إجراء مكالمات من رمز النموذج الأولي لتطبيقك باستخدام SDK للنظام الأساسي لقاعدة البيانات Realtime كالمعتاد ، أو باستخدام Realtime Database REST API.

تتوفر إرشادات تفصيلية تتضمن Realtime Database و Cloud Functions . يجب عليك أيضًا إلقاء نظرة على مقدمة 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() لحذف عدة توابع في استدعاء واحد لواجهة برمجة التطبيقات.

افصل المستمعين

تتم إزالة عمليات الاسترجاعات عن طريق استدعاء طريقة 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 بعد ذلك بمزامنة تلك البيانات مع خوادم قاعدة البيانات البعيدة ومع العملاء الآخرين على أساس "أفضل جهد".

نتيجة لذلك ، تؤدي جميع عمليات الكتابة في قاعدة البيانات إلى تشغيل الأحداث المحلية على الفور ، قبل أي تفاعل مع الخادم. هذا يعني أن تطبيقك يظل مستجيبًا بغض النظر عن زمن انتقال الشبكة أو الاتصال.

بمجرد إعادة إنشاء الاتصال ، يتلقى تطبيقك مجموعة الأحداث المناسبة بحيث يتزامن العميل مع حالة الخادم الحالية ، دون الحاجة إلى كتابة أي رمز مخصص.

سنتحدث أكثر عن السلوك في وضع عدم الاتصال في معرفة المزيد حول إمكانات الاتصال بالإنترنت وغير المتصلة .

الخطوات التالية