Android 中主流数据库分析及应用

背景:

  • 公司目前一直用 greenDao,还算稳定,前几天看到 greenDao 官方推荐另一个全新的数据库框架 ObjectBox,按照官方的说法,它的运行速度是SQ以及其他同类软件的10倍
  • 现在大多数公司都开始切换使用kotlin 语言,而greenDao 它其实是一个 Java数据库,对kotlin  并不是很友好
  • Google 官方推出了Jetpack  全家桶,里面包含的 Room 数据库也是一个非常不错的数据库,且作为开发者,JetPack 是一个大方向,很有必要了解和学习。

本文讲对比分析目前主流的数据库的一些优缺点及介绍如何将各个数据库引入到项目中使用。分析对比主要从以下方面进行:

  • 性能和速度
  • 包体积
  • 是否利于后期维护
  • 功能的扩展性
  • 学习成本

1.greenDao 的现状及存在的一些问题

  • 介绍:greenDao 是 Android中一个开源的对象关系映射框架,能够提供一个接口通过操作对象的方式去操作关系型数据库,完成 Java 对象的存储,更新,删除和查询。

  • 特点:

    • 最佳性能:(可能是Android最快的ORM,由智能代码生成驱动)

    • 最小的内存消耗

    • 小:库小于 150K,保持较低的构建时间,并避免65k 方法限制

    • 数据库加密

    • 强大而活跃的社区交流支持

  • 官方:Android 中主流数据库分析及应用_第1张图片

  • 基于当下,存在的不足

    • 性能上来说,并不是最优的选择

    • 官方基本已经停止维护,v3.3.0之后版本没有再更新过

    • 相比kotlin,更适合于 Java 项目

    • 数据库升级的不足和繁杂

2.常见开源优秀的数据库框架

  • ObjectBox :现在 greendao 官方强烈推荐的面向对象的数据库框架(superfast),能够嵌入到 Android、Linux、macOS 或 Windows 应用程序中。

  • Room:谷歌官方的数据库框架,基于sqlite 进行了封装,我们可以直接使用room 来进行数据库的访问。

  • Realm:完美替代SQLite,核心包含C++库,同时支持Android和Ios ,是专门为移动平台设计的NoSql 数据库。

  • 其他(如litePal)

3.常见优秀数据库的对比

  • 性能对比

    Android 中主流数据库分析及应用_第2张图片

    • https://github.com/objectbox/objectbox-performance

    • 在内存大小相同的情况下处理1000条数据所用时间:

    Android 中主流数据库分析及应用_第3张图片

    • 在内存大小相同的情况下处理10k条数据所用时间

      Android 中主流数据库分析及应用_第4张图片

    • 结论:从性能上来看,ObjectBox 完全优于Realm和greenDao以及Room,同时Room和greenDao差别不是很大。

  • 包体积大小

    • objectBox: 引入前:3.2M,引入后9.1M,包体积约为5.9M (1~1.5m)

    • Room:引入前:3.2M,引入后5.4M,包体积约为2.2M。(不到1m)

    • greenDao:<150K

    • Realm:引入前 3.2M,引入后12.3M,包体积约为9.1M

  • 方法数限制:

    • ObjectBox:1300

    • Room:300

    • Realm:2000

总结:如果追求速度和效率,明显选择ObjectBox,如果受限于app大小,方法数已经接近64k限制,也愿意处理SQL,可以考虑Room。

4. ObjectBox 的介绍

objectBox 是greenDao 官方推荐的一款基于nosql 的开源数据库框架,它比SQLite快得多并且易于使用,

Android 中主流数据库分析及应用_第5张图片

4.1特性:

Android 中主流数据库分析及应用_第6张图片

4.2 关于Nosql 和SQL的简单介绍和对比

  • 概念:

    • SQL:关系型数据库,比如mysql,oracle和SQL Server

    • No SQL:泛指 非关系型数据库,比如后台常用的MongoDB,Redis

  • 存储方式:

    • SQL:存在特定结构的表中,常以数据库表形式存储数据

    • NoSQL:更加灵活和可扩展,存储方式可以是JSON文档,哈希表或者其他方式。

  • 特点:

    • sql :必须先定义好表和字段结构才能添加数据,Nosql 中数据可以在任何地方添加,不需要定义表。

    • 在相同水平的系统设计的前提下,因为NoSQL中省略了JOIN查询的消耗,故理论上性能上是优于SQL的

4.3 项目中objectBox的引入及使用

4.3.1 添加依赖

  • 应用程序 build.gradle文件(模块级)下配置
apply plugin: 'io.objectbox' // 引入插件
  • 项目根 build.gradle (项目级别) 配置

4.3.2 新建实体类

/**
     *
     *  注解说明:
     * 1.对象必须有一个long类型的ID(注解为@Id)。
     * 当然也可以使用包装类java.lang.Long,但是我们不建议这么做,因为long在ObjectBox 中非常的高效
     *
     * 2 id 是对象的主键,默认情况下是由ObjectBox 自动管理的,也就是自增id。
     *   如果我们想要手动管理id则添加@Id(assignable = true)。id的值不能为负数,如果超过long的最大值,objectbox 会报错;;当id等于0时objectbox会认为这是一个新的实体对象,因此会新增到数据库表中;
     * 3.@Unique :将属性值标记为唯一
     *
     * 4.@Transient:表示该字段不会持久存储在数据库中
     * 5.@Backlink:定义反向链接关系,该关系基于另一个反向关系
     *6.@NotNull:指定该属性不为空
     *
     */
@Entity
public class QRCodeBox {
    private String SkinNo;
    @Unique
    private String deviceSn;
    public QRCodeBox(String skinNo, String deviceSn, long id) {
        SkinNo = skinNo;
        this.deviceSn = deviceSn;
        this.id = id;
    }
    public String getSkinNo() {
        return SkinNo;
    }
    public void setSkinNo(String skinNo) {
        SkinNo = skinNo;
    }
    public String getDeviceSn() {
        return deviceSn;
    }
    public void setDeviceSn(String deviceSn) {
        this.deviceSn = deviceSn;
    }
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    @Id
    private long id;
    public QRCodeBox(){
    }
}

4.3.3 初始化并生成一个BoxStore

public class ObjectBox {
    private static BoxStore boxStore;
    public static void init(Context context) {
        boxStore = MyObjectBox.builder()
                .androidContext(context.getApplicationContext())
                .build();
        if (BuildConfig.DEBUG) {
            new AndroidObjectBrowser(boxStore).start(context.getApplicationContext());
            Log.d("ObjectBox", String.format("Using ObjectBox %s (%s)", BoxStore.getVersion(), BoxStore.getVersionNative()));
        }
    }
    /****
     * @return boxStore 对象
     */
    public static BoxStore get(){
        return boxStore;
    }
}
  1. 核心API
  •  MyObjectBox:基于实体类自动生成,用于初始化生成 BoxStore对象

  • BoxStore:进行数据库的管理,管理Boxes

  • Box:每一个实体,对应一个Box,由BoxStore提供。

Box qrCodeBox =ObjectBox.get().boxFor(QRCodeBox.class)  

 2.自动生成的类

  • MyObjectBox:用于初始化生成 BoxStore对象

  • 实体_: 自动生成的属性类,如QRCodeBox__,和实体类QRCodeBox一一对应,查询操作的时候会经常用到

  • QRCodeBoxCursor:基于QRCodeBox生成的Cursor,几乎每一个操作都需要Cursor配合完成

4.4.3 数据库增删改查操作:

  • 增(put)

qrCodeBox.put(qrCode);  //添加一个实体
qrCodeBox.put(qrCode,qrCode2); //添加多个实体
  • 删除(remove)

 qrCodeBox.removeAll();  //删除全部
 qrCodeBox.remove(1);   //根据id 删除某个实体,1代表id
 qrCodeBox.remove(1,2,3,4);   //根据多个id删除多个对应的实体
 boolean isRemoved = qrCodeBox.remove(qrCode);   //判断某个对象是否已经删除
 qrCodeBox.remove(qrCode,qrCode2,qrCode3);    //删除多个实体
 
 //将多个id以集合的形式删除其对应的实体
  ArrayList id = new ArrayList<>();
            id.add((long) 1);
            id.add((long) 2);
            id.add((long) 3);
            id.add((long) 4);
            qrCodeBox.removeByIds(id);
            
 // 删除多个集合实体
  ArrayList qrCodeBoxArrayList = new ArrayList<>();
            QRCodeBox qrCode2 = new QRCodeBox();
            qrCode.setDeviceSn(System.currentTimeMillis()+"");
            qrCode.setSkinNo(new Random().nextInt(10)+"");
            qrCodeBoxArrayList.add(qrCode);
  • 更新 (put)

如果id存在就去更新,不存在添加

 /**
     * 更新
     */
    private void updateObjectBox() {
        // 查询姓名为tom的用户,并且只返回一个
        QRCodeBox qrCodeBean = qrCodeBox.query().equal(QRCodeBox_.SkinNo, "8").build().findFirst();
        if (null != qrCodeBean) {
            // 修改姓名为tom wang
            qrCodeBean.setSkinNo("9"); 
            // 更新user
            qrCodeBox.put(qrCodeBean);
        }
    }
  • 查询 (find)

先获取一个QueryBuilder 的查询对象

a.条件查询(可以多个条件叠加查询)

//查询所有第一个skinNow为8的实体
QRCodeBox qrCodeBean  = qrCodeBox.query().equal(QRCodeBox_.SkinNo, "8").build().findFirst();

// 查询所有skinNo不为8的实体集合
 List qrCodeBoxes = qrCodeBox.query().notEqual(QRCodeBox_.SkinNo, "8").build().find();
 
  //查询所有skinNo在2~8之间的实体集合
List qrCodeBoxes1 = qrCodeBox.query().between(QRCodeBox_.SkinNo, 2, 8).build().find();   //查询所有skinNo在2~8之间的实体集合


//多个条件叠加查询使用
 List build = qrCodeBox.query().equal(QRCodeBox_.SkinNo, "8")
                .notEqual(QRCodeBox_.deviceSn, "756373")
                .build().find();

其他:

  • 大于:greater

  • 以....开头 :startsWith

  • 小于:less

  • 以....结尾;endsWith

b.排序(order)

  • 升序(order)

userBox.query().equal(User_.firstName, "Joe")
    .order(User_.lastName) // 按升序排列,忽略大小写
    .find();
  • 降序:(orderDesc)

List build = qrCodeBox.query().orderDesc(QRCodeBox_.id).build().find();
        for (QRCodeBox qrCodeBox:build){
            Toast.makeText(this, qrCodeBox.toString(), Toast.LENGTH_SHORT).show();
        }

c. 分页查询

 从 第2个开始(不含第2个),查询之后的8个
qrCodeBox.query().equal(QRCodeBox_.SkinNo, 2).build().find(2,8);

d. 多表查询(link)

QueryBuilder stuBuilder = mStuBox.query()
				.equal(Student_.name, "小明0");
 List courses =stuBuilder
 				.link(Student_.courses)
 				.equal(Course_.courseName, "语文")
        .build().find();

4.3.5 数据模型变更(升级数据库

(1)新增或者删除字段

  • 步骤:直接删除或者新增即可,不用添加注解

(2)重命名(类名或者字段名)

  • 步骤:

    • 变更类名或者字段名

    • 在类名或者字段名(以变更字段名为例)前添加@Uid注解并编译代码,此时编译不会通过并且会报错,使用 [Rename] apply 后面的@Uid(54454312044669023L) 替换之前的@Uid,并再次运行以及解决代码冲突

Android 中主流数据库分析及应用_第7张图片

  • 原理:ObjectBox管理数据模型主要是自动管理。数据模型是我们自己定义的实体类。当我们添加或者删除实体属性,或者进行重命名的时候,ObjectBox会自动为我们处理这些更改。

Q:objectBox 如何是关联这些变更的?

A:通过UIDs来进行关联,ObjectBox会分配唯一ID(UIDs)来跟踪实体和属性。所有这些UIDs都存储在一个文件“objectbox-models / default.json”中,

Q:为什么需要UId?

A:变更的时候保存我们之前旧的数据

4.3.6 查看数据库中数据(通过浏览器)

  • 添加依赖

//支持浏览器查看数据库数据,如果不需要可以去掉dependencies 代码块
   debugImplementation "io.objectbox:objectbox-android-objectbrowser:$objectboxVersion"
    releaseImplementation "io.objectbox:objectbox-android:$objectboxVersion"
  • 开发环境下开启调试

    if (BuildConfig.DEBUG) {
            new AndroidObjectBrowser(boxStore).start(context.getApplicationContext());
            Log.d("ObjectBox", String.format("Using ObjectBox %s (%s)", BoxStore.getVersion(), BoxStore.getVersionNative()));
        }
  • 端口转发

adb forward tcp:8090 tcp:8090
  • 查看:http://localhost:8090/index.html

Android 中主流数据库分析及应用_第8张图片

其他

  • 存储复杂对象

  • 结合liveData使用

5. Room的使用

Room是Jetpack组件库一员,属于ORM库,主要是基于Sqlite做了一层封装,简化开发者对数据库操作。Room支持编译时的语法检查,并且支持返回LiveData。

Android 中主流数据库分析及应用_第9张图片

  • (1)增加5.1.添加项目依赖

         def room_version = "2.3.0"
        implementation "androidx.room:room-runtime:$room_version"
        annotationProcessor "androidx.room:room-compiler:$room_version"
        
        // To use Kotlin annotation processing tool (kapt)
       // kapt("androidx.room:room-compiler:$room_version")

    5.2.新建实体类

    @Entity
    public class User {
    ​
        /***
         * 每个实体必须将至少 1 个字段定义为主键。即使只有 1 个字段,您仍然需要为该字段添加 @PrimaryKey 注释。
         * 此外,如果您想让 Room 为实体分配自动 ID,则可以设置 @PrimaryKey 的 autoGenerate 属性。
         * 如果实体具有复合主键,您可以使用 @Entity 注释的 primaryKeys 属性
         *
         */
        @PrimaryKey(autoGenerate = true)
        public int uid;
    ​
        @ColumnInfo(name = "first_name")
        public String firstName;
    ​
        public int getUid() {
            return uid;
        }
        public void setUid(int uid) {
            this.uid = uid;
        }
        public String getFirstName() {
            return firstName;
        }
    ​
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
    ​
        public String getLastName() {
            return lastName;
        }
    ​
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
        @ColumnInfo(name = "last_name")
        public String lastName;
    }

    5.3. 定义DAO 接口

    @Dao
    public interface UserDao {
        /***
         *
         * 每个 @Query 方法都会在编译时进行验证,因此如果查询出现问题,则会发生编译错误,而不是运行时失败
         *
         * Room 还会验证查询的返回值,以确保当返回的对象中的字段名称与查询响应中的对应列名称不匹配时,Room 可以通过以下两种方式之一提醒您:
         *
         * 如果只有部分字段名称匹配,则会发出警告。
         * 如果没有任何字段名称匹配,则会发出错误。
         * @return
         */
    ​
        @Query("SELECT * FROM user")
        List getAll();
    ​
        @Query("SELECT * FROM user WHERE uid IN (:userIds)")
        List loadAllByIds(int[] userIds);
    ​
        @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
                "last_name LIKE :last LIMIT 1")
        User findByName(String first, String last);
    ​
        @Insert
        void insertAll(User... users);
    ​
        @Delete
        void delete(User user);
    ​
        @Update
        void update(User user);
    }

    5.4.新建数据库

    @Database(entities = {User.class}, version = 2,exportSchema = false)
    public abstract class UserDatabase extends RoomDatabase {
        private static final String DB_NAME = "userDatabase.db";
        private static volatile UserDatabase instance;
        /****
         *
         *    通过单例的形式获取数据库
         *
         * @param context
         * @return
         */
        static synchronized UserDatabase getInstance(Context context) {
            if (instance == null) {
                instance = create(context);
            }
            return instance;
        }
        private static UserDatabase create(final Context context) {
            return Room.databaseBuilder(context, UserDatabase.class, DB_NAME).allowMainThreadQueries().build();
        }
        public abstract UserDao  getUserDao();
    }

    5.5.数据库的操作

(1)增加

for (int i=0;i<5;i++){
    User user=new User();
    user.setFirstName("kong");
    user.setLastName("dexi");
    UserDatabase.getInstance(this).getUserDao().insertAll(user);
}

(2)更新

  int[] userIds={1,2,3,4,5};
List users = UserDatabase.getInstance(this).getUserDao().loadAllByIds(userIds);
for (User user :users){
    user.setLastName("dolphkon");
    UserDatabase.getInstance(this).getUserDao().update(user);
}

(3)查询

  int[] userIds={1,2,3,4,5};
List users = UserDatabase.getInstance(this).getUserDao().loadAllByIds(userIds);

(4)删除

List all = UserDatabase.getInstance(this).getUserDao().getAll();
for (User user :all){
    UserDatabase.getInstance(this).getUserDao().delete(user);
}

5.6.数据库的升级

  • 方式1:直接清空之前已的数据(不推荐)

/***
 * @param context  直接清空之前的数据
 * @return
 */
private static UserDatabase create(final Context context) {
    return Room.databaseBuilder(context, UserDatabase.class, DB_NAME)
            .fallbackToDestructiveMigration()   //直接清空之前的数据
            .allowMainThreadQueries()
            .build();
}
  • 方式二:在原来的基础上,修改或者添加我们想要的数据

案例:包含了数据库新增,删除和修改字段

  • 1~2 版本:新增address字段

  • 2~3 版本:将address字段修改为region

  • 3~4 新增 新增 employee_id字段

  • 4~5 版本 删除 last_name 字段

5.6.1 新增字段

  • 实体类中添加字段并实现set和get方法

    ....
public String address;
public String getAddress() {
    return address;
}
public void setAddress(String address) {
    this.address = address;
}
   ...
  • 升级版本号,修改字段名
@Database(entities = {User.class}, version = 2)
public abstract class UserDatabase extends RoomDatabase {
           ........// 代码省略
}
  • 添加Migration

 /***
     *     1~2 的迁移
     *
     */
    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE User ADD COLUMN  address TEXT");
        }
    };
    
    
     private static UserDatabase create(final Context context) {
        return Room.databaseBuilder(context, UserDatabase.class, DB_NAME)
                .addMigrations(MIGRATION_1_2)
                .allowMainThreadQueries()
                .build();
    }

5.6.2 将String 类型 address字段修改为region

  • 实体类中添加字段并实现set和get方法

...
public String region;
​
public String getRegion() {
    return region;
}
​
public void setRegion(String region) {
    this.region = region;
}
...
  • 升级版本号为3

  • 添加Migration

/***
 *     2~3的迁移
 *
 */
static final Migration MIGRATION_2_3 = new Migration(2,3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE User RENAME COLUMN  address TO region");
    }
};

 private static UserDatabase create(final Context context) {
        return Room.databaseBuilder(context, UserDatabase.class, DB_NAME)
                .addMigrations(MIGRATION_1_2,MIGRATION_2_3)
                .allowMainThreadQueries()
                .build();
    }

5.6.3 新增int 类型 employee_id字段

  • 修改实体类

@ColumnInfo(name = "employee_id")
@NonNull
public int employeeId;

public int getEmployeeId() {
    return employeeId;
}
public void setEmployeeId(int employeeId) {
    this.employeeId = employeeId;
}
  • 升级版本号为4

  • 添加Migration

/***
 *     3~4的迁移
 *
 */
static final Migration MIGRATION_3_4 = new Migration(3,4) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE User ADD COLUMN  employee_id INTEGER NOT NULL DEFAULT 0");
    }
};

private static UserDatabase create(final Context context) {
        return Room.databaseBuilder(context, UserDatabase.class, DB_NAME)
                .addMigrations(MIGRATION_1_2,MIGRATION_2_3,MIGRATION_3_4)
                .allowMainThreadQueries()
                .build();
    }

5.6.4 删除last_name字段

  • 修改实体类,删除last_name字段

  • 升级版本号为5

  • 添加Migration (删除字段时需要变向删除以及数据迁移)

/***
 *     4~5的迁移
 *
 */
static final Migration MIGRATION_4_5 = new Migration(4, 5) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {

        // Create the new table
        database.execSQL("CREATE TABLE users_new (uid INTEGER PRIMARY KEY NOT NULL, first_name TEXT, region TEXT, employee_id INTEGER NOT NULL)");
        // Copy the data
        database.execSQL("INSERT INTO users_new (uid, first_name, region,employee_id) SELECT uid, first_name,region, employee_id FROM user");
        // Remove the old table
        database.execSQL("DROP TABLE user");
        // Change the table name to the correct one
        database.execSQL("ALTER TABLE users_new RENAME TO user");
    }
};

5.6.5 多版本迁移

  • 添加Migration(1~5))

/***
 *     1~5的迁移
 *
 */
static final Migration MIGRATION_1_5 = new Migration(1, 5) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {

        // Create the new table
        database.execSQL("CREATE TABLE users_new (uid INTEGER PRIMARY KEY NOT NULL, first_name TEXT, region TEXT, employee_id INTEGER NOT NULL)");
        // Copy the data
        database.execSQL("INSERT INTO users_new (uid, first_name, region,employee_id) SELECT uid, first_name,region, employee_id FROM user");
        // Remove the old table
        database.execSQL("DROP TABLE user");
        // Change the table name to the correct one
        database.execSQL("ALTER TABLE users_new RENAME TO user");
    }
};

 private static UserDatabase create(final Context context) {
        return Room.databaseBuilder(context, UserDatabase.class, DB_NAME)
                .addMigrations(MIGRATION_1_2,MIGRATION_2_3,MIGRATION_3_4,MIGRATION_4_5,MIGRATION_1_5)
                .allowMainThreadQueries()
                .build();
    }

5.6.5 Schema文件 配置(可选)

Q:该文件有什么用?能解决什么样的问题?

A:如下:

  • 可以验证数据库的修改是否符合我们的预期

  • 可以查看到数据库的历次升级情况

在每次数据库的升级过程中,它都会为你导出一个Schema文件,这是一个json文件,里面包含了数据库的所有基本信息。有了这些文件,开发者们就能知道数据库的历史变更情况,极大地方便了开发者们排查问题。Schema文件是默认导出的,你只需要指定它导出的位置即可。

  • 1.配置Schema文件导出路径(在appbuild.gradle文件中指定Schema文件的导出位置。)
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]//指定数据库schema导出的位置
            }
        }
    }
}
  • 2.查看 schema 文件

Android 中主流数据库分析及应用_第10张图片

  • 3.如果不想用,指定exportSchema属性为false即可。

@Database(entities = {User.class}, exportSchema = false,version = 5)
public abstract class UserDatabase extends RoomDatabase {
  .....
}
  • 当更新了数据库的模式(schema)后,一些设备上的数据库可能仍然是旧的模式版本。如果 Room 无法找到将设备的数据库从旧版本升级到当前版本的迁移规则,就会出现 IllegalStateException异常。

  • 为了防止出现这种情况导致应用崩溃的问题,我们可以在创建数据库时调用 fallbackToDestructiveMigration() 方法,此时 Room 将会重建应用的数据库表(将直接删除原数据库表中的所有数据)。

private static UserDatabase create(final Context context) {
    return Room.databaseBuilder(context, UserDatabase.class, DB_NAME)
            .addMigrations(MIGRATION_1_2,MIGRATION_2_3,MIGRATION_3_4,MIGRATION_4_5,MIGRATION_1_5)
            .fallbackToDestructiveMigration()  //重建
            .allowMainThreadQueries()
            .build();
}

5.7.Room 浏览器数据库数据

  • userDatabase.db

Android 中主流数据库分析及应用_第11张图片

  • room_master_table

Android 中主流数据库分析及应用_第12张图片

5.8. 其他:

Room结合LiveData​​​​​​

你可能感兴趣的:(Android,框架)