背景
最近在bugly上看到少量的duplicate column 类似报错。准备看下数据库相关的代码。咋一看没什么问题,但仔细看数据库升级的代码和提交日志发现了一个非常容易犯的错误。什么错误?报错很明了,就是某个数据库中的列重复了。
场景复现
1,假设数据在version1的时候创建了一个表person:
public class DataBaseOpenHelper extends SQLiteOpenHelper {
private final static String DATABASE_NAME = "test.db";
private static DataBaseOpenHelper mDataBaseOpenHelper;
//version1
public static final String CREATE_PERSON =
"create table person(personid integer primary key autoincrement,name " +
"varchar" +
"(20)," +
"phone VARCHAR(12))";
public DataBaseOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
int version) {
super(context, name, factory, version);
}
static synchronized DataBaseOpenHelper getDBInstance(Context context) {
if (mDataBaseOpenHelper == null) {
mDataBaseOpenHelper = new DataBaseOpenHelper(context, DATABASE_NAME, null, 1);
}
return mDataBaseOpenHelper;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_PERSON);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
2,升级到version2的时候又创建一个表student:
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DataBaseOpenHelper extends SQLiteOpenHelper {
private final static String DATABASE_NAME = "test.db";
private static DataBaseOpenHelper mDataBaseOpenHelper;
//version1
public static final String CREATE_PERSON =
"create table person(personid integer primary key autoincrement,name " +
"varchar" +
"(20)," +
"phone VARCHAR(12))";
public static final String CREATE_STUDENT =
"create table student(studentid integer primary key autoincrement,name varchar(20)," +
"phone VARCHAR(12))";
public DataBaseOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
int version) {
super(context, name, factory, version);
}
static synchronized DataBaseOpenHelper getDBInstance(Context context) {
if (mDataBaseOpenHelper == null) {
mDataBaseOpenHelper = new DataBaseOpenHelper(context, DATABASE_NAME, null, 2);
}
return mDataBaseOpenHelper;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_PERSON);
//version2
db.execSQL(CREATE_STUDENT);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//数据库升级逻辑,记得要break
for (int i = oldVersion; i < newVersion; i++) {
switch (i) {
case 1:
db.execSQL(CREATE_STUDENT);
break;
default:
break;
}
}
}
}
3,升级到version3的时候,我们需要在student表中加一个新字段genderid integer。注意,version3如何做?
3.1 错误做法: 直接在CREATE_STUDENT 语句中添加genderid integer,然后在数据库升级中处理升级逻辑,代码:
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DataBaseOpenHelper extends SQLiteOpenHelper {
private final static String DATABASE_NAME = "test.db";
private static DataBaseOpenHelper mDataBaseOpenHelper;
//version1
public static final String CREATE_PERSON =
"create table person(personid integer primary key autoincrement,name " +
"varchar" +
"(20)," +
"phone VARCHAR(12))";
public static final String CREATE_STUDENT =
"create table student(studentid integer primary key autoincrement,name varchar(20)," +
"phone VARCHAR(12),genderid integer)";
//version3
public static final String ALTER_STUDENT = "alter table student add column genderid integer";
public DataBaseOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
int version) {
super(context, name, factory, version);
}
static synchronized DataBaseOpenHelper getDBInstance(Context context) {
if (mDataBaseOpenHelper == null) {
mDataBaseOpenHelper = new DataBaseOpenHelper(context, DATABASE_NAME, null, 3);
}
return mDataBaseOpenHelper;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_PERSON);
db.execSQL(CREATE_STUDENT);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
for (int i = oldVersion; i < newVersion; i++) {
switch (i) {
case 1:
db.execSQL(CREATE_STUDENT);
break;
case 2:
//version3
db.execSQL(ALTER_STUDENT);
break;
default:
break;
}
}
}
}
至此,一切都非常完美。version2升级到version3,走的是onUpgrade逻辑,执行alert语句直接插入genderid列;直接安装version3走的是onCreate逻辑,直接创建新表student肯定包含genderid。
但当从version1直接升级到version3的时候就会报错!!! 根据逐级升级逻辑,首先走的是case1,执行语句CREATE_STUDENT(注意,此时的语句已经不是version2时候的原始语句,而是version3改动后加了genderid列的语句),因此,当逐级执行到case2时,再新增genderid列时候就报错。
正确的做法:当数据库升级过程中向原有数据库新增字段时候,不要直接改动原来创建表的sql语句,而是另外写包含新字段的语句
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DataBaseOpenHelper extends SQLiteOpenHelper {
private final static String DATABASE_NAME = "test.db";
private static DataBaseOpenHelper mDataBaseOpenHelper;
//version1
public static final String CREATE_PERSON =
"create table person(personid integer primary key autoincrement,name " +
"varchar" +
"(20)," +
"phone VARCHAR(12))";
public static final String CREATE_STUDENT =
"create table student(studentid integer primary key autoincrement,name varchar(20)," +
"phone VARCHAR(12))";
//注意这里
public static final String CREATE_STUDENT_NEW =
"create table student(studentid integer primary key autoincrement,name varchar(20)," +
"phone VARCHAR(12),genderid integer)";
//version3
public static final String ALTER_STUDENT = "alter table student add column genderid integer";
public DataBaseOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
int version) {
super(context, name, factory, version);
}
static synchronized DataBaseOpenHelper getDBInstance(Context context) {
if (mDataBaseOpenHelper == null) {
mDataBaseOpenHelper = new DataBaseOpenHelper(context, DATABASE_NAME, null, 3);
}
return mDataBaseOpenHelper;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_PERSON);
// db.execSQL(CREATE_STUDENT); ///注意这里
db.execSQL(CREATE_STUDENT_NEW); ///注意这里
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
for (int i = oldVersion; i < newVersion; i++) {
switch (i) {
case 1:
db.execSQL(CREATE_STUDENT);
break;
case 2:
//version3
db.execSQL(ALTER_STUDENT);
break;
default:
break;
}
}
}
}
这样不管是从哪个旧版本升级还是直接安装最新版本都能不会报错了。ok,细节很重要。