2020 Android 大厂面试(三)数据库 含 参考答案

3,008 阅读9分钟

三、数据库

1.Sqlite升级,增加字段的语句
2.数据库框架对比和源码分析
3.数据库的优化
4.数据库数据迁移问题

参考答案:

1.Sqlite升级,增加字段的语句

blog.csdn.net/qq_26287435…

SQLite supports a limited subset of ALTER TABLE. 
The ALTER TABLE command in SQLite allows the user to rename a table or to add a new column to an existing table.
alter table table_name
add column 'new_col' varchar(100)
default '1'

2.数据库框架对比和源码分析

www.codexiu.cn/android/blo…

ORMLite:github.com/j256/ormlit…

Afinal:github.com/yangfuhai/a…

ActiviteAndroid:github.com/pardom/Acti…

SugarORM:Android 平台专用ORM

GreenDao:github.com/greenrobot/…

Realm:github.com/realm/realm…

www.jianshu.com/p/5eab05820…

GreenDao Room
主键使用@Id注解标注,可以设置autoincrement = true让主键自增,注意:这里主键类型必须是Long,因为只有id=null时自增才会生效 主键使用PrimaryKey注解,可以设置autoGenerate = true表示主键自增
如果不需要时使用Transient注解忽略此属性 使用@Ignore注解,表示忽略此属性
支持对象之间的关联(一对一,一对多等等) 允许entity对象相互引用,Room仍然允许你定义entity之间的外键(Foreign Key)约束

Room: 1、 使用Room时需要创建自己的AppDatabasele抽象类,并且继承自RoomDatabase。

2、 使用@Database注解标注,其中可以指定entitys,将这个库中所有的表对应的实体放在entitys中,version字段可以指定数据库版本。

3、 Dao对象需要开发者编写各entity对应的Dao接口,并用@Dao注解标注,接口中提供需要的方法声明。Room会自动生成对应的实现类,并实现所有的方法。

@Dao
public interface ProductDao {

    @Query("select * from products")
    List<ProductEntity> queryProducts();

    @Query("select * from products where id = :productId")
    ProductEntity getProductById(long productId);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAll(ProductEntity... entity);

    @Update(onConflict = OnConflictStrategy.REPLACE)
    void updateAll(ProductEntity... entity);

    @Delete
    void deleteAll(ProductEntity... entity);

}

4、在AppDatabase中提供对应实体的Dao的方法声明。这样就可以通过APPDatabase实例获取到指定的dao对应从而操作表了。 说明:Room会自动生成该类的实现类。使用时直接调用即可。代码如下:

@Database(entities = {ProductEntity.class, CommentEntity.class},version = 1)
@TypeConverters(DateConverter.class)
public abstract class AppDatabase extends RoomDatabase {
    public static String DATABASE_NAME = "room-sample-db";

    public abstract ProductDao productDao();

    public abstract CommentDao commentDao();
}

5、获取数据库实例

使用Room提供的静态方法创建数据库。代码如下:

appDatabase = Room.databaseBuilder(context.getApplicationContext(),
                AppDatabase.class,DATABASE_NAME)
                .addMigrations(MIGRATION_1_2)
                .build();

GreenDao

1、使用GreenDao时会自动生成DaoMaster,DaoSession,XXXEntityDao三个类。

DaoMaster

  • 是GreenDao的入口,也是greenDao顶级对象,对于一个指定的表单持有数据库对象(SQLite数据库)并且能够管理DAO类,能够创建表和删除表
  • 其内部类OpenHelper与DevOpenHelper是创建SQlite数据库的SQLiteOpenHelper的具体实现

DaoSession

  • 对于一个指定的表单可以管理所有的Dao对象。
  • 也能够对实体类执行 insert load update refresh delete 操作。
  • DaoSession 也能跟踪 identity scope:即session查询后的实体会存在缓存中,并给该实体生成一个flag来追踪该实体,下次再次查询时会直接从缓存中取出来而不是从数据库中取出来

Daos

  • 能够持久访问和查询实体类
  • 比起DaoSession有更多的持久化方法 count loadAll insertInt 等等;

2、获取数据库实例

1)、首先需要获取SQLiteOpenHelper对象。可以通过DaoMaster中的DevOpenHelper来实例化,也可以自定义类实现DaoMaster中的OpenHelper

SQLiteOpenHelper oh = new DaoMaster.DevOpenHelper(context,DB_NAME,null);

2)、获取DaoSession: 通过DaoMaster的newSession()获取

mDatabase = oh.getWritableDb();
daoSession = new DaoMaster(mDatabase).newSession();

3)、获取Dao对象:通过DaoSession获取

daoSession.getProductEntityDao();

操作Sql(增删改查) insert,update,delete Room:使用@Insert,@Update,@Delete注解,并且可以指定相应的策略(replace,rollback,fail等) GreenDao:直接生成对应的方法。没有提供相应的策略。但是提供了执行sql的方法。

query Room:使用@Query注解标注,直接传sql

GreenDao:以对象的方式操作。可以使用QueryBuilder类执行各种复杂的查询。

两者做法各有千秋,就看使用者的习惯。就我个人而言还更喜欢使用纯sql,可读性高。

加密 Room不支持SqlCipher加密,不过后续SqlCipher应该会提供针对Room的加密方式,这只是时间的问题。

GreenDao集成了SqlCipher加密。

数据库升级

Room

生成AppDatabase实例时可以通过addMigrations()添加对应的Migration来支持迁移操作。代码如下:

appDatabase = Room.databaseBuilder(context.getApplicationContext(),
            AppDatabase.class, DATABASE_NAME)
            .addMigrations(MIGRATION_1_2)
            .build();
            
private static Migration MIGRATION_1_2 = new Migration(1,2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        // TODO: 
    }
};

GreenDao 通过重写DaoMaster.OpenHelper类实现迁移操作:

public static class AppSQLiteOpenHelper extends DaoMaster.OpenHelper {
    public AppSQLiteOpenHelper(Context context, String name) {
        super(context, name);
    }

    public AppSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
        super(context, name, factory);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        //数据库迁移操作
        MigrationHelper.getInstance().migrate(db, ProductEntityDao.class, CommentEntityDao.class);
    }
}

Room添加对liveData的支持

GreenDao不支持LiveData,需要自己实现。

2-0.realm使用

@Entity(indices = {@Index(value = {"first_name", "last_name"},
        unique = true)})
public class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
public class Book {
    @PrimaryKey
    public int bookId;

    public String title;

    @ColumnInfo(name = "user_id")
    public int userId;
}

2-1.GreenDao 源码分析

blog.ralf.wang/2019/04/数据库…

DaoMaster:使用 greenDAO 的切入点。 DaoMaster 保存数据库对象(SQLiteDatabase)并管理特定模式的 DAO 类(而不是对象)。 它有静态方法来创建表或删除它们。 它的内部类 OpenHelper 和 DevOpenHelper 都是 SQLiteOpenHelper 的实现,用来在 SQLite 数据库中创建和升级等操作。

DaoSession:管理特定模式的所有可用 DAO 对象,你可以使用其中一个的 getter 方法获取 DAO 对象。 DaoSession 还为实体提供了一些通用的持久性方法,如插入,加载,更新,刷新和删除。 最后,DaoSession 对象也跟踪 identity scope。

xxDAO:数据访问对象(DAO),用于实体的持久化和查询。 对于每个实体,greenDAO 会生成一个 DAO。 它比 DaoSession 拥有更多的持久化方法,例如:count,loadAll 和 insertInTx。

DevOpenHelper --> OpenHelper -- > DatabaseOpenHelper -- > SQLiteOpenHelper

理解了 DaoMaster、DaoSession、xxDAO 这个几个类, greenDAO 也就基本理解了,这里给出一个比喻,DaoMaster 相当于一个 Boss,统领大局,但是具体的工作会交给 DaoSession, DaoSession 相当于一个 Leader,会负责管理一些 xxDAO,当然也可以有多个 DaoSession,DaoSession 会提供数据库操作的一些方便的入口方法,其内部具体的执行会交给 xxDAO 执行,xxDAO 负责自己对应的实体类对象的增删改查,其中执行过程中需要 DaoConfig 和 DatabaseStatement,拿到拼接的 sql 语句和要存储的实体类的参数值,最终会通过原生数据库完成操作,基本就是这样一个过程,可以理解为职责一步步向下传递的过程。

2-1.Room 源码分析

图中有一些不合适的地方,removeObserver并不是有Dao调用的,而是查询返回的RxJava Observable或者LiveData“不再用”了的时候被各自调用的。
其中,“重新查询”的意思就是数据流被更新,数据流相关的RxJava Observable或者LiveData都没有在包含在图中。

my.oschina.net/sfshine/blo…

Room是Google提供的一个ORM库。Room提供了三个主要的组件:

@Database:用来注解类,并且注解的类必须是继承自RoomDatabase的抽象类。该类主要作用是创建数据库和创建Daos(data access objects,数据访问对象)。

@Entity:用来注解实体类,@Database通过entities属性引用被@Entity注解的类,并利用该类的所有字段作为表的列名来创建表。

@Dao:用来注解一个接口或者抽象方法,该类的作用是提供访问数据库的方法。在使用@Database注解的类中必须定一个不带参数的方法,这个方法返回使用@Dao注解的类

数据库的创建

数据库必须是一个抽象类 RoomDatabase 的扩展类

在注释中包括与数据库关联的实体列表 必须包含一个具有0个参数且返回带@Dao注释的类的抽象方法

通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 创建数据库实例

@Database(entities = {User.class}, version = 1)  // 注释
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();  // 抽象方法
}

以单例形式对外提供 RoomDataBase 实例

public static UserDataBase getInstance(Context context) {
    if (userDataBase == null) {
        synchronized (UserDataBase.class) {
            if (userDataBase == null) {
                userDataBase = Room.databaseBuilder(context.getApplicationContext()
                        , UserDataBase.class, "user_data").build();
            }
        }
    }
    return userDataBase;
}

定义实体数据:表示数据库中的表 主键

@Entity(tableName = "userDataBase")
class User {
    @PrimaryKey(autoGenerate = true)    // 单个主键设置为自增长
    public var id = 0
    @ColumnInfo(name = "nameUser")  // 定义列名
    public var name: String? = null
}
 
@Entity(primaryKeys = ["id", "name"])  // 组合主键
添加索引@Entity
使用 @Entity 的indices 属性,列出要包含在索引或复合索引中的列的名称
@Entity(indices = [Index("nameUser"), Index(value = ["name"])])  // 创建索引
@Entity(indices = [Index("nameUser"), Index(value = ["name"] ,unique = true)]) //唯一索引

外键约束@ForeignKey
使用@ForeignKey 注释定义其与实体的 关系;ForeignKey中 entity 为要关联的父实体类;
parentColumns 为关联父实体类的列名;childColumns此实体类中的列名

// 在User实体中引入Address
@Embedded
public var address: Address? = null
@Entity(foreignKeys = [ForeignKey(entity = User::class,
        parentColumns = ["id"],
        childColumns = ["user_id"])])
class Book {
    @PrimaryKey
    var bookId: Int = 0
    var title: String? = null
    @ColumnInfo(name = "user_id")
    var userId: Int = 0
}
public class Converters {
    @TypeConverter
    public static Date fromTimestamp(Long value) {
        return value == null ? null : new Date(value);
    }
 
    @TypeConverter
    public static Long dateToTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}

在抽象数据库类中添加转换注解

@TypeConverters({Converters.class})

使用 类型转换器

@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to") List findUsersBornBetweenDates(Date from, Date to);

3.数据库的优化

www.jianshu.com/p/8bef9e422…

String sql ="insert into tb_test values (?,'test');";

SQLiteStatement sqLiteStatement =getReadableDatabase().compileStatement(sql);

for (int count =0 ; count <1000 ; count++){
    sqLiteStatement.clearBindings();
    sqLiteStatement.bindLong(1, count);
    sqLiteStatement.executeInsert();
}
CREATE INDEX index_name ON table_name;

四、分库分表

五、其他 少用cursor.getColumnIndex():使用cursor.getColumnIndex(),系统会根据列名来获取列所在的下标,比较耗时,所以我们可以使用static定义下标,直接通过下标来获取某列。

使用StringBuilder或StringBuffer来拼接字符串:SQL语句字符串的拼接或创建多个临时变量,此时我们可以使用StringBuilder或StringBuffer来拼接字符串,减少不必要的资源占用。

查询时,只返回自己需要的值或结果:如果在查询数据时直接将全部数据获取出来,这样的操作会带来不必要的系统资源开销和浪费。在查询时,我们尽量只取自己需要的字段和结果。

cursor使用后要及时关闭:即在查询完结果后,调用cursor.close()将资源关闭。

4.数据库数据迁移问题

blog.csdn.net/github_3713…

数据库升级,主要有以下这几种情况:

增加表 删除表 修改表:分为“增加表字段”以及“删除表字段”

先将旧的表删除再创建新的表,这是最简单暴力的,但前面提过这不是我们想要的结果。

主要思路是:首先将原来的表进行改名称rename table(临时表),接着创建新的表create table,再者将旧表内的数据迁移到新表内,最后drop table删除临时表。

blog.csdn.net/a714530833/…

5.Sqlite 常见异常

blog.csdn.net/Gaugamela/a…

android.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database file

blog.csdn.net/gaugamela/a…

android.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database file

blog.csdn.net/Aaren_Jiang…

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/data/com.ant.sqlite3/databases/mydata.db

echo.vars.me/android/sql…

database is locked

attempt to re-open an already-closed object

SQLiteDatabase created and never closed

attempt to re-open an already-closed object

database disk image is malformed

codeday.me/bug/2018072…

Caused by: android.database.sqlite.SQLiteException: no such table: ligas_bd: , while compiling: SELECT * FROM ligas_bd

www.cnblogs.com/wangmars/p/…

java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed. java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase:

realm 无事务插入1W条 亲测 java.lang.IllegalStateException: Changing Realm data can only be done from inside a transaction.

juejin.cn/post/684490…

distinct