三、数据库
1.Sqlite升级,增加字段的语句
2.数据库框架对比和源码分析
3.数据库的优化
4.数据库数据迁移问题
参考答案:
1.Sqlite升级,增加字段的语句
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.数据库框架对比和源码分析
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…
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 源码分析
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都没有在包含在图中。
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.数据库的优化
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.数据库数据迁移问题
数据库升级,主要有以下这几种情况:
增加表 删除表 修改表:分为“增加表字段”以及“删除表字段”
先将旧的表删除再创建新的表,这是最简单暴力的,但前面提过这不是我们想要的结果。
主要思路是:首先将原来的表进行改名称rename table(临时表),接着创建新的表create table,再者将旧表内的数据迁移到新表内,最后drop table删除临时表。
5.Sqlite 常见异常
android.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database file
android.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database file
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/data/com.ant.sqlite3/databases/mydata.db
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
Caused by: android.database.sqlite.SQLiteException: no such table: ligas_bd: , while compiling: SELECT * FROM ligas_bd
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.
distinct