多线程并发操作数据库以及数据库升级

1 多线程并发操作数据库会导致数据库异常:

         例1:cursor会为空的情况,打印cursor的时候不为空,使用的时候就为空了,原因考虑是,多线程操作数据库导致数据库异常

         例2:提示正在尝试打开一个已经被关闭的数据库:在多线程访问数据库的时候会出现这样的异常: 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 : 或 java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase:

        解决方法(一)

当你在多线程中只使用一个SQLiteDatabase的引用时,需要格外注意你SQLiteDataBase.close()调用的时机,因为你是使用的同一个引用,比如在一个线程中当一个Add操作结束后立刻关闭了数据库连接,而另一个现场中正准备执行查询操作,但此时db已经被关闭了,然后就会报异常错误。此时一般有三种解决方案,①简单粗暴给所有的CRUD添加一个 synchronized关键字;②永远不关闭数据库连接,只在最后退出是关闭连接。其实每次执行getWriteableDataBase()或getReadableDatabase()方法时,如果有已经建立的数据库连接则直接返回(例外:如果旧的连接是以只读方式打开的,则会在新建连接成功的前提下,关闭旧连接),所以程序中将始终保持有且只有一个数据库连接(前提是单例),资源消耗的很少。③可以自己进行引用计数,简单示例代码如下:
 
//打开数据库方法
public synchronized SQLiteDatabase openDatabase() {
if (mOpenCounter.incrementAndGet() == 1) {
// Opening new database
try { 
mDatabase = sInstance.getWritableDatabase();
} catch (Exception e) {      f
mDatabase = sInstance.getReadableDatabase();
}
}
return mDatabase;
}
 
//关闭数据库方法
public synchronized void closeDatabase() {
if (mOpenCounter.decrementAndGet() == 0) {
// Closing database
mDatabase.close();
}
 }


解决方法(二)

这样的异常信息,Sqlite 自身是不支持多线程同时操作的,下面呢我们给出一个解决方案并列出一些项目中用到的代码。

我们会用到AtomicInteger,一个提供原子操作的Integer的类。因为Android 依托强大的jdk在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口,由此我们可以做一个DatabaseManager 这样的类,具体代码见下面的代码块:

public class DatabaseManager {
  private AtomicInteger mOpenCounter = new AtomicInteger();
  private static DatabaseManager instance;  
  private static SQLiteOpenHelper mDatabaseHelper;  
  private SQLiteDatabase mDatabase; 
  public static synchronized void initializeInstance(SQLiteOpenHelper helper) {  
    if (instance == null) {  
      instance = new DatabaseManager();  
      mDatabaseHelper = helper;  
    }  
  }  
  public static synchronized DatabaseManager getInstance(SQLiteOpenHelper helper) {  
    if (instance == null) {  
      initializeInstance(helper);
    }  
    return instance;  
  }  
  public synchronized SQLiteDatabase getWritableDatabase() {  
    if(mOpenCounter.incrementAndGet() == 1) {  
      // Opening new database  
      mDatabase = mDatabaseHelper.getWritableDatabase();  
    }  
    return mDatabase;  
  }  
  public synchronized SQLiteDatabase getReadableDatabase() {  
    if(mOpenCounter.incrementAndGet() == 1) {  
      // Opening new database  
      mDatabase = mDatabaseHelper.getReadableDatabase();  
    }  
    return mDatabase;  
  }  
  public synchronized void closeDatabase() {  
    if(mOpenCounter.decrementAndGet() == 0) {  
      // Closing database  
      mDatabase.close();  
    }  
  }

在我们进行关闭数据库的时候判断

mOpenCounter.decrementAndGet() == 0 (更新器管理的给定对象的字段的当前值为0)的时候才正式关闭数据库,就不会出现上述异常。

用方式呢,在我们操作数据库逻辑代码中如下使用
首相要取得
mDatabaseManager = DatabaseManager.getInstance(mContext);
对象
/***
 * 判断表中是否有值
 */
public boolean isExistTabValus() {
  boolean flag = false;
  SQLiteDatabase db = mDatabaseManager.getReadableDatabase();//获取一个可读的数据库对象
  Cursor curcor = null;
  try {
    curcor = db.rawQuery("select * from tab ", null);
    while (curcor.moveToNext()) {
      if (curcor.getCount() > 0) {
        flag = true;
      }
    }
  } catch (Exception e) {
    Log.e(TAG, "isExistTabValus  error");
  } finally {
    if (curcor != null) {
      curcor.close();
    }
    mDatabaseManager.closeDatabase();//关闭数据库
  }
  return flag;
}

上面提供一个使用方法,现在项目中使用这种方法关于数据库的操作从未没有出现并发的问题,大家可以尝试一下。


解决方法(三)

1 为了防止多线程操作导致访问数据库异常,在数据库管理类中使用同一个handler来管理消息队列。所有的数据库操作都在一个handler里处理来操作唯一一个单例数据库代码如下:

package com.example.aaviewpager;


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;


import com.example.aaviewpager.DatebaseHandleListener.DbListener;
import com.org.xdbutils.DbManager;
import com.org.xdbutils.DbManager.DbOpenListener;
import com.org.xdbutils.DbManager.DbUpgradeListener;
import com.org.xdbutils.dbutil;
import com.org.xdbutils.db.sqlite.WhereBuilder;


/***
 * Parent test = db.selector(Parent.class).where("id", "in", new int[]{1, 3,
 * 6}).findFirst(); long count = db.selector(Parent.class).where("name", "LIKE",
 * "w%").and("age", ">", 32).count(); List testList =
 * db.selector(Parent.class).where("id", "between", new String[]{"1",
 * "5"}).findAll();
 * */
public class DateBaseManager {
private static final String DATABASE_NAME = "conference.db";
private static DateBaseManager instance;
private static DbManager db;


private DateBaseManager() {
db = getDb();
initThread();
};


public static DateBaseManager getInstance() {
if (instance == null) {
instance = new DateBaseManager();
}
if (db == null) {
db = getDb();
}
return instance;
}


/**
 * 获取 xutils �?db 模块
 * 
 * @return
 */
private static DbManager getDb() {
final DbManager.DaoConfig config = new DbManager.DaoConfig();
config.setDbName(DATABASE_NAME);
// config.setDbDir(LogFileUtil.getDiskCacheDir(BaseApplication.getBaseApplication(), "database"));// 设置数据库目�?不设置默认在
// app
// 的私有目录中;
config.setDbVersion(2);
config.setDbOpenListener(new DbOpenListener() {


@SuppressLint("NewApi")
@Override
public void onDbOpened(DbManager db) {
// TODO Auto-generated method stub
db.getDatabase().enableWriteAheadLogging();// �?��wal, 提高写入速度
}
});
config.setDbUpgradeListener(new DbUpgradeListener() {

@Override
public void onUpgrade(DbManager db, int oldVersion, int newVersion) {
// TODO Auto-generated method stub


}
});
return dbutil.getDb(config);
}



public void close() {


try {
if (db != null) {
db.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
uninitSynchronizeHandler();
if (instance != null) {
instance = null;
}


}


private Handler dbHandler;
private Thread dbThread;


/**
 * 该方法初始化同步数据的线程�?如果参数为空,则方法会创建一个默认线程来处理数据的同步�? 该方法只会在第一次调用时有效�?  * 
 * @param looper
 */
private void initThread() {
if (dbHandler != null) {
return;
}
dbThread = new DBThread();
dbThread.setName("DBThread");
dbThread.start();
}


/**
 * 该方法在程序�?��时被调用,且必须被调用,否则可能会出现内容泄漏的问题�?  */
public void uninitSynchronizeHandler() {
if (dbHandler != null) {
dbHandler.getLooper().quit();
dbHandler = null;
}
if (dbThread != null) {
// if(synchronizeThread.isAlive()){
// synchronizeThread.destroy();
// }
}
}


private void sendDbMessage(Message msg) {
if (dbHandler == null) {
initHandler();
}
dbHandler.sendMessage(msg);
}


private final static int Msg_updateModle = 0x001;
private final static int Msg_deleteModle = 0x002;
private final static int Msg_deleteAllModle = 0x003;
private final static int Msg_queryModel = 0x004;


private void initHandler() {
dbHandler = new Handler(Looper.myLooper()) {


@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
MessageParams params=null;
switch (msg.what) {


case Msg_updateModle:
params =(MessageParams) msg.obj;
if (params == null || params.handle == null){
if(params==null)
System.out.println("------dbManager Msg_updateModle null return:params==null");
else
System.out.println("------dbManager Msg_updateModle null return:params.handle==null");
return;
}
update(params.handle, params.listener);
break;
case Msg_deleteModle:
params =(MessageParams) msg.obj;
if (params == null || params.handle == null)
return;
delete(params.handle, params.listener);
break;
case Msg_deleteAllModle:
params =(MessageParams) msg.obj;
if (params == null || params.handle == null)
return;
deleteAll(params.handle, params.listener);
break;
case Msg_queryModel:
params =(MessageParams) msg.obj;
if (params == null || params.handle == null){
if(params==null)
Log.d("db","------dbManager Msg_queryModel null return:params==null");
else
Log.d("db","------dbManager Msg_queryModel null return:params.handle==null");
return;
}
query(params.handle, params.build,params.listener);
break;
default:
break;
}
}
};
}


public class DBThread extends Thread {


@Override
public void run() {
// TODO Auto-generated method stub
// super.run();
Looper.prepare();
initHandler();
Looper.loop();
}
}


public void updateObject(DatebaseHandleListener conctoll, DbListener listener) {
if(conctoll==null)
System.out.println("-------dbmanger:updateHadle handle ==null");
MessageParams params = new MessageParams(conctoll, listener);
Message msg = Message.obtain();
msg.obj = params;
msg.what = Msg_updateModle;
sendDbMessage(msg);
}


private void update(DatebaseHandleListener conctoll, DbListener listener) {
conctoll.updateObject(db,listener);
}


public void dleleteObject(DatebaseHandleListener handle, DbListener listener) {
MessageParams params = new MessageParams(handle, listener);
Message msg = Message.obtain();
msg.obj = params;
msg.what = Msg_deleteModle;
sendDbMessage(msg);
}
private void delete(DatebaseHandleListener conctoll, DbListener listener) {
conctoll.deleteObject(db,listener);
}

public void deleteAllHandle(DatebaseHandleListener handle, DbListener listener) {
MessageParams params = new MessageParams(handle, listener);
Message msg = Message.obtain();
msg.obj = params;
msg.what = Msg_deleteAllModle;
sendDbMessage(msg);
}


private void deleteAll(DatebaseHandleListener conctoll, DbListener listener) {
conctoll.deleteAll(db,listener);
}

public void queryObject(DatebaseHandleListener handle,WhereBuilder build, DbListener listener) {
MessageParams params = new MessageParams(handle,build, listener);
Message msg = Message.obtain();
msg.obj = params;
msg.what = Msg_queryModel;
sendDbMessage(msg);
}

private void query(DatebaseHandleListener conctoll,WhereBuilder b, DbListener listener) {
conctoll.select(db, b, listener);
}





private List dbListeners;


public void addListener(DbListener listener) {
if (dbListeners == null) {
dbListeners = new ArrayList();
}
if (!dbListeners.contains(listener)) {
dbListeners.add(listener);
}
}


public void removeListener(DbListener listener) {
if (dbListeners == null) {
return;
}
if (dbListeners.contains(listener)) {
dbListeners.remove(listener);
}
}




class MessageParams {
DbListener listener;
DatebaseHandleListener handle;
WhereBuilder build;
public MessageParams(DatebaseHandleListener handle, DbListener listener) {
this.handle = handle;
this.listener = listener;
}
public MessageParams(DatebaseHandleListener handle,WhereBuilder b, DbListener listener) {
this.handle = handle;
this.listener = listener;
this.build = b;
}
}
}


2 定义一个数据库操作方法的接口类


package com.example.aaviewpager;


import java.util.List;


import com.org.xdbutils.DbManager;
import com.org.xdbutils.db.sqlite.WhereBuilder;


/**
 * 数据库操作接�? * 
 * @param
 */
public interface DatebaseHandleListener {


boolean updateObject(DbManager db, DbListener listener);


boolean deleteObject(DbManager db, DbListener listener);


boolean deleteAll(DbManager db, DbListener listener);


T selectSingle(DbManager db, WhereBuilder b, DbListener listener);


List select(DbManager db, WhereBuilder b, DbListener listener);


// 数据回调接口管理
public interface DbListener {
/****数据库操作回*/
void getResult(int cmd, boolean isSuccess, Object object);
}


}

3 声明一个实体类JavaBean来实现数据库操作方法的接口类 代码如下:


  package com.example.aaviewpager;


import java.util.List;


import com.nucomtech.conference.model.dbmodel.DatabaseModleCmd;
import com.org.xdbutils.DbManager;
import com.org.xdbutils.db.annotation.Column;
import com.org.xdbutils.db.annotation.Table;
import com.org.xdbutils.db.sqlite.WhereBuilder;
import com.org.xdbutils.ex.DbException;


@Table(name = "loginInfo")
public class LoginUserModel implements DatebaseHandleListener{


@Column(name = "_id",isId = true)
private long _id;
@Column(name = "name")
private String name;
@Column(name = "password")
private String password;

public LoginUserModel(){

}
public long get_id() {
return _id;
}


public void set_id(long _id) {
this._id = _id;
}


public String getName() {
return name;
}


public void setName(String name) {
this.name = name;
}


public String getPassword() {
return password;
}


public void setPassword(String password) {
this.password = password;
}


@Override
public boolean updateObject(DbManager db,
com.example.aaviewpager.DatebaseHandleListener.DbListener listener){
// TODO Auto-generated method stub
// return false;
try {
db.selector(LoginUserModel.class).where("name", "=", 123);

db.saveOrUpdate(this);
listenerResult(listener,DatabaseModleCmd.LoginUserModle_saveOrUpdate, true,this);
db.save(this);
return true;
} catch (DbException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return true;
}


private void listenerResult(
com.example.aaviewpager.DatebaseHandleListener.DbListener listener,
int cmd, boolean isSuccess,
LoginUserModel model) {
// TODO Auto-generated method stub
if(listener == null){
return;
}
listener.getResult(cmd, isSuccess, model);
}
@Override
public boolean deleteObject(DbManager db,
com.example.aaviewpager.DatebaseHandleListener.DbListener listener) {
// TODO Auto-generated method stub
return false;
}


@Override
public boolean deleteAll(DbManager db,
com.example.aaviewpager.DatebaseHandleListener.DbListener listener) {
// TODO Auto-generated method stub
return false;
}


@Override
public LoginUserModel selectSingle(DbManager db, WhereBuilder b,
com.example.aaviewpager.DatebaseHandleListener.DbListener listener) {
// TODO Auto-generated method stub
return null;
}


@Override
public List select(DbManager db, WhereBuilder b,
com.example.aaviewpager.DatebaseHandleListener.DbListener listener) {
// TODO Auto-generated method stub
return null;
}
@Override
public String toString() {
return "LoginUserModel [_id=" + _id + ", name=" + name + ", password="
+ password + "]";
}




}

4 在Activity中声明javaBean实体类的对象(修改的属性new成对象作为访问数据库的参数),

package com.example.aaviewpager;


import java.util.ArrayList;
import java.util.List;


import com.example.aaviewpager.DatebaseHandleListener.DbListener;
import com.nucomtech.conference.model.dbmodel.DatabaseModleCmd;
import com.nucomtech.conference.model.dbmodel.LoginUserModel;
import com.nucomtech.conference.serviceproxy.xmppproxy.MessageState;
import com.nucomtech.conference.view.iview.ILoginView;


import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.app.ActionBar.LayoutParams;
import android.app.Activity;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.view.Menu;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.LinearLayout;


public class MainActivity extends Activity {


private ViewPager mVp;
private LinearLayout ll_points;
private int preposition = 0;
private int[] images;
@Override
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);


LoginUserModel loginUserModel = new LoginUserModel();
loginUserModel.set_id(123456);
DateBaseManager.getInstance().updateObject(loginUserModel, new DbListener() {

@Override
public void getResult(int cmd, boolean isSuccess, Object object) {
// TODO Auto-generated method stub
if (cmd == DatabaseModleCmd.LoginUserModle_saveOrUpdate) {


} else if (cmd == DatabaseModleCmd.LoginUserModle_select) {
if (isSuccess) {
LoginUserModel user = (LoginUserModel) object;

//   数据库访问成功后做一些其它的操作
// if (user != null)
// getView().onLoginResult(ILoginView.Type_loadAccount, MessageState.State_Success,
// user.getUserName());
}
}
}
});


}

}



数据库升级的意义

我们在开发Android应用的时候,不可避免地要使用数据库。而数据库的结构在第一版的时候定下来,之后发布功能更新,或增加业务逻辑,原来的数据库结构可能就不适用了。而如果数据库的结构与之前版本的结构不同,新版本的应用读取旧数据库肯定会出问题。解决办法只有两种:

1.让用户卸载老版本再安装新的程序;

2.软件自行更新数据库结构。

第一种办法很明显不具备可操作性,而且用户一旦卸载软件,数据就丢失了,这是不能容忍的事情。因此,作为开发者必须妥善处理数据库的升级问题。

当然了,有的同学会说,这问题没意义嘛。我们在设计软件的时候就把数据库设计得完备一点就好了,一开始就考虑周全,以后再也不用管升级的事情。这种方法理论上虽然可行,但实际操作起来是非常困难的,除非你在开发定制软件(例如某些和硬件结合的产品,其硬件发布之后就不再更新或很少更新,这样的软件确实没多大改动)。对于面向市场的应用来说,很可能在立项之初根本不会知道以后会增加哪些功能。这样,我们终究还是要面对这个问题。

保留数据的升级

现在以一个假想的程序数据库来谈如何保留原有的数据库进行升级。为直观起见,这里在每次软件版本安装之后导出了数据库文件,并使用SQLite Studio显示表结构和内容,过程从略。并且为了代码的简洁,省略了一些异常处理。我们假设数据库有这样三个版本:

v1:t_user(username, password);

v2: t_user(username, password),  t_region(region, code);

v3: t_user(username, password),  t_region(region, code, country);

可以看出,第一次升级增加了一张表,而第二次升级修改了表的定义。

创建和升级数据表的基本知识

我们在使用数据库之前,基本上会自定义一个类继承自SQLiteOpenHelper。该类的其中一个构造函数形式是这样的(另一个多出来一个DatabaseErrorHandler):

    public SQLiteOpenHelper(Context context, String name, 
                    CursorFactory factory, int version) {
        this(context, name, factory, version, null);
    }

这个构造函数里面的version参数即是我们设定的版本号。第一次使用数据库时传递的这个版本将被系统记录,并调用SQLiteOpenHelper#onCreate()方法进行建表操作。后续传入的版本如果比这个高,则会调用SQLiteOpenHelper#onUpgrade()方法进行升级。

增加一张表

很明显,增加新表(或者删除没有外键的表)的操作代价很小,只需要在onUpgrade()中写好建表操作和插入初始数据就好了。

  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    if (oldVersion==1){
      db.execSQL("CREATE TABLE t_region(_id integer primary key"
       + "autoincrement, region varchar, code varchar)");
      //insert data...
    }
  }

多线程并发操作数据库以及数据库升级_第1张图片

多线程并发操作数据库以及数据库升级_第2张图片

从上面的图里可以看到,新版本的数据库中已经有t_region表了。

修改表定义

SQLite数库对ALTER TABLE命令支持非常有限,只能在表末尾添加列,不能修改列定义,不能删除已有的列。那么如果要修改表呢?我们可以采用临时表的办法。具体来说有四步:

  1. 将现有表重命名为临时表;

  2. 创建新表;

  3. 将临时表的数据导入新表(注意处理修改的列);

  4. 删除临时表。

以例子中的v2升级到v3为例:

  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    if (oldVersion==2){
      db.execSQL("ALTER TABLE t_region RENAME TO t_region_temp");
      db.execSQL("CREATE TABLE t_region(_id integer primary key"
           + "autoincrement, region varchar, code varchar, "
           + "country varchar)");
      db.execSQL("insert into t_region(_id, region, code, country) " 
          + "select _id, region, code, \"CHINA\" from t_region_temp");
      db.execSQL("DROP TABLE t_region_temp");
    }
  }

多线程并发操作数据库以及数据库升级_第3张图片

多线程并发操作数据库以及数据库升级_第4张图片

需要注意:

  • 重命名表的SQL格式为ALTER TABLE  RENAME TO ;

  • 导入新数据的insert into select语句中 不能出现values关键字 ;

  • 记得删除临时表 

跨越版本的升级

处理好了单个版本的升级,还有一个更加棘手的问题:如果应用程序发布了多个版本,以致出现了三个以上数据库版本, 如何确保所有的用户升级应用后数据库都能用呢?有两种方式:

方式一:确定 相邻版本 的差别,从版本1开始依次迭代更新,先执行v1到v2,再v2到v3……

方式二:为 每个版本 确定与现在数据库的差别,为每个case撰写专门的升级代码。

方式一的优点是每次更新数据库的时候只需要在onUpgrade方法的末尾加一段从上个版本升级到新版本的代码,易于理解和维护,缺点是当版本变多之后,多次迭代升级可能需要花费不少时间,增加用户等待;

方式二的优点则是可以保证每个版本的用户都可以在消耗最少的时间升级到最新的数据库而无需做无用的数据多次转存,缺点是强迫开发者记忆所有版本数据库的完整结构,且每次升级时onUpgrade方法都必须全部重写。

以上简单分析了两种方案的优缺点,它们可以说在花费时间上是刚好相反的,至于如何取舍,可能还需要结合具体情况分析。


      

你可能感兴趣的:(数据库)