تعرّف على IndexedDB


Ayman

تُقدم IndexedDB أو "قاعدة البيانات المُفَهرَسَة" طريقة حيوية وفعالّة لتخزين وجلب البيانات باستخدام المتصفح. و لكونها تتعامل مع قواعد البيانات في الخادم فإن IndexedDB تسمح بتوليد مفاتيح وبحث البيانات أو ترتيبها في حقل معين. سنُبحر في هذه المقالة في عالم IndexedDB حيث سنشرح واجهة التطبيقات البرمجية الخاصة بقواعد البيانات المفهرسة IndexedDB API وذلك من خلال بناء مدير مهام من نوع خاص. لكن أولاً دعونا نأخذ أولاً فكرة عن قواعد البيانات وIndexedDB.

ماهي IndexedDB ؟

تعتبر IndexedDB مخزن للكائنات key-value كما أنها غير متزامنة ومرحلية. تحتوي IndexedDB على الكثير من الأفكار والتي يمكن شرحها بجملة واحدة، لكنيّ سأقوم بشرح كل واحدةٍ منها على حدى.

  • غير متزامنة: تعني بأن IndexedDB لاتحجب واجهة المستخدم. حيث أن العمليات تجري على نحوٍ لاحق، أي أنها ليست فورية. وهذا بالطبع يسمح لواجهة المستخدم بالإستجابة للأوامر الأخرى. وكمثال للمقارنة: يعتبر التخزين المحلي localStorage متزامنًا، أي أن العمليات تجري بشكل فوري وﻻ يحدث أي شيء حتى تكتمل العملية. وفي حال كان لدينا الكثير من أوامر القراءة والكتابة سيحدث بطؤُ ملحوظ في البرنامج.
  • مرحلية: وتعني بأن جميع العمليات في IndexedDB يجب أن تتم جميعها أو لا تتم كليةً. يُمكن أن تفشل العملية للعديد من الأسباب عندها ستقوم قاعدة البيانات بالرجوع إلى حالتها السابقة. لأنه من غير الممكن أن يكون هناك أمر جزئي في قاعدة البيانات المُفَهرسَة IndexedDB.
  • تخزين كائن key-value: تعني بأن كل أمر في قاعدة البيانات هو كائن بحد ذاته، أي أنه يُعارض مفهوم الصفوف (rows) في قاعدة البيانات. تُستخدم قواعد البيانات التقليدية نموذجًا منطقيًا، حيث أن البيانات يتم التعرّف عليها في جدول كما ترتبط بصلات بين قيمة جدول معين وأيضاً المفاتيح لجدول آخر. كما في الصورة1. 

من خلال عملية تخزين الكائن key-value كمفتاح وقيمة، يحتوي كل أمر على كائن مستقل خاص به. حيث لا يمكن الربط بين أي كائن وآخر، كما يمكن أن يكون في نفس مكان التخزين أكثر من كائن و كل كائن مختلفٌ نوعه عن الآخر.

الصورة 1: كائنات في نفس مكان التخزين ولا يتوجب أن تكون تلك الكائنات بنفس الخاصيات.

يُعتبر هذا الاختلاف الأكبر من نوعه بين قواعد البيانات المفهرسة IndexedDB وبين قواعد البيانات الأخرى مثل SQL أو MySQL. فمثلاً يجب أن يحتوي كل حقل قيمة معينة في قواعد البيانات SQL حتى وإن كانت خالية NULL ولكن باستخدام IndexedDB فإن بنية قاعدة البيانات يمكن أن تكون مرنة حسب احتياجك واستخدامك لها.

و أيضاً تحتوي IndexedDB على كمّية هائلة من تخزين البيانات تفوق التخزين المحلي localStorage والذي يفرض مساحة لاتقل عن 250 ميجابايت على معظم المتصفحات. حيث أن هذه المساحة أساسية مع المتصفح Internet Explorer ولكن تستخدم Google Opera وOpera نسبة مئوية للتخزين بينما غير معروف عن كيمة ومحدودية التخزين الذي يستخدمها فَيرفُكس.

بينما مع IndexedDB يمكن إنشاء تخزين ثنائي للبيانات وبكلمات أخرى تشبه هذه العملية تركيبة Javascript مع التعامل مع البيانات والقدرة على استيعاب قواعد بيانات الموقع.

المتصفحات الداعمة الحالية

للأسف تُعتبر IndexedDB غير مدعومة من جميع المتصفحات بعد. فقط مع متصفح Opera الاصدار 15 ومافوق وGoogle Chrome 24 ومافوق وفَيرفُكس15 ومافوق، أيضاُ Internet Explorer 10 ومافوق. بينما الاصدارات الأقدم من Chrome وفَيرفُكس يتم دعمها من خلال التجارب فقط التي تدعم واجهة التطبيقات البرمجية API. ولن نتكلم عن هذا.

لا يدعم متصفح سفاري IndexedDB مطلقاً ولا نسخة Presto-based المدعومة من متصفح Opera اصدرات ماقبل نسخة 12. و لكن عوضاً عن ذلك فهو يدعم قاعدة بيانات الموقع القديم. كما يدعم في معظم الأحيان IndexedDBShim. طبعاً و بالنسبة للمتصفحات التي لاتدعم أيًّا منهم فمن الأفضل البقاء على قواعد البيانات التي تعمل على الخادم.

كيفية اختبار دعم IndexedDB

يمكنك كتابة الشِفرة التالية لاختبار فيما إذا كان استخدام IndexedDB ممكناً:

var hasIDB = typeof window.indexedDB != 'undefined';

أو يمكنك استخدام  Modernizr:

var hasIDB = Modernizr.indexeddb;

كن متأكداً بأن هذه الاختبار فقط للمتصفحات الأخيرة الداعمة لـِ IndexedDB.

إنشاء مدير مهام

هذه الفقرة فقط لخبراء المشروع التجريبي. لكن مع ذلك يمكنك تحميل المصدر وذلك للحصول على فهم أفضل حول عمل هذه الدوال في الكود.

باستخدام مدير المهام ستحصل على مايلي:

  • حفظ المهمة.
  • ضبط بداية و نهاية التواريخ.
  • ضبط أولوية المهمة.
  • إضافة ملاحظة لكل مهمة.
  • ميزة البحث عن المهام و الملاحظات السابقة.

أولاً لنبدأ بإنشاء نموذج والذي سنستخدمه لإضافة المهام الجديدة في قاعدة البيانات:

<form id="addnew">
  <div>
    <!-- Used for updates -->
    <input type="hidden" name="key" id="key" value="">
    <label for="task">What do you need to do? (required)</label>
    <input type="text" name="task" id="task" value="" required>
  </div>
 
  <div class="txtright">
    <input type="checkbox" name="status" id="status"><label for="status">Completed?</label>
  </div>
 
  <div>
    <label for="start">Start date:</label>
    <input type="date" id="start" name="start" value="">
  </div>
 
  <div>
    <label for="due">Due date:</label>
    <input type="date" id="due" name="due" value="">
  </div>
 
  <div>
    <label for="priority">Priority:</label>
    <select id="priority" name="priority">
      <option value="0">None</option>
      <option value="1">1 - High</option>
      <option value="2">2</option>
      <option value="3">3 - Medium</option>
      <option value="4">4</option>
      <option value="5">5 - Low</option>
    </select>
  </div>
 
  <div>
    <label for="tasknotes">Task notes</label>
    <textarea id="tasknotes" name="tasknotes" cols="30" rows="3"></textarea>
    <button type="submit" id="submit">Save entry</button>
    <button type="button" id="delete" class="hidden" >Delete entry</button>
  </div>
</form>

تشرح الشِفرة البرمجية السابقة المؤلفة من HTML وبعضٍ من CSS إنشاء نموذج مشابه نوعاً ما للصورة2.

الصورة 2 : نموذج مدير المهام

ماذا تحتاج أن تفعل؟ كل ما تحتاجه يجب أن يكون ضمن حقول النموذج حيث ستستخدم النموذج أيضاً لاحقاً للإضافة والتحديث على المهام. وبما أننا شرحنا ما يجب علينا فعله سنقوم الآن ببناء قاعدة البيانات.

إنشاء قاعدة البيانات

لإنشاء قاعدة بيانات IndexedDB يجب استخدام الطريقة ()open في كائن indexedDB.

var idb = indexedDB.open('IDBTaskManager', 1);

في البداية يجب وضع اسم مناسب لقاعدة البيانات، وهذا إجباري، يجب أن يكون الاسم نصًا عاديًا. يمكن تسمية قاعدة البيانات لأي شيء تريده لكن فقط نص عادي. وبكل الأحوال يجب أن يكون كل اسم لقاعدة البيانات فريدًا و أصليًا. أصلي نعني به أن يكون مكونًا من البروتوكول:اسم المستضيف:المنفذ مثل https://dev.opera.com أو http://www.example.com:80.

ثانياً و اختيارياً يمكن وضع رقم اصدار قاعدة البيانات، بما أن هذا المشروع هو تجربتنا الأولى سنضع الرقم1.

استخدام رقم الاصدار مهم جداً لأن الدالة open تقوم بإنشاء قاعدة اذا لم تكن موجودة من الأصل ووضع رقم الاصدار هو 1 وإذا لم تكن قاعدة البيانات موجودة سيتم إنشاء اتصال إليها.

يجب أن يكون رقم الاصدار integer أي عبارة عن رقم فقط وأن يكون أكبر من الصفر. بينما الأرقام العشرية أو التي تحتوي على كسور سيتم حذف تلك الكسور لإرجاع الرقم الى رقم integer. أي أن الرقم 2.5 سيصبح 2 فقط وأيضاً 0.8 سيصبح 0 (حيث يسبب ذلك خطأ برمجي). الحد الأعلى لرقم الاصدار هو 253 أو 9,007,199,254,740,992. و هذا الحد الأعلى أيضاً يمكن تطبيقه على موّلد المفاتيح.

ملاحظة: يمكن من خلال متصفح الاوبرا استخدام فحص تخزين كائنات IndexedDB وأيضاً المفاتيح والقيم من خلال الخيار المصادر الموجود في خيارات المطورون في المتصفح.

وبذلك سيتم إنجاز العملية والتي ستعيد الكائن IDBOpenDBRequest كما سيتم نجاح الحدث. الآن دعونا نعرّف الأمر onsuccess لهذا الحدث. سنقوم بتعيين الكائن IDBDatabase مع event.target.result والذي سيتم تحويله إلى متغير والذي يقوم بنشر هدف إلى الدوال الأخرى.

// Define a global variable to hold our database object
var dbobject;

idb.onsuccess = function(evt){
  dbobject = evt.target.result;
}

مع استخدام IndexedDB فإن كل خطوة أو عملية في قاعدة البيانات يجب أن تكون ضمن دالّة لأمر مرجعي. ولفعل ذلك يحتاج كائن قاعدة البيانات لأن يكون متوفرًا على كل دالة والتي تقوم بالعملية كاملة.

نسخة قاعدة البيانات

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

يعتبر إنشاء الحدث upgradeneeded الطريقة الوحيدة لتغيير البنية الأصلية لقاعدة البيانات. تتضمن التغيرات الجذرية إنشاء وحذف تخزين الكائن أو اضافة فهرسات. يمكننا صنع هذه التغيرات ضمن الحدث المرجعي onupgradeneeded كما في المثال التالي:

idb.onupgradeneeded = function (evt) {
  if (evt.oldVersion < 1) {
    // Create our object store and define indexes.
  }
}

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

لكن عند طرده سيظهر الحدث upgradeneeded قبل حدث نجاح الاتصال success. و أيضاً لن يتم تعريف المتغير dbobject عند استدعاء idb.onupgradeneeded. فقط تذكر ذلك عند برمجة و تطوير التطبيقات.

إنشاء تخزين الكائن

لاقيمة لإنشاء قاعدة البيانات بمفردها. لحفظ و تعديل الأوامر ستحتاج أيضاً إلى إنشاء تخزين الكائن. تخزين الكائنات مشابهة لعملية الجداول في SQL وهي الوحدة التي تحمل جميع البيانات والصفوف.

تتغير البنية عند إنشاء تخزين الكائن ولهذا سنقوم بهذه الخطوة ضمن الأمر المرجعي onupgradeneeded. الآن سنضيف تخزين الكائن وليكن اسمه tasks باستخدام الطريقة createObjectStore:

idb.onupgradeneeded = function(evt){
  var dbobject = evt.target.result;
  // Check our version number
  if (evt.oldVersion < 1) {
    dbobject.createObjectStore('tasks',{autoIncrement: true});
  }
};

الوسيطة الأولى للكائن createObjectStore مطلوبة لأنها اسم تخزين الكائن بينما الوسيطة الثانية اختيارية. و مع ذلك يجب أن يكون هناك قاموس والذي يعرف خيارات المفتاح للمخزن.

تشبه القواميس كائنات Javascript حرفياً. و لكنهم بصراحة تم تخصيصهم للمصفوفات مع المفاتيح والقيم الخاصة بهم. تسمح لنا القواميس بتخطي الوسائط بدون قلق حول ترتيبهم. ومع createObjectStore يُمكن أن يحتوي القاموس على الخاصيات والقيم التالية:

  • keyPath: يحدد أيّ خاصية للكائن ينبغي استخدامها كمفتاح لكل صف . القيمة الافتراضية هي null.
  • autoIncrement: هي قيمة منطقية و التي  تحدد فيما اذا يمكن توليد مفاتيح تلقائي في الصفوف أو لا . القيمة الافتراضية خطأ false.

يسمح لنا keyPath بتحويل خاصية معينة إلى اجبارية. فعلى سبيل المثال استخدام: 

{keyPath: 'task'} 

تعني بأن كل كائن يتم إضافته للمخزن يجب أن يحتوي على الخاصية task.

ستلاحظ من خلال المشروع التجريبي بأننا نستحدم autoIncrement. و بإستخدام كلاً من autoIncrement أو keyPath لن نتمكن من تحديد الواسطة في الطرق add أو put.

ملاحظة: يمكنك استخدام autoIncrement وkeyPath سوياً حيث سيصبح المفاتيح رقمية كما أن الكائنات ستحصل على حقول إجبارية.

العمل على التسجيلات

يتألف العمل مع التسجيلات من أربع مراحل وهي الإضافة، التحديث، الحذف أو الاستعادة.

  1. إنشاء طريقة اتصال لكائن أو أكثر باستخدام readwrite أو readonly.
  2. تحديد اي كائن سيقوم بالأمر مع طريقة الاستجابة.
  3. إنشاء طلب باستخدام واحداً من طرق الطلبات أو تمرير كائن.
  4. افعل شيئاً ما مع النتيجة باستخدام الأمر المرجعي onsuccess.

يعتبر صعباً العمل على صف واحد من قاعدة البيانات  من العمل مع مجموعة من الصفوف. وفي هذا القسم سنعمل على صف واحد باستخدام المؤشر لاسترجاع صفوف متعددة.

إضافة صف/ تسجيل

لإضافة صفٍ جديد لديك خياران: إما باستخدام ()add أو ()put. الفرق بينهما أن ()add يمكن استخدامها في حال اضافة صف جديد فقط بينما ()put تستخدم لإضافة أو تحديث صف معين. كلا الطريقتين تقبل نقاشين كحد أعلى.

  • Value (مطلوب): حفظ الكائن.
  • Key (اختياري): مفتاح الكائن وهو ضروري فقط في حال كانت الحالة خاطئة للطريقة autoIncrement ولم يتم تعريف keyPath.

الآن دعونا نحفظ المهمة في قاعدة البيانات عندما يقوم المستخدم بالضغط على ارسال النموذج:

var addnewhandler, addnew;
addnew = document.getElementById('addnew');

addnewhandler = function (evt) {
  'use strict';

  evt.preventDefault();
  var entry = {}, transaction, objectstore, request, fields = evt.target, o;

  // Build our task object.
  for (o in fields) {
    if ( fields.hasOwnProperty(o)) {
      entry[o] = fields[o].value;
    }
  }  

  // Open a transaction for writing
  transaction = dbobject.transaction(['tasks'], 'readwrite');
  objectstore = transaction.objectStore('tasks');
  // Save the entry object
  request = objectstore.add(entry);
  transaction.oncomplete = function (evt) {
    displaytasks(dbobject);
  };
  transaction.onerror = errorhandler; 
};

addnew.addEventListener('submit', addnewhandler);

لانحتاج فعلاً لحذف تلك القيم بما أن المدخلات الضارة محدودة للمستخدم. يمكنك إدراج الأقواس < و > عند اظهار النتيجة. في حال أردت مزامنة بيانات IndexedDB مع قواعد بيانات السيرفر عندها كن متأكداً بفلترة وادراج مدخلات المستخدم كأولوية لقاعدة البيانات.

الخطوة التالية هي: فتح طريق اتصال لكائن قاعدة البيانات باستخدام الطريقة transaction. حيث تقبل الطريقة transaction نقاشين هما: تسلسل أسماء تخزين الكائن وأيضاً صيغة هذا الاتصال.

التسلسل هو قائمة من مخزن كائن واحد أو أكثر حيث ينتج اتصال  وهنا سنقوم فقط بفتح مخزن الكائن tasks.

دعونا نفترض بأن التطبيق يسمح لنا بتعيين مهام للآخرين. يتم تخزين أولئك الأشخاص ووظائفهم في الكائن assignees. إذا أردنا أيضاً استخدام وظائف القراءة والكتابة في assignees عندها يجب استخدامهم معاً.

transaction = dbobject.transaction(['tasks', 'assignees'], 'readwrite');

ملاحظة: تستخدم الأقواس [ وَ ] بشكل اختياري في معظم اصدارات المتصفحات في حال أردت إنشاء اتصال لكائن واحد. لكن بعض الاصدارات القديمة للمتصفحات لاتزال تطلبهم. ولأفضل توافقية استخدم تلك الأقواس.

لدينا خياران مع صيغة نقاش وهما: readwrite وreadonly. يسمح لنا readwrite باسترجاع وإضافة وتحديث أو حذف الصفوف. وبكل الأحوال للحفاظ على سلامة البيانات يمكن استخدام readwrite مرة واحدة.

لاسترجاع الصفوف لعرضها يمكنك فقط استخدام readonly. يمكن تشغيل أكثر من readonly مرة واحدة في نفس تخزين الكائن والذي بذلك يساعد على تحسين الأداء وبما أننا نضيف صفوف يجب أن نستخدم الصيغة readwrite.

الخطوة الثالثة وهي اختيار أي كائن سنستخدمه في طلب الأمر:

transaction.objectStore('tasks');

أخيراً يكتب الأمر (request = objectstore.add(entry طريقة ادخال الكائن إلى قاعدة البيانات. سيتم استدعاء الدالة displaytasks عند اكتمال العملية.

لإضافة صفوف متعددة يمكنك استدعاء الطريقة ()add مرات عديدة باستخدام نفس طلب الكائن.

request = objectstore.add({object1:'Test object 1'});
request = objectstore.add({object2:'Test object 2'});
request = objectstore.add({object3:'Test object 3'});

في هذه الحالة سيتم طرد الأحداث success وcomplete فور اكتمال جميع عمليات add وput عوضاً عن طردهم واحداً في كل مرة.

تحديث الصف / التسجيل

يجب استخدام الطريقة put لجميع عمليات التحديث متضمنة مفتاح النقاش. ترك تلك الطريقة سيقوم بإنشاء صف جديد ولكن هذا ما لا نريده هنا.

يستخدم تطبيقنا نفس المظهر في كلاً من عمليات الإضافة والتعديل على المهام. سنقوم باستخدام الدالّة addnewhandler للتعامل مع عمليات الإضافة والتعديل. فقط نحتاج تعديلها للتحرير بإضافة كود شرطي. يكون حقل key خالياً في النموذج لهذا سنقوم بإضافة صف جديد. و إذا كان يحمل أي قيمة سنقوم بالتعديل باستخدام put.

addnewhandler = function (evt) {

  evt.preventDefault();

  var entry = {}, transaction, objectstore, request, fields = evt.target, o;

  for (o in fields) {
    if ( fields.hasOwnProperty(o)) {
      entry[o] = fields[o].value;
     }
  }

  transaction = dbobject.transaction(['tasks'], 'readwrite');
  objectstore = transaction.objectStore('tasks');
  // Save the entry object with a key if one is available.
  if(fields.key.value){
    // +fields.key.value converts our key to a number
    request = objectstore.put(entry, +fields.key.value);
  } else {
    request = objectstore.add(entry);
  }

  transaction.oncomplete = function (evt) {
    displaytasks(dbobject);
  };

  transaction.onerror = errorhandler;
};

استرجاع الصفوف

للتعديل على الصفوف تحتاج أولاً لاسترجاعها. وهنا يمكننا استخدام الطريقة get. سيتم إدراج الحدث hashchange عند الضغط على قائمة المهام. سنقوم الآن بتعريف الدالّة hashchangehandler لإستعادة العناصر المطابقة.

hashchangehandler = function (evt) {

  var transaction, objectstore, request, key;

  if (window.location.hash) {
    // Extract digit characters from the hash, and convert to a number.
    // Generated IndexedDB keys are numbers. String values won’t work.
    key = +window.location.hash.match(/\d/g).join('');
    // Run a read-only transaction on this object store.
    transaction = dbobject.transaction(['tasks'], 'readonly');
    objectstore = transaction.objectStore('tasks');
    // Retrieve the record by its key
    request = objectstore.get(key);
    // If it’s successful, update our form fields.
    request.onsuccess = function (successevent) {
      var o, data = successevent.target.result;
      for(o in data) {
        if( o == 'status') {
          addnew.status.checked = !!data.status;
        }
      
      addnew[o] = data[o];
    }
  };
  
  transaction.oncomplete = function (evt) {
    hide('#tasklist');
    show('#addnew');
  }
};

تسترجع الطريقة get الصفوف. كما أنها تقبل المُعامل (parameter) المفرد، أي سيتم استرجاع المفتاح كما أننا قمنا باستعادة الصف من خلال صيغة العمليات من خلال استخدام readonly.

ملاحظة: تعتبر مفاتيح IndexedDB محددة من خلال النوع كما أن مولد المفاتيح هو عيارة عن أرقام. في حال قمت بتخطي get حتى ولو كان رقمي فإنه لن يعمل. ستحتاج إلى تحويل المُعامل إلى رقم كما فعلنا سابقاً.

إذا كان الطلب ناجحاً سنقوم بتزويد النموذج addnew# بالنتيجة لعملية get.

يجب تعريف الحدث onsuccess في أي وقت عند ظهور نتيجة الطلب. لقد قمنا أيضاً بتعريف oncomplete والذي سيتم إدراجه في حال اكتمال العملية. تنتهي العملية دوماً في حال فشل الطلب.

حذف الصف

سنقوم باستخدام طريقة مألوفة في حذف الصف كما فعلنا سابقاً مع استرجاع البيانات. ولكن هذه المرة سنقوم بحذف البيانات باستخدام الطريقة delete. تتطلب الطريقة delete مُعاملا واحدًا، ومفتاح الكائن سيقوم بحذف التخزين. أيضاً نحتاج لفتح عملية جديدة من خلال الصيغة readwrite.

deletehandler = function (evt) {

  var transaction, objectstore, request, key;

  if (window.location.hash) {
    // Retrieve the key from the hash and convert it to a number
    key = +window.location.hash.match(/\d/g).join('');
    // Perform the transaction
    transaction = dbobject.transaction(['tasks'], 'readwrite');
    objectstore = transaction.objectStore('tasks');
    request = objectstore.delete(key);
    // Recreate the task list display
    transaction.oncomplete = function (evt) {
      tbody.innerHTML = '';
      displaytasks(dbobject);
    };
    transaction.onerror = errorhandler;
  }
};

لقد عرفنا هنا المعالج oncomplete للكائن transaction وبما أن delete لن يقدم نتيجة بعد.

حذف الصفوف و مولّد المفاتيح

كما في قواعد البيانات التقليدية فإن حذف الصف لا يعيد القيمة إلى مولد المفاتيح . يمكن أن ترى في الصورة 3 أن لدينا 16 صفًا في قاعدة البيانات. كما أن أعلى قيمة للمفتاح هي 30.

الصورة 3: لم يتم إعادة ضبط المفتاح بعد حذف الصف من قاعدة البيانات

من الممكن إعادة استخدام المفتاح للصف المحذوف. حيث يمكنك تخطي المفتاح المطلوب كما في المناقشة الثانية للعمليات add أو put.

استخدام المؤشر لاستعادة صفوف متعددة

تعتبر عملية استرجاع مجموعة من الصفوف مختلفة نوعاً ما. لهذا نحتاج إلى استخدام المؤشر. المؤشرات هي تقنيات عابرة تستخدم لإعادة مجموعة صفوف في قاعدة البيانات (المؤشرات معرفة تلقائياً من IndexedDB). يُبقي المؤشر امتداد العمليات المتزامنة حيث يتحرك بترتيب تصاعدي أو تنازلي معتمداً على وجهة فتح المؤشر. تستخدم المؤشرات عملية الدوران while.

دعونا نلقي نظرة على كيفية استعادة مجموعة من النتائج مع المؤشر:

var displaytasks = function (database) {

  var transaction, objectstore, request;
  transaction = dbobject.transaction(['tasks'], 'readonly');
  objectstore = transaction.objectStore('tasks');
  request = objectstore.openCursor(IDBKeyRange.lowerBound(0), 'next');

  request.onsuccess = function (successevent) {
    var task, tbody = document.querySelector('#list tbody');
    if (request.result) {
      task = buildtask(request.result);
      tbody.appendChild(task);
      cursor.continue(); // advance to the next result
    }
  }
}

مجدداً، قمنا بإنشاء عمليات للكائن واختيار تخزين الكائن. مافعلناه مختلفًا نوعاً ما لكن لفتح كائن المؤشر يمكن استخدام الطريقة openCursor.

تقبل الطريقة openCursor مُعاملين، كلاهما اختياري:

  • المدى range: يجب أن يكون إما مفتاح أو مفتاح مدى.
  • الاتجاه direction: يجب أن يكون واحداً من الطرق 'next' أو 'prev' أو 'nextunique' أو 'prevunique'.

إنشاء مدى

لإنشاء مفتاح مدى نحتاج إلى استخدام الواجهة IDBKeyRange وجميع الطرق التي فيها ثابتة.

  • IDBKeyRange.lowerBound: يحدد مفتاح منخفض فقط.
  • IDBKeyRange.upperbound: يحدد مفتاح مرتفع فقط.
  • IDBKeyRange.bound: يحدد مفتاح منخفض و مرتفع.
  • IDBKeyRange.only: يوافق على قيمة واحدة مشابهة تماماً لعملية get.

لقد قمنا بتعيين أصغر قيمة مقيدة وهي الصفر باستخدام IDBKeyRange.lowerBound ولم نقم بتعيين القيمة العليا. كل صف في قيمة المفتاح والتي هي اكبر من صفر سيتم إرجاعها وذلك في كل صف في مخزن الكائن tasks ويتم ارجاع الأقدم أولاً.

يعيد IDBKeyRange.upperBound جميع الكائنات مع قيمها والتي هي أقل مما هي عليه في المُعامل. على سبيل المثال يقوم  (IDBKeyRange.upperBound(20 بإعادة جميع الكائنات التي تحتوي على قيمة 20 أو أقل.

يعيد IDBKeyRange.bound جميع قيم الكائنات المحددة المدى من أصغر تحديد الى الأعلى تحديداً. ولإعادة الصفوف بين 11 و20 على سبيل المثال نستخدم (IDBKeyRange.bound(11,20.

لاتوجد أي طريقة تعيد عدد النتائج. و عوضاً عن ذلك تعيد المفاتيج ضمن المدى المحدد. دعونا نقول على سبيل المثال بأن مفاتيح تخزين الكائن هي 1, 2, 4, 8, 9, 11, 15, 16, 20, 21, 22,23. قد قمنا بحذف بعض الصفوف لذلك هناك فجوة في تسلسل المفاتيح.

  • يعيد (IDBKeyRange.lowerBound(0 جميع الكائنات.
  • يعيد (IDBKeyRange.lowerBound(10 جميع الكائنات للمفاتيح 11, 15, 16, 20, 21, 22 , 23.
  • لا يعيد (IDBKeyRange.upperBound(0 أي كائن.
  • يعيد (IDBKeyRange.bound(0,20 جميع الكائنات ماعدا 21, 22, 23.

بما أن مدى المفاتيح مقيد ضمن النتيجة فإنه من الممكن إظهارهم من خلال مناقشة جديدة باستخدام open. يجب  أن تكون القيمة منطقية كما أن القيمة الافتراضية هي false. لتخطي أول صف من النتيجة التي قمنا بها على سبيل المثال يمكن استخدام (IDBKeyRange.lowerBound(0, true.

يختلف الأمر IDBKeyRange.bound عن الآخرين إذ أنه يقبل مُعاملين إضافيين هُما: lowerOpen  وupperOpen. إذا أردنا عرض أول وآخر نتيجة من IDBKeyRange.bound يجب علينا جعل القيمة صحيحة true مرتين: (IDBKeyRange.bound(0, 10, true, true.

اختيار وجهة المؤشر

يُشير المُعامل الثاني openCursor إلى وجهة المؤشر وأين يجب أن يتجه. باستخدام next يعني بأن الصفوف سيتم ترتيبها حسب المفتاح بترتيب تصاعدي. أيضاً يمكننا إنشاء مفتاح مزدوج  باستخدام nextunique وprevunique وتعتبر هذه الطريقة مفيدة جداً عند العمل مع الخاصيات المفهرسة.

إضافة علامة

قمنا سابقاً باستعادة المُدخلات من خلال المفتاح ومدى المفتاح. ولكن لأجل عملية قائمة المهام يجب استعادة وترتيب الصفوف من خلال اسم المهمة والأولوية والتاريخ المستحق والحالة.

ملاحظة: لاتدعم IndexedDBShim فتح أو فهرسة المؤشرات. ستحتاج إلى وسلية أخرى لفعل ذلك.

فكر بالعلامات المفهرسة بطريقة سريعة ورتّب صفوفك في قاعدة البيانات. تسمح لك العلامات بمشاهدة الكائنات من خلال خصائصهم فضلاً عن مفاتيحهم.

لإضافة علامة لمخزن الكائن سنتحتاج الى استخدام طريقة createIndex. ولأن إضافة العلامة تقوم بتغيير البنية سنحتاج إلى فعل ذلك يدوياً من خلال الحدث versionchange واستخدام onupgradeneeded أيضا. دعنا نقوم بتحديث الدالّة onupgradeneeded:

idb.onupgradeneeded = function (evt) {
  var tasks, transaction;
  dbobject = evt.target.result;

  if (evt.oldVersion < 1) {
    tasks = dbobject.createObjectStore('tasks', {autoIncrement: true});
    // Create indexes on the object store.
    transaction = evt.target.transaction.objectStore('tasks');
    transaction.createIndex('by_task', 'task');
    transaction.createIndex('priority', 'priority');
    transaction.createIndex('status', 'status');
    transaction.createIndex('due', 'due');
    transaction.createIndex('start', 'start');
  }
};

تقبل الطريقة createIndex ثلاث مُعامِلات:

  • Name (مطلوب): اسم العلامة التي تريد اضافتها.
  • keyPath (مطلوب): خاصية الكائن.
  • optionalParameters: وهو قاموس يحتوي على الضبط يتكون من unique و/أو multiEntry للعلامة.

أضفنا علامة مفهرسة للحقول task, priority, status, start, due. يمكن أن تشارك هذه العلامات نفس الاسم و الخاصية التي تحملها.

يتم إدخال مخزن العلامات فقط للكائنات التي تحتوي خاصيات الفهرسة.

الصورة 4: انظر إلى علامة by_task في الجدول في متصفح Opera

عند تغيير خاصية الصف عندها ستصبح تلك الخاصيات أكثر مرونة في الجدول، دعنا نلقي نظرة على الصفوف الأساسية في الفهرس. سنقوم بتحديث الدالّة displaytasks من الأعلى.

استعادة الصفوف من خلال علاماتهم

سنقوم بفتح المؤشر لمخزن الكائن من خلال النسخة السابقة من الدالّة displaytasks. وهنا سنحتاج لإضافة سطر جديد والذي سيعيد الفهرس by_task. ثم سنقوم باستدعاء openCursor كما في الشِفرة البرمجية التالية:

var displaytasks = function (database) {

  var transaction, objectstore, request, index;
  transaction = dbobject.transaction(['tasks'], 'readonly');
  objectstore = transaction.objectStore('tasks');
  // New line to select the index
  index = objectstore.index('by_task');
  // Our request opens a cursor on the index,
  // rather than the object store.
  request = index.openCursor(IDBKeyRange.lowerBound(0), 'next');
  request.onsuccess = function (successevent) {
    var task, tbody = document.querySelector('#list tbody');
    if (request.result) {
      task = buildtask(request.result);
      tbody.appendChild(task);
      cursor.continue();
    }
}

ترتّب أيضاً العلامات القيم لكل خاصية. ستعيد الدالّة displaytasks قائمة المهام مُرتبة أبجدياً.

محدودية العلامات

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

var searchhandler, search = document.getElementById('search');

searchhandler = function (evt) {
  evt.preventDefault();
  var transaction, objectstore, index, request;
  transaction = dbobject.transaction(['tasks'], 'readwrite');
  objectstore = transaction.objectStore('tasks');
  index = objectstore.index('by_task');
  request = index.openCursor(IDBKeyRange.lowerBound(0), 'next');
  request.onsuccess = function (successevent) {
    var reg, cursor, task;
    reg = new RegExp(evt.target.find.value, "i");
    cursor = request.result;
    if (cursor !== null) {
      if (reg.test(cursor.value.task)) {
        task = buildtask(cursor);
      }
      cursor.continue();
    }
  }
}

search.addEventListener('submit', searchhandler);

الصورة 5 : فلترة المهام من خلال استخدام تعبير بسيط للبحث

تحتوي التعابير التي تقوم بالبحث على محدوديات. وبكل الأحوال البحث عن كلمة cafe لن يُطابق الكلمة café لأن الحرفان الأخيران في كلا الكلمتين غير متشابهين. و هنا يمكنك استخدام تقنية بسيطة لتخطي هذه المشكلة باستخدام تعبير نمطي ويقوم بالبحث تلقائياً عن caf فقط.

الخاتمة

تقدم IndexedDB قدرة قاعدة البيانات الأساسية للمتصفح، كما تجعل بناء تطبيقات ويب تعمل في حالة الإتصال (online) وفي حالة الانفصال (offline) مُمكنا. يمكنك الاطلاع أكثر على هذا النوع من قواعد البيانات لأنه سيحسن كثيراً لديك فكرة عملهم.

في حال أردت تعلم أكثر حول المُدخلات والمخرجات لقاعدة البيانات IndexedDB. يمكنك قراءة تخصيصات IndexedDB. و هذا غير كافي للقراءة لأنه فقط مرجع تعريفي. يقدم أيضًا مطوروا شركة Mozilla بعض الشروحات حول الفكرة من وراء IndexedDB في حال كانت الفكرة من ورائها لا تزال مُبهمة لديك.

 

ترجمة -وبتصرّف- للمقال: An Introduction to IndexedDB لصاحبته: Tiffany Brown.





تفاعل الأعضاء


Guest Mohamed BinOthman

نشر

( تعني بأن IndexedDB لاتحجب واجهة المستخدم. حيث أن العمليات تجري على نحوٍ لاحق، أي أنها ليست فورية ) حاولت ان استوعب هذه الجملة لكن لم افهم ثم انتقلت الى اسفل المقالة لاجدها مترجمة مثلمها توقعت فالموضوع ترجمة حرفية.

شارك هذا التعليق


رابط هذا التعليق
شارك على الشبكات الإجتماعية

( تعني بأن IndexedDB لاتحجب واجهة المستخدم. حيث أن العمليات تجري على نحوٍ لاحق، أي أنها ليست فورية ) حاولت ان استوعب هذه الجملة لكن لم افهم ثم انتقلت الى اسفل المقالة لاجدها مترجمة مثلمها توقعت فالموضوع ترجمة حرفية.

​أخي ممكن أن هناك تقديم وتأخير لكن الجملة واضحة

شارك هذا التعليق


رابط هذا التعليق
شارك على الشبكات الإجتماعية


يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن