One, a brief introduction

As we all know, the database used by mobile terminal (whether Android or iOS) is Sqlite. This kind of small database is very suitable for mobile terminal to store a large amount of data, and it is basically the same as mysql. However, the API provided by the official is really not good in operation, you must master a certain degree of SQL statements, otherwise it will be difficult to control. Therefore, there are a lot of third-party database frameworks became popular, such as GreenDao, Litepal and so on. These ORM database framework, can help developers to save a lot of time to write database operation code, just to the object assignment operation, can be used on the database, convenient for us to develop more complex business logic. The theme of this article is to create a simple database framework using advanced techniques such as design patterns, generics, annotations, and reflection.

2. Common database operations

Database operations are nothing more than add, delete, change, and query (CRUD), and the Dao classes that typically operate on database tables are called Dao classes, so you can extract a common interface for these Dao classes:

public interface IBaseDao<M> {

    Long insert(M entity);

    Integer delete(M where);

    Integer update(M entitiy, M where);

    List<M> query(M where);

    List<M> query(M where, String orderBy);

    List<M> query(M where, String orderBy, Integer page, Integer pageCount);

}
Copy the code

The database framework we want to do is also an ORM framework, the presentation layer does not involve any SQL statements, the direct operation is data objects, but the specific data type is not clear in this interface, so we use generics to represent.

Dao factory

A program that usually only a database, a database contains multiple tables, such as user table, access tables, and so on, this means that the project, can have multiple Dao class, because the database is nothing but the CRUD operation, and so their structure can be determined, the same only specific operation tables and fields of different (i.e., data types), so, A generic + factory is the best way to produce these Dao classes. The Dao class factory code is posted below, and then analyzed one by one:

public class BaseDaoFactory { private static String mDbPath; private SQLiteDatabase mDatabase; private static class Instance { public static BaseDaoFactory INSTANCE = new BaseDaoFactory(); } public static BaseDaoFactory getInstance() { return Instance.INSTANCE; } public static void init(String dbPath) {mDbPath = dbPath; } public BaseDaoFactory() {if (textutils.isempty (mDbPath)) {throw new RuntimeException(" Before using BaseDaoFactory, Call basedaoFactory.init () to initialize the database path. ); } / / open the database, database objects mDatabase = SQLiteDatabase. OpenOrCreateDatabase (mDbPath, null); } public <T extends BaseDao<M>, M> T getDataHelper(Class<T> clazz, Class<M> entity) { T baseDao = null; try { baseDao = clazz.newInstance(); baseDao.init(mDatabase, entity); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return baseDao; }}Copy the code

1. Database location

With SQLiteDatabase, you can create or open a database anywhere. This has the following benefits: If you use SQLiteOpenHelper to create a database, the database file will be created in the /data/data/ package name /databases directory by default. When the application is deleted, the database will be deleted. You can use previous database information, such as bookmark local recovery in UC Browser.

Public static void init(String dbPath) {mDbPath = dbPath; } public BaseDaoFactory() {if (textutils.isempty (mDbPath)) {throw new RuntimeException(" Before using BaseDaoFactory, Call basedaoFactory.init () to initialize the database path. ); } / / open the database, database objects mDatabase = SQLiteDatabase. OpenOrCreateDatabase (mDbPath, null); }Copy the code

This framework allows developers to customize the location of the database. Since this path is used in the constructor to create the database, it is statically assigned to (mDbPath) before the Dao factory is instantiated. This approach is common in the source code of many third-party frameworks. It is commonly used in custom applications to assign values to variables required by the framework by calling the framework init() method.

2. Factory single case

In this framework, only one Dao class factory is enough, so we need to use the singleton pattern. Common singleton patterns are hungry and lazy. Here we choose the static internal class singleton pattern, why?

1) Hungry

The hungry type when loading a class has been initialized, regardless of whether or not to use in the project, will take up memory, although the high efficiency, but developing generally don’t have to.

public class BaseDaoFactory { private static BaseDaoFactory Instance = new BaseDaoFactory(); public static BaseDaoFactory getInstance() { return Instance; }}Copy the code

2) slacker style

Lazy style although solved the disadvantages of hungry style, the implementation of the call to create a singleton, but the thread is not safe, of course we can use a double detection mechanism to solve the problem, but it also reduces the efficiency (at least the first initialization needs synchronization, reduced efficiency), is the most common singleton implementation in development.

public class BaseDaoFactory { private static BaseDaoFactory mInstance; public static BaseDaoFactory getInstance() { if (mInstance == null) { synchronized (BaseDaoFactory.class) { if (mInstance == null) { mInstance = new BaseDaoFactory(); } } } return mInstance; }}Copy the code

3) Static inner class singleton

Static inner class singletons combine the advantages of the previous two, namely, the singletons are created on invocation, which is efficient and has no thread-safety issues. The static inner class Instance is loaded only when the factory singleton is created using the getInstance() method, and the inner class attribute Instance is initialized, that is, the external BaseDaoFactory object (DaoFactory) is initialized, because the static inner class is loaded only once. So the INSTANCE object will only be created once.

private static class Instance { public static BaseDaoFactory INSTANCE = new BaseDaoFactory(); } public static BaseDaoFactory getInstance() { return Instance.INSTANCE; } // In the singleton pattern, constructors are generally privatized to ensure that they are not initialized externally. private BaseDaoFactory() { ... }Copy the code

Create Dao classes

The Dao factory provides a public method for outsiders to get the required Dao classes by passing in the classes corresponding to the concrete Dao class and the data entity class.

public <T extends BaseDao<M>, M> T getDataHelper(Class<T> clazz, Class<M> entity) {
    T baseDao = null;
    try {
        baseDao = clazz.newInstance();
        baseDao.init(mDatabase, entity);
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return baseDao;
}
Copy the code

Perhaps the most confusing part of this code is the fact that <T extends BaseDao<M>, M> (regardless of the implementation in the method body) is just how a method declares generics. As we know, it’s easy to declare generics on a class. We just add <T> after the class (whatever the letter is), as in:

Public class Person<M,T> {... }Copy the code

Method generics are declared between modifiers (public, etc.) and return values. For example:

public <T> void eat(){
    ...
}
Copy the code

The above code does not report an error, but it is meaningless because generics are not used. Generics declared on ordinary methods can be used as method parameter types and return value types, such as:

Public <T,M> T eat(T T,M M){ }Copy the code

It can be argued that the default generic represents Object (when encoded). Sometimes we need to be more precise about the type of a generic. This can be done through extends, for example:

public <T extends Person> void doSomething(T t){
    ...
}
Copy the code

The argument passed to doSomething() must be a subclass of Person (Boy or Girl), and the method declared in Person, such as eat(), can be called with t in the body of the method when coding. If extends is not used to specify the specific type of the generic t, then when coding, T is considered Object, and custom methods like eat() cannot be called. At this point, go back and look at the code above, should not be confused.

4. Database operation encapsulation

We have already extracted a common interface for the Dao class to standardize the basic operations of the Dao class. Moreover, we do not want to directly manipulate the database in the concrete Dao class, so we must have a layer in the middle to complete the database operations and encapsulate the operation methods. This layer is called BaseDao. Here are the methods that can be called from outside the class:

public abstract class BaseDao<M> implements IBaseDao<M> { protected boolean init(SQLiteDatabase database, Class<M> entity) { ... } @Override public Long insert(M entity) { ... } @Override public Integer delete(M where) { ... } @Override public Integer update(M entitiy, M where) { ... } @Override public List<M> query(M where) { ... } @Override public List<M> query(M where, String orderBy) { ... } @Override public List<M> query(M where, String orderBy, Integer page, Integer pageCount) { ... }}Copy the code

In framework design, it is important to remember that methods or attributes that do not need to be known by the outside world (presentation layer) should be privatized as much as possible, both for the safety of the framework and to avoid unnecessary conflicts in team development. Because BaseDao’s init() method is only called by DaoFactory and both are in the same package, the protected modifier is used.

1. Custom annotations: TbName and TbField

Before parsing BaseDao, there are two very important annotations — TbName and TbField. Almost all ORM database frameworks in the market use custom annotations to describe a data entity. For example, the class name of the User class corresponds to the table name, which may be User or t_user. Then developers can use the annotations provided by the framework (TbName) to customize the table name. The same is true for attribute names in a class that correspond to table field names, but the initialization of the table also needs to know the length of the table field, so table Field annotations (TbField) also have a length attribute.

@target (elementtype.type) @Retention(retentionPolicy.runtime) public @interface TbName {String value(); */ @target (elementtype.field) @Retention(retentionPolicy.runtime) public @interface TbField {String value(); int length(); }Copy the code

On the one hand, BaseDao interprets TbName and TbField at RUNTIME, so these two annotations must be visible at RUNTIME, i.e. the Retention value must be retentionPolicy.runtime. On the other hand, TbName is annotated on the class, so its Target value is elementType.type; TbField is annotated on a class property, so its Target value is elementType.field.

2. Initialize the Dao class

The Dao factory already uses the BaseDao init() method to do some general initialization of the Dao class. Here’s what it does:

/** * Initializes the table operation object, generally including: */ protected Boolean init(SQLiteDatabase database, Class<M> entity) {mDatabase = database; mEntityClass = entity; // The following operation must be based on the database already open premise if (! database.isOpen()) { return false; } // Get the table name TbName TbName = entity.getannotation (tbname.class); mTbName = tbName == null ? entity.getSimpleName() : tbName.value(); // get table mapping field if (! genFieldMap()) { return false; } // create database if (! createTable(database)) { return false; } return true; }Copy the code

In this init() method, you can see that the annotation TbName is first used by the framework. When the developer uses the annotation, the table name is the value in the TbName annotation, otherwise it is the class name.

Annotations are “static” and fixed when the class is loaded, meaning that they cannot be modified at run time (except by Javassist), so you can use the class object of the class via getAnnotation. Class. Annotation Properties () this method also gets the attribute value of the annotation.

1) Obtain the mapping between table fields and class fields

As we know, the attribute name of a class may be different from the field name of a table, and many subsequent operations in BaseDao will deal with both. Therefore, during the initialization of BaseDao, the relationship between the two will be saved using a Map for subsequent operations.

private boolean genFieldMap() { mFieldMap = new HashMap<>(); Field[] fields = mEntityClass.getFields(); If (fields = = null | | fields. The length = = 0) {the e (TAG, "get less than class field"); return false; } for (Field field : fields) { field.setAccessible(true); TbField tbField = field.getAnnotation(TbField.class); mFieldMap.put(tbField == null ? field.getName() : tbField.value(), field); } return true; }Copy the code

A few tips on reflection:

  • Field[] fields = mEntityClass.getFields(); // Get the public fields of the class, including the parent class.
  • Field[] fields = mEntityClass.getDeclaredFields(); // Get the fields declared in the class (whether public, protected, private), excluding the parent class.
  • field.setAccessible(true); // Make private or final properties accessible

The getFields() method is used because the data entity class may extend the parent class by inheritance, using the attribute values of the parent class, but the tradeoff is that the attributes in the entity class must be public.

2) Create tables

The initialization of the Dao class also includes the creation of tables. On the one hand, since the table cannot be recreated each time the Dao class is created and initialized, the if Not EXISTS keyword in SQL statements is used to avoid the problem of duplicate table creation. On the other hand, the SQL statement used to create a table is a template. When two different tables are created using SQL, the only difference is that the table name, field name, field type, and field length are different. Combined with the mapping between table fields and class fields (mFieldMap) obtained in the previous step, the code could be written as follows:

/** * create table (can be overridden by subclasses, CreateTable (SQLiteDatabase database) {StringBuilder sb = new StringBuilder(); for (Map.Entry<String, Field> entry : mFieldMap.entrySet()) { String columnName = entry.getKey(); Field field = entry.getValue(); TbField tbField = field.getAnnotation(TbField.class); int length = tbField == null ? 255 : tbField.length(); String type = ""; Class<? > fieldType = field.getType(); if (fieldType == String.class) { type = "varchar"; } else if (fieldType == int.class || fieldType == Integer.class) { type = "int"; } else if (fieldType == double.class || fieldType == Double.class) { type = "double"; } else if (fieldType == float.class || fieldType == Float.class) { type = "float"; } if (textutils.isempty (type)) {log.e (TAG, type.getClass().getName() + "unsupported field "); } else { sb.append(columnName + " " + type + "(" + length + "),"); } } sb.deleteCharAt(sb.lastIndexOf(",")); String s = sb.toString(); If (textutils.isempty (s)) {log. e(TAG, "not available "); return false; } String sql = "create table if not exists " + mTbName + " (" + s + ") "; Log.e(TAG, sql); database.execSQL(sql); return true; }Copy the code

At this point, initialization of the Dao class is done, and CRUD encapsulation is now in order.

3,

As we know, if you use the native SQLiteDatabase to insert data into a table, you wrap the data into a ContentValues object and then call its INSERT () method to perform the insert operation. So, now that we have a data entity, all we need to do is convert that data entity into a ContentValues object and use the INSERT () method of SQLiteDatabase to perform the insert. We can encapsulate SQLiteDatabase. The code is divided into three steps as follows:

  1. Convert an attribute in an object to a key-value pair called values.
  2. Convert the key-value pair values into ContentValues objects.
  3. Insert data using the INSERT () method of SQLiteDatabase.

In conjunction with mFieldMap, the code for table data insertion can be written as follows:

@Override public Long insert(M entity) { try { Map<String, String> values = getValues(entity); ContentValues cv = getContentValues(values); return mDatabase.insert(mTbName, null, cv); } catch (IllegalAccessException e) { e.printStackTrace(); } return 0L; } /** * Convert an attribute in an object to a key-value pair */ private Map<String, String> getValues(M entity) throws IllegalAccessException { Map<String, String> result = new HashMap<>(); for (Map.Entry<String, Field> entry : mFieldMap.entrySet()) { Object value = entry.getValue().get(entity); result.put(entry.getKey(), value == null ? "" : value.toString()); } return result; } /** * private ContentValues getContentValues(Map<String, String> values) { ContentValues cv = new ContentValues(); for (Map.Entry<String, String> val : values.entrySet()) { cv.put(val.getKey(), val.getValue()); } return cv; }Copy the code

4, delete

To implement the ability to delete table data, use the delete() method to SQLiteDatabase, where whereClause and whereArgs are key. Because the framework is an ORM framework, in the presentation layer, the deletion conditions need to be encapsulated by data entities, while inside the framework, the incoming data entities are parsed, and the attributes in the object whose attribute value is not null are taken out as the deletion conditions (this also means that common data types cannot be used, such as int, But can be replaced with Integer), which can be divided into two steps:

  1. Convert an attribute in an object into a key-value pair.
  2. The Condition constructor is used to concatenate key-value pairs from whereMap whose value is not null.

To sum up, BaseDao implements table data deletion as follows:

@Override public Integer delete(M where) { try { Map<String, String> whereMap = getValues(where); Condition condition = new Condition(whereMap); return mDatabase.delete(mTbName, condition.whereClause, condition.whereArgs); } catch (IllegalAccessException e) { e.printStackTrace(); } return 0; } class Condition { public Condition(Map<String, String> whereMap) { StringBuilder sb = new StringBuilder(); List<String> list = new ArrayList<>(); for (Map.Entry<String, String> entry : whereMap.entrySet()) { if (! TextUtils.isEmpty(entry.getValue())) { sb.append("and " + entry.getKey() + "=? "); list.add(entry.getValue()); } } this.whereClause = sb.delete(0, 4).toString(); this.whereArgs = list.toArray(new String[list.size()]); } String whereClause; String[] whereArgs; }Copy the code

Where eclause is a delete condition, which is a string, and you need to use? Multiple conditions need to be joined using the and keyword, for example: name=? and password=? . WhereArgs is an array of font strings that numerically replace placeholders in whereClause, such as new String[]{“LQR”,”123456″}.

5, change

The update part of SQLiteDatabase is easier to understand because the update() method of SQLiteDatabase takes the ContentValues object, whereClause, and whereArgs parameters, In fact, it is the addition and deletion of the code implementation, this is not nonsense, the implementation of the code as follows:

@Override
public Integer update(M entitiy, M where) {
    try {
        Map<String, String> values = getValues(entitiy);
        ContentValues cv = getContentValues(values);

        Map<String, String> whereMap = getValues(where);
        Condition condition = new Condition(whereMap);

        return mDatabase.update(mTbName, cv, condition.whereClause, condition.whereArgs);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return 0;
}
Copy the code

6,

Finally, the last step in THE CRUD: data query (Retrieve). SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase: SQLiteDatabase

  1. Query all the table data that meets the conditions.
  2. The table data that meets the conditions can be queried and sorted.
  3. The table data that meets the conditions can be queried, in addition to sorting, but also paging query.

One thing to note is paging queries, because the first page of SQLiteDatabase starts at 0, and I want the presentation layer to start at 1, so it decrement in the framework code. The code for these three methods is as follows:

@Override public List<M> query(M where) { return query(where, null); } @Override public List<M> query(M where, String orderBy) { return query(where, orderBy, null, null); } @Override public List<M> query(M where, String orderBy, Integer page, Integer pageCount) { List<M> list = null; Cursor cursor = null; try { String limit = null; if (page ! = null && pageCount ! = null) { int startIndex = --page; limit = (startIndex < 0 ? 0 : startIndex) + "," + pageCount; } if (where ! = null) { Map<String, String> whereMap = getValues(where); Condition condition = new Condition(whereMap); cursor = mDatabase.query(mTbName, null, condition.whereClause, condition.whereArgs, null, null, orderBy, limit); } else { cursor = mDatabase.query(mTbName, null, null, null, null, null, orderBy, limit); List = getDataList(cursor); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } finally { if (cursor ! = null) { cursor.close(); cursor = null; } } return list; } /** * pass cursor, */ Private List<M> getDataList(Cursor Cursor) throws IllegalAccessException, InstantiationException { if (cursor ! = null) { List<M> result = new ArrayList<>(); // traverses the cursor, While (cursor.movetonext ()) {ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass(); / / get the current new object of generic type of parent Class < M > clazz = (Class < M >) pt. GetActualTypeArguments () [0]; // Get the real type of the first type argument M item = clazz.newinstance (); // Iterate over the table fields, using cursor values one by one, and assign values to newly created objects. Iterator<String> iterator = mFieldMap.keySet().iterator(); While (iterator.hasnext ()) {// Find the table field String columnName = iterator.next(); Field Field = mfieldmap. get(columnName); // Use the cursor to get the table value Object val = null; Class<? > fieldType = field.getType(); if (fieldType == String.class) { val = cursor.getString(cursor.getColumnIndex(columnName)); } else if (fieldType == int.class || fieldType == Integer.class) { val = cursor.getInt(cursor.getColumnIndex(columnName)); } else if (fieldType == double.class || fieldType == Double.class) { val = cursor.getDouble(cursor.getColumnIndex(columnName)); } else if (fieldType == float.class || fieldType == Float.class) { val = cursor.getFloat(cursor.getColumnIndex(columnName)); } // Reflection assigns a value to the object attribute field.set(item, val); } // Add the object to the collection result.add(item); } return result; } return null; }Copy the code

At this point, this simple database framework is written, let’s test it.

Five, the test

The layout of the Activity is very simple, I won’t paste, just a few simple buttons.

1. Prepare for the test

1) the User

A simple data entity class, nothing to say, look at the code.

@TbName("tb_user") public class User { @TbField(value = "tb_name", length = 30) public String username; @TbField(value = "tb_password", length = 20) public String password; @TbField(value = "tb_age", length = 11) public Integer age; public User() { } public User(String username, String password) { this.username = username; this.password = password; } public User(String username, String password, int age) { this.username = username; this.password = password; this.age = age; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "[username:" + this.username + ", password:" + this.getPassword() + ", age:" + this.getAge() + "]"; }}Copy the code

2) UserDao

As you can see, the UserDao doesn’t have much code in it, but it can extend the functionality of its parent class by overriding the createTable() method to create tables more flexibly, or by customizing some other methods.

public class UserDao extends BaseDao<User> {
    // @Override
    // protected boolean createTable(SQLiteDatabase database) {
    // database.execSQL("create table if not exists t_user(tb_name varchar(30),tb_password varchar(10))");
    // return super.createTable(database);
    // }
}
Copy the code

2. Functional testing

1) Initialization

The framework needs to specify a database location, which is specified by calling the framework’s init() method in the Activity’s onCreate() method, preferably in a custom Application.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_customer_db_frame);
    BaseDaoFactory.init(new File(getFilesDir(), "csdn_lqr.db").getAbsolutePath());
    mUserDao = BaseDaoFactory.getInstance().getDataHelper(UserDao.class, User.class);
    mUser = new User("CSDN_LQR", "123456", 10);
}
Copy the code

2) increase

Select * from user where username = ‘CSDN_LQR’ and password = ‘123456’;

public void insert(View view) { Long insert = mUserDao.insert(mUser); Toast.maketext (getApplicationContext(), "added" + (insert! = 1? 1:0) + "data ", toast.length_short).show(); }Copy the code

3) delete

Select * from user where username = ‘CSDN_LQR’;

public void delete(View view) { User where = new User(); where.setUsername("CSDN_LQR"); Integer delete = mUserDao.delete(where); Toast.maketext (getApplicationContext(), "delete" + delete + "strip of data ", toast.length_short).show(); }Copy the code

4) change

Select * from user where username = ‘CSDN_LQR’;

public void update(View view) { User user = new User("LQR_CSDN", "654321", 9); User where = new User(); where.setUsername("CSDN_LQR"); Integer update = mUserDao.update(user, where); Toast.maketext (getApplicationContext(), "modified" + update + "LENGTH_SHORT ", toast.length_short).show(); }Copy the code

5) check

A. Query all the table data that meets the conditions

Select * from user where username = ‘CSDN_LQR’ and username = ‘CSDN_LQR’;

public void query1(View view) {
    User where = new User();
    where.setUsername("CSDN_LQR");

    List<User> list = mUserDao.query(where);
    int query = list == null ? 0 : list.size();
    Toast.makeText(getApplicationContext(), "查出了" + query + "条数据", Toast.LENGTH_SHORT).show();
    for (User user : list) {
        System.out.println(user);
    }
}
Copy the code

B. Query and sort the table data that meets the conditions

Select * from user where age = 0;

public void query2(View view) { List<User> list = mUserDao.query(null, "tb_age asc"); int query = list == null ? 0 : list.size(); Toast.maketext (getApplicationContext(), "toast.length_short ", toast.length_short).show(); for (User user : list) { System.out.println(user); } } public void query3(View view) { List<User> list = mUserDao.query(null, "tb_age desc"); int query = list == null ? 0 : list.size(); Toast.maketext (getApplicationContext(), "toast.length_short ", toast.length_short).show(); for (User user : list) { System.out.println(user); }}Copy the code

C. Query the table data that meets the conditions and page it.

Select * from user; select * from user;

public void query4(View view) {
    User where = new User();

    List<User> list = mUserDao.query(where, null, 1, 2);
    int query = list == null ? 0 : list.size();
    Toast.makeText(getApplicationContext(), "查出了" + query + "条数据", Toast.LENGTH_SHORT).show();
    for (User user : list) {
        System.out.println(user);
    }
}
Copy the code

Big success, scatter flowers.

Finally, paste the Demo address:

Github.com/GitLqr/Simp…