Working with SQLite database in Android (without auxiliary libraries)

Working with SQLite database in Android (without auxiliary libraries)

Contract class

The Contract class has only the role of being a container for constants that define names for URIs, tables, and columns. This class allows us to use the same constants in all other classes, which makes our job much easier when working with the database because it allows us to change the column name or similar in one place.

1) Creating classes and constructors

The first thing is to create a class and its empty constructor. Since this is just a container for constants that are static, there is no need to instantiate this class, and to prevent someone from accidentally instantiating the class, we define the constructor as private:

2) Creating a subclass that represents the table

It is recommended that for each table within the database we create a new static subclass that implements the BaseColumns interface. Within this class we define table-related constants such as table name and column names:

NOTE:
It is not necessary to assign constants for columns named _ID and _COUNT because we get them by implementing the “BaseColumns” interface. See more about this interface here.

SQLiteOpenHelper class

For maximum control over local data and working with the SQLite database (for executing SQL requests…) it is good to create a class that extends the SQLiteOpenHelper class. The “SQLiteOpenHelper” class is, as its name suggests, a “helper” class that facilitates database creation and versioning.

sqlite open helper

Constructive method

As we mentioned our class extends the SQLiteOpenHelper.java class, so its constructor method generated by Android Studio looks like this:

As you can see, the constructor method generated in this way has quite a number of attributes, however, if we define constants for the name and version of the database, then the constructor method could look like in the following example:

Immediately after extending the class, we notice that our class must implement two methods: onCreate() and onUpgrade(). We will talk about their role later.

Instantiating a class using the “singleton pattern”

Since the database is used throughout the application (services, fragments…) for this reason, the recommended practice is to apply the so-called “Singleton Pattern” to instances of the SQLiteOpenHelper class to avoid memory leaks. The best solution is to make the database instance a single instance for the entire life cycle of the application.

The getInstance() static method ensures that there will be only one AppDBHelper instance and only one buyLista.db database at any given time. If the instance of the helper class is not initialized (“sInstance” object), one object will be created, and if it has already been created, then that existing object will simply be used.

Database reference

Getting the database that is created as soon as this helper class is instantiated is done by calling the getWriteableDatabase() method. Since the database connection is cached, it is not “expensive” to call this onemethod wherever we need it in the class.

Now that we have an instance of our database that inherits the properties of the SQLiteDatabase.java class, we can use all of its methods. You can view some of the frequently used methods here.

Defining the table within onCreate()

Within the onCreate() method, our database is defined. First, we write a “query” that creates a table, and then we execute it with the execSQL() method. The SQL query that creates the table looks like this:

And when we apply it to our example like this:

NOTE:
We could avoid repeating the DBContract.TaskTable sequence within the query by importing everything in the contract class:

After which we could remove the DBContract from the query.

Database versioning

As we already mentioned, the class that extends “SQLiteOpenHelper” is also used for database versioning. Database versioning is important because if at some point in the new version of our application we realize that we need another column, we won’t be able to do it without this.
We need to define within the onUpgrade() method what we want to be done if the table changes. This method will only be called if a database with the same DATABASE_NAME already exists, but the DATABASE_VERSION is different from the version of the database that exists on disk.

Example

In this example, the simplest implementation is shown, when in this case the old table is deleted and recreated:

NOTE:
In order to call the onUpgrade() method at all, it is necessary to change the base version in the code:

Query to the database – db.query()

One of the most important functions for which this helper method is intended is getting data from the database. We get data from the database with a standard SQL query using the method query() which returns a Cursor object. You can see more about the Cursor object and what it represents here.

Example

When we have data within the Cursor object, we need to go through it and fill the list, this is usually done in the form of one method, as in the following example:

Inserting data – db.insert()

The insert() method is used to insert data into the database. This method takes parameters: “tablename”, “nullColumnHack” and “ContentValues”. ContentValues is a class responsible for storing a key-value set of values ​​within an object. To store one set of dataits method (String key, TypeData value):

is used

Only when we have fields defined in one row and saved in the ContentValues object can we insert them into the database with the aforementioned insert() method:

Deleting a row from the table – db.delete()

To delete a table row, it is necessary to pass to the delete() method the name of the table and the WHERE clause on the basis of which it will find the desired row:

You can see the entire code of this class here, while the entire project can be found on the GitHub page “SQLdatabaseWithoutLibrary”.

When we have created this helper class and defined its methods for working with the database, in the MVVM architecture we access them from the Repository class while it further broadcasts changes to higher levels. See how the whole process looks in the article “Repository – ViewModel – View cycle”.

×

Cursor

Cursor is an interface that represents a two-dimensional table of some database. When we want to get data from database using query() method (and within it the SELECT command), the database returns a Cursor object, which is responsible for accepting and storing the received data.
The pointer always points to the 0th location at the beginning, and the first data is actually stored in the first location (the zero location also exists when the Cursor is empty). For this reason, when we want to retrieve data from the cursor, we must first go to the first record. That’s why we have to use it
moveToFirst() method that moves the cursor pointer to the first place. After that, we can only access the data present in the first record. In addition to this method, there are the following methods for iterating through the Cursor object:

  • moveToLast()
  • moveToNext()
  • moveToPrevious()
  • moveToPosition(position)

To get the total number of elements of the resulting query, we will use the getCount() method, while we use the isAfterLast() method to check whether the end of the query result has been reached.
The cursor also provides the so-called getter methods (eg getLong(columnIndex), getInt(columnIndex) …) to access the data of a specific column. The necessary parameter for this method is “columnIndex” and we get it with the method getColumnIndex().

Example

While “passing” through the cursor rows, we get the field value in a certain row from the desired column as follows:

We use the column index to get data from that column (eg string) with the method getString():

Example

The entire iteration through the Cursor object can look like this:

×

nullColumnHack

This parameter is used to tell android what to do in case ContentValues is empty. This is important because in SQL the command:

is used

When an empty ContentValues is passed to the base, it doesn’t know what to put in the “value” slot (and it can’t be empty). This “nullColumnHack” parameter just solves it, it defines what will be put in the base field to which nothing is passed (most often null is put).

×

Methods of the SQLiteDatabase class
  • beginTransaction() – starting the transaction in EXCLUSIVE mode when we want to write to the database.
  • beginTransactionNonExclusive()() – starting a transaction in IMMEDIATE mode.
  • endTransaction() – stop the transaction
  • execSQL(String sql) – execute a single SQL statement that is NOT SELECT or any other SQL expression that returns data.
  • getVersion() – returns the database version.
  • insert(String table, String nullColumnHack, ContentValues values) – Inserts a new member/row into the table
  • insertOrThrow(String table, String nullColumnHack, ContentValues values) – Inserts a new member/row into the table but throws an exception if the transaction is unsuccessful. This method will return -1 if the code executed correctly and throw an error if it did not.
  • query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) – Executes the passed SQL query and returns a Cursor, has protection against SQL injections.
  • rawQuery(String sql, String[] selectionArgs) – Executes the passed SQL query and returns a Cursor. It has no protection and limitations, more flexible so more complex queries are used.
  • replace(String table, String nullColumnHack, ContentValues initialValues) – Replaces a row in the table with another
  • replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) – Replaces a row in the table with another, but throws an exception if the transaction is unsuccessful.
  • setForeignKeyConstraintsEnabled(boolean enable) – Sets constraints depending on whether ForeignKey is used.
  • setVersion(int version) – Sets the database version programmed.
  • update(String table, ContentValues values, String whereClause, String[] whereArgs) – Updates the table member/row with the newdata.

×

SQL database locking levels

SQL has the following database locking levels:

  • UNLOCKED: Meaning: default inactive lock.
  • SHARED: Which means: “Now I’m reading the data now, don’t write (if it has to be written to the database it has to wait)”
  • RESERVED: Meaning: “I plan to write soon.” Only one connection can obtain a RESERVED lock, all other write attempts must wait their turn.”
  • PENDING: Which means: “I’ll write as soon as everyone else stops doing things.”
  • EXCLUSIVE: Meaning: “I’m writing NOW, go away!” Everything stops while the DB is updated.
Types of transactions depending on the lock

Depending on these lock levels there are three types of transactions:

  • DEFERRED : Locking and unlocking of each SQL operation is performed automatically. The philosophy here is Just-In-Time. (This is the default when no actual mode is specified.)
  • IMMEDIATE : Immediately try to obtain and hold RESERVED locks on all databases opened by this connection. This currently blocks all other writers for the duration of this transaction. BEGIN IMMEDIATE TRANSACTION will block or fail if another connection has a RESERVED or EXCLUSIVE lock on any of this connection’s open DBs.
  • EXCLUSIVE : Immediately initiates an EXCLUSIVE lock on all databases opened by this connection. This currently blocks all other connections for the duration of this transaction. BEGIN EXCLUSIVE TRANSACTION will block or fail if another connection has any locks on any of this connection’s open DBs.

×

×

BaseColumns interface

This interface provides the two most commonly used columns within dashboards: _ID and _COUNT. Its code would basically look like this:

We can also define our own constants for id without using this particular interface, but methods like CursorAdapter.java require a constant like “_ID” so it’s good to use the provided interface. Another example is that the ListView adapter uses the _ID column to give you the unique ID of the list item being clicked within OnItemClickListener.onItemClick(), without having to explicitly state your column ID each time.
Using common names allows the Android platform (and developers too) to refer to any data item as a unit, regardless of its overall structure(ie Other, non-ID columns). Defining constants for commonly used strings in an interface / class avoids repetition and typos throughout the code.