DEV Community

HarmonyOS
HarmonyOS

Posted on

How can relational functional tables be created with RDB Store?

Read the original article:How can relational functional tables be created with RDB Store?

Requirement Description

When developing HarmonyOS applications, developers frequently require persistent data storage mechanisms that allow applications to retain state and manage user data across sessions. For lightweight storage of simple key-value pairs, Shared Preferences provides a convenient solution. However, when applications need to handle structured, complex, or relational data, developers often turn to RDB Store. Both technologies complement each other as local database services, enabling developers to select the most suitable option based on their use cases.

Background Knowledge

RDB Store is an SQL-based local database technology integrated into HarmonyOS. It provides developers with the ability to define schemas, create tables, and manage structured datasets with powerful querying capabilities. Unlike Shared Preferences, which is designed for simple data storage, RDB Store supports the creation of multiple tables that can be related to each other through primary keys, foreign keys, and joins, enabling relational database design within an application.

This relational model allows developers to:

  • Define entities such as users, products, or transactions as separate tables.
  • Establish relationships between these entities (e.g., one-to-many or many-to-many).
  • Perform advanced operations locally, such as filtering, aggregating, and joining data across related tables.

By leveraging RDB Store, HarmonyOS applications can achieve robust local data management, improve data consistency, and deliver a richer offline experience for end users.

Implementation Steps

For example, consider an application that allows users to organize their contacts into custom groups they create, and provides functionality to manage both the groups and the associated contacts. Accordingly, the implementation steps we need to follow are as follows:

  1. Import the RDB Store into the project
  2. Create three interrelated tables named contacts, groups, and contact_groups
  3. Implement the corresponding CRUD operations

Code Snippet / Configuration

1.First, in order to ensure that the relevant database and tables are created as soon as the application is launched, we perform the necessary operations within the EntryAbility.

   import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
   import { hilog } from '@kit.PerformanceAnalysisKit';
   import { window } from '@kit.ArkUI';
   import relationalStore from '@ohos.data.relationalStore';

   export let rdbStore: relationalStore.RdbStore;
   interface StoreConfig {
     name: string;
     securityLevel: relationalStore.SecurityLevel;
     encrypt?: boolean;
   }

   const DOMAIN = 0x0000;

   export default class EntryAbility extends UIAbility {
     public async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
       this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
       hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');

       const CONTACTSTREE_CONFIG: StoreConfig = {
         name: 'ContactsTreeDB.db',
         securityLevel: relationalStore.SecurityLevel.S1,
         encrypt: false
       }

       const SQL_CREATE_CONTACTS_TABLE = `
       CREATE TABLE IF NOT EXISTS contacts (
         id INTEGER PRIMARY KEY,
         key TEXT NOT NULL,
         name TEXT NOT NULL,
         phone TEXT NOT NULL
       )
       `;

       const SQL_CREATE_GROUPS_TABLE = `
       CREATE TABLE IF NOT EXISTS groups (
         id INTEGER PRIMARY KEY AUTOINCREMENT,
         name TEXT NOT NULL UNIQUE
       )
       `;

       const SQL_CREATE_CONTACT_GROUPS_TABLE = `
       CREATE TABLE IF NOT EXISTS contact_groups (
         id INTEGER PRIMARY KEY AUTOINCREMENT,
         contact_id INTEGER NOT NULL,
         group_id INTEGER NOT NULL,
         FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE,
         FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE,
         UNIQUE(contact_id, group_id)
       )
       `;

       try {
         rdbStore = await relationalStore.getRdbStore(this.context, CONTACTSTREE_CONFIG);
         await rdbStore.executeSql(SQL_CREATE_CONTACTS_TABLE);
         await rdbStore.executeSql(SQL_CREATE_GROUPS_TABLE);
         await rdbStore.executeSql(SQL_CREATE_CONTACT_GROUPS_TABLE);
       } catch (error) {
         hilog.error(DOMAIN, 'DataAbility', 'RDB error: %{public}s', JSON.stringify(error));
       }

     }

     onDestroy(): void {
       hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
     }

     onWindowStageCreate(windowStage: window.WindowStage): void {
       // Main window is created, set main page for this ability
       hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

       windowStage.loadContent('pages/Index', (err) => {
         if (err.code) {
           hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
           return;
         }
         hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
       });
     }

     onWindowStageDestroy(): void {
       // Main window is destroyed, release UI related resources
       hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
     }

     onForeground(): void {
       // Ability has brought to foreground
       hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
     }

     onBackground(): void {
       // Ability has back to background
       hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
     }
   }
Enter fullscreen mode Exit fullscreen mode

2.Secondly, we create a class called ContactUtils that handles CRUD operations on the contacts we have created. In the example, we also use @kit.ContactsKit for handling contact operations.

   import { contact } from '@kit.ContactsKit';
   import { common } from '@kit.AbilityKit';
   import { BusinessError } from '@kit.BasicServicesKit';
   import { promptAction } from '@kit.ArkUI';
   import relationalStore from '@ohos.data.relationalStore';
   import {Contact} from '../types/Contact';
   import {rdbStore} from '../entryability/EntryAbility'


   export async function addContact(context: common.UIAbilityContext, fullName: string, phoneNumber: string) {
     try {

       let contactInfo: contact.Contact = {
         name: { fullName: fullName },
         phoneNumbers: [{ phoneNumber: phoneNumber }]
       }

       contact.addContact(context, contactInfo, async (err: BusinessError, data) => {
         if (err) {
           console.error('Adding Error:', err.code);
           promptAction.showToast({
             message: 'Adding Error: ' + err.code,
             duration: 3000
           });
           return;
         }

         let valueBucket: relationalStore.ValuesBucket = {
           'id': data,
           'key': data,
           'name': fullName,
           'phone': phoneNumber
         };
         await rdbStore.insert('contacts', valueBucket)
         promptAction.showToast({
           message: 'Contact added successfully',
           duration: 3000
         });
       })

     } catch (error) {
       console.error('Unexpected error in addContact:', error);
       promptAction.showToast({
         message: 'Unexpected error occurred while adding contact',
         duration: 3000
       });
     }
   }

   export async function deleteContact(context: common.UIAbilityContext, key: string) {
     try {
       contact.deleteContact(context, key);
       promptAction.showToast({
         message: 'Contact deleted successfully',
         duration: 3000
       });
     } catch (error) {
       console.error('Unexpected error in deleteContact:', error);
       promptAction.showToast({
         message: 'Unexpected error occurred while deleting contact',
         duration: 3000
       });
     }
   }

   export async function updateContact(context: common.UIAbilityContext, id: number, fullName: string, phoneNumber: string) {
     try {
       let newContactInfo: contact.Contact = {
         name: { fullName: fullName },
         phoneNumbers: [{ phoneNumber: phoneNumber }],
         id: id
       }

       contact.updateContact(context, newContactInfo);
       let predicates = new relationalStore.RdbPredicates('contacts');
       predicates.equalTo('id', id);
       let valueBucket: relationalStore.ValuesBucket = {
         'name': fullName,
         'phone': phoneNumber
       };
       await rdbStore.update(valueBucket, predicates);
       promptAction.showToast({
         message: 'Contact updated successfully',
         duration: 3000
       });
     } catch (error) {
       console.error('Unexpected error in updateContact:', error);
       promptAction.showToast({
         message: 'Unexpected error occurred while updating contact',
         duration: 3000
       });
     }
   }

   export async function getContacts(context: common.UIAbilityContext): Promise<Contact[]> {
     return new Promise((resolve, reject) => {
       try {
         contact.queryContacts(context, (err: BusinessError, data: contact.Contact[]) => {
           if (err) {
             console.error('Query contacts error:', err.code);
             reject(err);
             return;
           }

           if (!data || data.length === 0) {
             resolve([]);
             return;
           }

           const contactsList: Contact[] = [];

           data.forEach((contactItem) => {
             try {
               contactsList.push({
                 id: contactItem.id || 0,
                 key: contactItem.key || '',
                 name: contactItem.name?.fullName || 'Unknown Name',
                 phone: contactItem.phoneNumbers?.[0]?.phoneNumber || 'No Phone'
               });
             } catch (itemError) {
               console.warn('Skipping invalid contact item:', itemError);
             }
           });

           resolve(contactsList);

         })
       } catch (error) {
         console.error('Unexpected error in getContacts:', error);
         promptAction.showToast({
           message: 'Error: ' + (error as BusinessError).code,
           duration: 3000
         });
         reject(error);
       }
     })
   }
Enter fullscreen mode Exit fullscreen mode

3.Finally, to perform operations on groups, we need to create a class called GroupsUtils.

   import { BusinessError } from '@kit.BasicServicesKit';
   import { promptAction } from '@kit.ArkUI';
   import relationalStore from '@ohos.data.relationalStore';
   import {Group} from '../types/Group';
   import { Contact } from '../types/Contact';
   import {rdbStore} from '../entryability/EntryAbility'


   export async function getGroups(): Promise<Group[]> {
     const groups: Group[] = [];

     try {
       const predicates = new relationalStore.RdbPredicates('groups');
       const resultSet = await rdbStore.query(predicates, ['id', 'name']);

       while (resultSet.goToNextRow()) {
         groups.push({
           id: resultSet.getLong(resultSet.getColumnIndex('id')),
           name: resultSet.getString(resultSet.getColumnIndex('name'))
         });
       }
       resultSet.close();

     } catch (error) {
       console.error(`Get groups failed, code: ${error.code}, message: ${error.message}`);
     }
     return groups;
   }

   export async function addGroup(name: string){
     if (!name || name.trim().length === 0) {
       throw new Error('Name cannot be empty');
     }

     try {
       const valueBucket: relationalStore.ValuesBucket = {
         'name': name.trim()
       };
       await rdbStore.insert('groups', valueBucket);
     } catch (error) {
       throw new Error(error);
     }
   }

   export async function updateGroup(groupId: number, newGroupName: string) {
     if (!groupId || groupId <= 0) {
       throw new Error('Invalid Id');
     }

     if (!newGroupName || newGroupName.trim().length === 0) {
       throw new Error('Group name cannot be empty');
     }

     const trimmedName = newGroupName.trim();

     try {

       const predicatesUpdate = new relationalStore.RdbPredicates('groups');
       predicatesUpdate.equalTo('id', groupId);

       const valueBucket: relationalStore.ValuesBucket = {
         'name': trimmedName
       };

       await rdbStore.update(valueBucket, predicatesUpdate);
     } catch(error) {
       throw new Error(error);
     }
   }

   export async function deleteGroup(groupId: number) {
     if (!groupId || groupId <= 0) {
       throw new Error('Invalid group ID');
     }

     try {
       const predicates = new relationalStore.RdbPredicates('groups');
       predicates.equalTo('id', groupId);
       await rdbStore.delete(predicates);
     } catch (error) {
       throw new Error(error);
     }
   }

   export async function addContactToGroup(contactId: number, groupId: number) {

     if (!contactId || contactId <= 0) {
       throw new Error('Invalid contact ID');
     }
     if (!groupId || groupId <= 0) {
       throw new Error('Invalid group ID');
     }

     try {

       const valueBucket: relationalStore.ValuesBucket = {
         'contact_id': contactId,
         'group_id': groupId
       };

       await rdbStore.insert('contact_groups', valueBucket);

     } catch (error) {
       const err = error as BusinessError;

       if (err.code === 2067) {
         throw new Error('This contact is already in the group!');
       }

       if (err.code === 2069) {
         throw new Error('Contact or group not found!');
       }

       console.error(`Add contact to group failed, code: ${err.code}, message: ${err.message}`);
       throw new Error(`Failed to add contact to group: ${err.message}`);
     }
   }

   export async function removeContactFromGroup(contactId: number, groupId: number) {

     if (!contactId || contactId <= 0) {
       throw new Error('Invalid contact ID');
     }
     if (!groupId || groupId <= 0) {
       throw new Error('Invalid group ID');
     }

     try {
       const predicates = new relationalStore.RdbPredicates('contact_groups');
       predicates.equalTo('contact_id', contactId);
       predicates.equalTo('group_id', groupId);

       const affectedRows = await rdbStore.delete(predicates);

       if (affectedRows === 0) {
         throw new Error('Contact is not in this group!');
       }

     } catch (error) {
       const err = error as BusinessError;
       console.error(`Remove contact from group failed, code: ${err.code}, message: ${err.message}`);
       throw new Error(`Failed to remove contact from group: ${err.message}`);
     }
   }

   export async function getContactsInGroup(groupId: number): Promise<Contact[]> {
     const contacts: Contact[] = [];
     let resultSet: relationalStore.ResultSet | null = null;

     try {
       const query = `
         SELECT c.*
         FROM contacts c
         INNER JOIN contact_groups cg ON c.id = cg.contact_id
         WHERE cg.group_id = ?
       `;

       resultSet = await rdbStore.querySql(query, [groupId]) as relationalStore.ResultSet;

       while (resultSet.goToNextRow()) {
         contacts.push({
           id: resultSet.getLong(resultSet.getColumnIndex('id')),
           key: resultSet.getString(resultSet.getColumnIndex('key')),
           name: resultSet.getString(resultSet.getColumnIndex('name')),
           phone: resultSet.getString(resultSet.getColumnIndex('phone'))
         });
       }

       return contacts;

     } catch (error) {
       const err = error as BusinessError;
       console.error(`Get contacts in group failed, code: ${err.code}, message: ${err.message}`);
       throw new Error(`Failed to get contacts in group: ${err.message}`);
     } finally {
       if (resultSet !== null) {
         resultSet.close();
       }
     }
   }

Enter fullscreen mode Exit fullscreen mode

Test Results

The following screenshots illustrate the results of the database operations that have been performed.

image.pngimage.pngimage.pngimage.png

Limitations or Considerations

The technologies used in this example are compatible with real HarmonyOS mobile and wearable devices.

Related Documents or Links

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/data-persistence-by-rdb-store

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/contacts-kit

Written by Mehmet Algul

Top comments (0)