18.移动架构数据库升级解决方案

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 今天的数据库升级将通过这个脚本文件完成,我们假设目前版本迭代中,已经上线了V001 V002版本,近期将要上线V003。首先先理清三个版本的数据库情况,三个版本中都涉及一个个数据库文件(暂不考虑分库的情况),user.

今天的数据库升级将通过这个脚本文件完成,我们假设目前版本迭代中,已经上线了V001 V002版本,近期将要上线V003。首先先理清三个版本的数据库情况,三个版本中都涉及一个个数据库文件(暂不考虑分库的情况),user.db,user.db中有一个表tb_user,和tb_photo表

在V001版本中
tb_user表有3个属性,user_id,name,password
tb_photo表有2个属性,path,time

在V002版本中
tb_user表有4个属性,user_id,name,password,lastLoginTime
tb_photo表有3个属性,path,time,sendTime

在V003版本中(即将上线的版本)
tb_user表有5个属性,user_id,name,password,lastLoginTime,lastExistTime
tb_photo表有4个属性,path,time,sendTime,size

此时上线,市场上涉及到的用户升级会有几种情况?

1.V001版本升级到V002版本
2.V002版本升级到V003版本
3.V001版本升级到V003版本

由于版本较少,所以涉及到的升级组合并不多,实际项目中可能会有很多,这里只举个简单的例子。我们每升级一个版本,都要考虑到目前市场上存在的所有版本升级的情况,所以这一点需要在我们的脚本文件有所体现

<!-- 保证 UTF-8编码 -->
<updateXml>
    <!--升级到V002需要创建的表结构-->
    <createVersion version="V002">
        <createDb name="user">
            <sql_createTable>
                create table if not exists tb_user(
                user_id Integer primary key,
                name TEXT,
                password TEXT,
                lastLoginTime TEXT
                );
            </sql_createTable>
            <sql_createTable>
                create table if not exists tb_photo(
                path TEXT,
                time TEXT,
                sendTime TEXT
                );
            </sql_createTable>
        </createDb>
    </createVersion>

    <!--升级到V003需要创建的表结构-->
    <createVersion version="V003">
        <createDb name="user">
            <sql_createTable>
                create table if not exists tb_user(
                user_id Integer primary key,
                name TEXT,
                password TEXT,
                lastLoginTime TEXT,
                lastExistTime TEXT
                );
            </sql_createTable>

            <sql_createTable>
                create table if not exists tb_photo(
                path TEXT,
                time TEXT,
                sendTime TEXT,
                size TEXT
                );
            </sql_createTable>
        </createDb>
    </createVersion>

    <!--从V001升级到V003-->
    <updateStep
        versionFrom="V001"
        versionTo="V003">
        <updateDb name="user">
            <sql_before>alter table tb_user rename to bak_tb_user;</sql_before>
            <sql_after>
                insert into tb_user(user_id,name, password)
                select user_id,name,password
                from bak_tb_user;
            </sql_after>
            <sql_after>
                drop table if exists bak_tb_user;
            </sql_after>
        </updateDb>

        <updateDb name="user">
            <sql_before>alter table tb_photo rename to bak_tb_photo;</sql_before>
            <sql_after>
                insert into tb_photo(path,time)
                select path,time
                from bak_tb_photo;
            </sql_after>
            <sql_after>
                drop table if exists bak_tb_photo;
            </sql_after>
        </updateDb>
    </updateStep>
    <!--从V001升级到V002-->
    <updateStep
        versionFrom="V001"
        versionTo="V002">
        <updateDb name="user">
            <sql_before>alter table tb_user rename to bak_tb_user;</sql_before>
            <sql_after>
                insert into tb_user(user_id,name, password)
                select user_id,name,password
                from bak_tb_user;
            </sql_after>
            <sql_after>
                drop table if exists bak_tb_user;
            </sql_after>
        </updateDb>

        <updateDb name="user">
            <sql_before>alter table tb_photo rename to bak_tb_photo;</sql_before>
            <sql_after>
                insert into tb_photo(path,time)
                select path,time
                from bak_tb_photo;
            </sql_after>
            <sql_after>
                drop table if exists bak_tb_photo;
            </sql_after>
        </updateDb>
    </updateStep>
    <!--从V002升级到V003-->
    <updateStep
        versionFrom="V002"
        versionTo="V003">
        <updateDb name="user">
            <sql_before>alter table tb_user rename to bak_tb_user;</sql_before>
            <sql_after>
                insert into tb_user(user_id,name, password,lastLoginTime)
                select user_id,name, password,lastLoginTime
                from bak_tb_user;
            </sql_after>
            <sql_after>
                drop table if exists bak_tb_user;
            </sql_after>
        </updateDb>

        <updateDb name="user">
            <sql_before>alter table tb_photo rename to bak_tb_photo;</sql_before>
            <sql_after>
                insert into tb_photo(path,time,sendTime)
                select path,time,sendTime
                from bak_tb_photo;
            </sql_after>
            <sql_after>
                drop table if exists bak_tb_photo;
            </sql_after>
        </updateDb>

    </updateStep>
</updateXml>

脚本中主要包括两个功能,数据库的创建和更新,考虑到每个用户升级的版本可能不同,所以每一种升级组合情况都要照顾到,比如说createVersion 节点中关于数据库创建的操作,版本V001的用户在选择升级的时候,有可能升级到V002也有可能升级到V003,所以需要针对这两个版本创建不同的数据库表结构。在更新数据库表的时候又需要根据不同的升级方向对表结构进行不同的处理,这些都会在脚本中做不同处理。

升级原理

升级原理很简单,在app完成升级启动时,将原有的数据库表重命名为bak_xxxx,从脚本文件中读取到创建新版本表结构的命令,创建一个新的表格,此时,这个表是空的,然后将重命名后的表bak_xxxx中的数据插入到新表中,插入成功,删除bak_xxxx,实现数据库升级


img_e3e3e54187acf7e18f70643e5bb15934.png
数据库升级原理.png
脚本解析

接下来看代码中如何实现,本次数据库升级以脚本为中心,所以一开始的任务就是读取脚本信息,封装成对象,因此,定义了以下的对象来存储脚本信息

脚本最外层的updateXml对应的对象:UpdateDbXml,可以对照脚本文件来阅读这个对象,对象中封装了两个集合,对应脚本中的updateStep和createVersion节点

package com.example.sqlite.rzm.update;

import android.database.sqlite.SQLiteDatabase;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

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

public class UpdateDbXml {
    /**
     * 升级脚本列表
     */
    private List<UpdateStep> updateSteps;

    /**
     * 升级版本
     */
    private List<CreateVersion> createVersions;

    public UpdateDbXml(Document document) {
        {
            // 获取到xml中所有updateStep节点组成的集合
            NodeList updateSteps = document.getElementsByTagName(SqlConstant.KEY_UPDATE_STEP);
            this.updateSteps = new ArrayList<UpdateStep>();
            for (int i = 0; i < updateSteps.getLength(); i++) {
                //for循环遍历每一个updateStep节点,强转成Element对象
                Element ele = (Element) (updateSteps.item(i));
                //把每一个Element封装成Java对象UpdateStep
                UpdateStep step = new UpdateStep(ele);
                //加入集合
                this.updateSteps.add(step);
            }
        }
        {
            /**
             * 获取各升级版本
             */
            NodeList createVersions = document.getElementsByTagName(SqlConstant.KEY_CREATE_VERSION);
            this.createVersions = new ArrayList<CreateVersion>();
            for (int i = 0; i < createVersions.getLength(); i++) {
                Element ele = (Element) (createVersions.item(i));
                //封装成CreateVersion对象
                CreateVersion cv = new CreateVersion(ele);
                this.createVersions.add(cv);
            }
        }
    }

    public List<UpdateStep> getUpdateSteps() {
        return updateSteps;
    }

    public void setUpdateSteps(List<UpdateStep> updateSteps) {
        this.updateSteps = updateSteps;
    }

    public List<CreateVersion> getCreateVersions() {
        return createVersions;
    }

    public void setCreateVersions(List<CreateVersion> createVersions) {
        this.createVersions = createVersions;
    }

}

UpdateStep和CreateVersion节点的封装

package com.example.sqlite.rzm.update;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

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


public class UpdateStep {
    /**
     * 旧版本
     */
    private String versionFrom;

    /**
     * 新版本
     */
    private String versionTo;

    /**
     * 更新数据库脚本
     */
    private List<UpdateDb> updateDbs;

    // ==================================================

    public UpdateStep(Element ele) {
        //就像findViewById一样获取这个节点的属性attribute
        versionFrom = ele.getAttribute(SqlConstant.KEY_VERSION_FROM);
        versionTo = ele.getAttribute(SqlConstant.KEY_VERSION_TO);
        updateDbs = new ArrayList<UpdateDb>();

        //updateDb是一个跟updateStep一样的元素Element,这里会获取
        // 到所有同类型的元素组成的集合
        NodeList dbs = ele.getElementsByTagName(SqlConstant.KEY_UPDATE_DB);
        for (int i = 0; i < dbs.getLength(); i++) {
            //得到每一个updateDb元素封装成UpdateDb对象
            Element db = (Element) (dbs.item(i));
            UpdateDb updateDb = new UpdateDb(db);
            //加入集合
            this.updateDbs.add(updateDb);
        }
    }

    public List<UpdateDb> getUpdateDbs() {
        return updateDbs;
    }

    public void setUpdateDbs(List<UpdateDb> updateDbs) {
        this.updateDbs = updateDbs;
    }

    public String getVersionFrom() {
        return versionFrom;
    }

    public void setVersionFrom(String versionFrom) {
        this.versionFrom = versionFrom;
    }

    public String getVersionTo() {
        return versionTo;
    }

    public void setVersionTo(String versionTo) {
        this.versionTo = versionTo;
    }

}

package com.example.sqlite.rzm.update;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

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


public class CreateVersion {
    /**
     * 版本信息
     */
    private String version;

    /**
     * 创建数据库表脚本
     */
    private List<CreateDb> createDbs;

    public CreateVersion(Element ele) {
        version = ele.getAttribute(SqlConstant.KEY_DB_VERSION);

        {
            createDbs = new ArrayList<CreateDb>();
            NodeList cs = ele.getElementsByTagName(SqlConstant.KEY_CREATE_DB);
            for (int i = 0; i < cs.getLength(); i++) {
                Element ci = (Element) (cs.item(i));
                CreateDb cd = new CreateDb(ci);
                this.createDbs.add(cd);
            }
        }
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public List<CreateDb> getCreateDbs() {
        return createDbs;
    }

    public void setCreateDbs(List<CreateDb> createDbs) {
        this.createDbs = createDbs;
    }

}

往脚本内层切入还有CreateDb和UpdateDb两个节点

package com.example.sqlite.rzm.update;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

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


public class CreateDb {

    /**
     * 数据库表名
     */
    private String name;

    /**
     * 创建表的sql语句集合
     */
    private List<String> sqlCreates;

    public CreateDb(Element ele) {
        name = ele.getAttribute(SqlConstant.KEY_DB_NAME);

        {
            sqlCreates = new ArrayList<String>();
            NodeList sqls = ele.getElementsByTagName(SqlConstant.KEY_SQL_CREATE_TABLE);
            for (int i = 0; i < sqls.getLength(); i++) {
                String sqlCreate = sqls.item(i).getTextContent();
                this.sqlCreates.add(sqlCreate);
            }
        }
    }

    public String getName() {
        return name;
    }

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

    public List<String> getSqlCreates() {
        return sqlCreates;
    }

    public void setSqlCreates(List<String> sqlCreates) {
        this.sqlCreates = sqlCreates;
    }

}
package com.example.sqlite.rzm.update;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

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

public class UpdateDb {
    /**
     * 数据库名称
     */
    private String dbName;

    /**
     *
     */
    private List<String> sqlBefores;

    /**
     *
     */
    private List<String> sqlAfters;

    public UpdateDb(Element ele) {
        //获取name属性
        dbName = ele.getAttribute(SqlConstant.KEY_DB_NAME);

        sqlBefores = new ArrayList<String>();
        sqlAfters = new ArrayList<String>();

        //这个元素中还包含有别的元素节点,使用同样的方式获取
        {
            NodeList sqls = ele.getElementsByTagName(SqlConstant.KEY_SQL_BEFORE);
            for (int i = 0; i < sqls.getLength(); i++) {
                //获取到这个节点中的文字内容
                String sql_before = sqls.item(i).getTextContent();
                //存入集合
                this.sqlBefores.add(sql_before);
            }
        }
        //同上
        {
            NodeList sqls = ele.getElementsByTagName(SqlConstant.KEY_SQL_AFTER);
            for (int i = 0; i < sqls.getLength(); i++) {
                String sql_after = sqls.item(i).getTextContent();
                this.sqlAfters.add(sql_after);
            }
        }

    }

    public String getDbName() {
        return dbName;
    }

    public void setDbName(String dbName) {
        this.dbName = dbName;
    }

    public List<String> getSqlBefores() {
        return sqlBefores;
    }

    public void setSqlBefores(List<String> sqlBefores) {
        this.sqlBefores = sqlBefores;
    }

    public List<String> getSqlAfters() {
        return sqlAfters;
    }

    public void setSqlAfters(List<String> sqlAfters) {
        this.sqlAfters = sqlAfters;
    }
}

这里只需要知道以点,这些对象是为了解析脚本文件而创建的即可

升级过程

每当我们发布一个新版本,用户下载了新版本apk,进行覆盖安装,在安装之前需要有一步保存当前版本的操作,目的是什么?因为当app版本升级之后,原有数据库表结构还是保持上一版本的状态,而我们要实现将其结构升级到当前版本,需要知道上一个版本的数据库结构才能进行相应的升级。所以这一步必不可少

//保存上一版本版本号
updateManager.saveVersionInfo("V001");

等覆盖安装完成,app启动的时候,就需要去读取这个保存的版本号,从而根据上一版本和当前版本的版本信息从脚本文件中读取相应的命令进行升级

private boolean getLocalVersionInfo() {
        boolean ret = false;

        File file = new File(parentFile, UPDATE_INFO);

        if (file.exists()) {
            int byteRead = 0;
            byte[] tempBytes = new byte[100];
            StringBuilder stringBuilder = new StringBuilder();
            InputStream in = null;
            try {
                in = new FileInputStream(file);
                while ((byteRead = in.read(tempBytes)) != -1) {
                    stringBuilder.append(new String(tempBytes, 0, byteRead));
                }
                String infos = stringBuilder.toString();
                //lastVersion是当前版本升级之前的版本,当前版本是从这个版本升级过来的
                //这个信息是通过saveVersionInfo保存的
                lastVersion = infos;
                ret = true;
            } catch (Exception e) {

            } finally {
                if (null != in) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    in = null;
                }
            }
        }

        return ret;
    }

拿到版本信息之后就可以开始解析脚本文件了,脚本文件中设置了和每一个版本升级所需要的sql命令,所以在解析之前必须要获取到升级版本信息才行。

            //拿到当前版本
            String currentVersion = getVersionName(context);
            //拿到上一个版本
            String lastVersion = this.lastVersion;
            UpdateStep updateStep = analyseUpdateStep(updateDbxml, lastVersion, currentVersion);

            if (updateStep == null) {
                return;
            }
            List<UpdateDb> updateDbs = updateStep.getUpdateDbs();
            CreateVersion createVersion = analyseCreateVersion(updateDbxml, currentVersion);

如上,分别从脚本文件中解析出对应的updateStep和createVersion对象,这两个对象中封装了从脚本中解析出来的操作命令,接下来我们就要去执行这些命令,实现数据库的升级

                //****************   把数据库都拷贝一份到backDb文件夹中  *********************
                backUpDb(updateDbs);
                //****************   把数据库都拷贝一份到backDb文件夹中  *********************

                //****************   将原有数据库重命名  *********************
                executeDb(updateDbs, -1);
                //****************   将原有数据库重命名  *********************

                //****************   创建新的数据库和表  *********************
                executeCreateVersion(createVersion, false);
                //****************   创建新的数据库和表  *********************

                //****************   将重命名后的数据库中的数据插入新建的数据库表中,然后删除这个重命名后的数据库
                executeDb(updateDbs, 1);
                //****************   将重命名后的数据库中的数据插入新建的数据库表中,然后删除这个重命名后的数据库

具体的过程可以查看GitHub源码,这里不再一一介绍,总之就是一个解析xml脚本,执行sql命令生成或者更新表的过程。这里贴一下核心逻辑类UpdateManager的代码

package com.example.sqlite.rzm.update;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;
import android.text.TextUtils;

import com.example.sqlite.rzm.fileutil.FileUtil;

import org.w3c.dom.Document;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;


public class UpdateManager {
    /**
     * 数据库存放的根目录 目前是内存卡下的一个目录ROOT_DB_DIR
     */
    private final String ROOT_DB_DIR = "update";
    /**
     * 数据库备份的目录ROOT_BACK_UP_DIR,存放于ROOT_DB_DIR中
     */
    private final String ROOT_BACK_UP_DIR = "backDb";
    /**
     * 版本更新的相关信息存放文件
     */
    private final String UPDATE_INFO = "update.txt";
    /**
     * 升级用的脚本文件
     */
    private final String UPDATE_SCRIPT_XML = "updateXml.xml";
    private File parentFile = new File(Environment.getExternalStorageDirectory(), ROOT_DB_DIR);

    private File bakFile = new File(parentFile, ROOT_BACK_UP_DIR);

    public UpdateManager() {
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        if (!bakFile.exists()) {
            bakFile.mkdirs();
        }

    }

    public void checkThisVersionTable(Context context) {
        //解析mxl文件,将解析到的信息封装到UpdateDbXml对象中
        UpdateDbXml xml = readDbXml(context);
        //获取当前版本信息
        String thisVersion = this.getVersionName(context);
        //获取到和当前升级版本匹配的CreateVersion,xml中的每一个createVersion都对应这一个
        //版本的升级处理,这里第二个参数表示的是升级后的版本号,为什么是升级后的,因为执行到这一步
        //时,是已经下载了新版本apk,覆盖安装之后读取了安装后的apk的版本号,有了这个版本号我们
        //就知道相应的数据库应该升级到哪个版本,然后选取相应的CreateVersion对象进行操作
        CreateVersion thisCreateVersion = analyseCreateVersion(xml, thisVersion);
        try {
            //升级数据库
            executeCreateVersion(thisCreateVersion, true);
        } catch (Exception e) {
        }

    }

    /**
     * 开始升级
     *
     * @param context
     */
    public void startUpdateDb(Context context) {
        UpdateDbXml updateDbxml = readDbXml(context);
        if (getLocalVersionInfo()) {
            //拿到当前版本
            String currentVersion = getVersionName(context);
            //拿到上一个版本
            String lastVersion = this.lastVersion;
            UpdateStep updateStep = analyseUpdateStep(updateDbxml, lastVersion, currentVersion);

            if (updateStep == null) {
                return;
            }
            List<UpdateDb> updateDbs = updateStep.getUpdateDbs();
            CreateVersion createVersion = analyseCreateVersion(updateDbxml, currentVersion);

            try {
                //****************   把数据库都拷贝一份到backDb文件夹中  *********************
                backUpDb(updateDbs);
                //****************   把数据库都拷贝一份到backDb文件夹中  *********************

                //****************   将原有数据库重命名  *********************
                executeDb(updateDbs, -1);
                //****************   将原有数据库重命名  *********************

                //****************   创建新的数据库和表  *********************
                executeCreateVersion(createVersion, false);
                //****************   创建新的数据库和表  *********************

                //****************   将重命名后的数据库中的数据插入新建的数据库表中,然后删除这个重命名后的数据库
                executeDb(updateDbs, 1);
                //****************   将重命名后的数据库中的数据插入新建的数据库表中,然后删除这个重命名后的数据库
            } catch (Exception e) {
                e.printStackTrace();
                //发生异常,做一些处理
            }
            //****************   删除备份数据库(备份数据库是为了防止升级过程中出现问题而设)  *********************
            deleteBackupDb(updateDbs);
            //****************   删除备份数据库  *********************
        }
    }

    /**
     * 升级成功,删除备份
     *
     * @param updateDbs
     */
    private void deleteBackupDb(List<UpdateDb> updateDbs) {
        List<String> dbNames = getUpdateDbNames(updateDbs);
        if (dbNames.size() > 0) {
            for (String dbName : dbNames) {
                File userFileBak = new File(bakFile.getAbsolutePath() + File.separator + dbName + ".db");
                if (userFileBak.exists()) {
                    userFileBak.delete();
                }
            }
        }

    }

    /**
     * 备份原有数据库
     *
     * @param updateDbs 包含当前版本需要跟新的数据库信息,只需要将需要更新的数据库备份即可
     */
    private void backUpDb(List<UpdateDb> updateDbs) {

        List<String> dbNames = getUpdateDbNames(updateDbs);
        if (dbNames.size() > 0) {
            for (String dbName : dbNames) {
                String user = parentFile.getAbsolutePath() + File.separator + dbName + ".db";
                String user_bak = bakFile.getAbsolutePath() + File.separator + dbName + ".db";
                FileUtil.CopySingleFile(user, user_bak);
            }
        }
    }

    /**
     * 获取到所有需要备份的数据库名称,存入集合返回,一般在未分库的情况下,只有一个结果
     *
     * @param updateDbs
     * @return
     */
    private List<String> getUpdateDbNames(List<UpdateDb> updateDbs) {
        ArrayList<String> dbNames = new ArrayList<>();
        //获取所有需要备份的数据库名称并去除重复
        if (updateDbs != null) {
            for (UpdateDb updateDb : updateDbs) {
                String dbName = updateDb.getDbName();
                if (!TextUtils.isEmpty(dbName) && !dbNames.contains(dbName)) {
                    dbNames.add(dbName);
                }

            }
        }
        return dbNames;
    }

    /**
     * 根据建表脚本,核实一遍应该存在的表
     *
     * @param createVersion
     * @throws Exception
     */
    private void executeCreateVersion(CreateVersion createVersion, boolean isLogic) throws Exception {
        if (createVersion == null || createVersion.getCreateDbs() == null) {
            throw new Exception("check you updateXml.xml file to see if createVersion or createDbs node is null;");
        }

        for (CreateDb cd : createVersion.getCreateDbs()) {
            if (cd == null || cd.getName() == null) {
                throw new Exception("check you updateXml.xml file to see if createDb node or name is null");
            }
            // 创建数据库表sql
            List<String> sqls = cd.getSqlCreates();

            SQLiteDatabase sqlitedb = null;

            sqlitedb = getDb(cd.getName());
            executeSql(sqlitedb, sqls);
            sqlitedb.close();
        }
    }


    /**
     * 执行针对db升级的sql集合
     *
     * @param updateDbs 数据库操作脚本集合
     * @param type      小于0为建表前,大于0为建表后
     * @throws Exception
     * @throws throws    [违例类型] [违例说明]
     * @see
     */
    private void executeDb(List<UpdateDb> updateDbs, int type) throws Exception {
        if (updateDbs == null) {
            throw new Exception("updateDbs is null;");
        }
        for (UpdateDb db : updateDbs) {
            if (db == null || db.getDbName() == null) {
                throw new Exception("db or dbName is null;");
            }

            List<String> sqls = null;
            //更改表
            if (type < 0) {
                sqls = db.getSqlBefores();
            } else if (type > 0) {
                sqls = db.getSqlAfters();
            }

            SQLiteDatabase sqlitedb = null;

            sqlitedb = getDb(db.getDbName());

            executeSql(sqlitedb, sqls);

            sqlitedb.close();

        }
    }

    /**
     * 执行sql语句
     *
     * @param sqlitedb SQLiteDatabase
     * @param sqls     sql语句集合
     * @throws Exception 异常
     * @throws throws    [违例类型] [违例说明]
     * @see
     */
    private void executeSql(SQLiteDatabase sqlitedb, List<String> sqls) throws Exception {
        // 检查参数
        if (sqls == null || sqls.size() == 0) {
            return;
        }

        // 事务
        sqlitedb.beginTransaction();

        for (String sql : sqls) {
            sql = sql.replaceAll("\r\n", " ");
            sql = sql.replaceAll("\n", " ");
            if (!"".equals(sql.trim())) {
                try {
                    sqlitedb.execSQL(sql);
                } catch (SQLException e) {
                }
            }
        }

        sqlitedb.setTransactionSuccessful();
        sqlitedb.endTransaction();
    }


    /**
     * 新表插入数据
     *
     * @param xml
     * @param lastVersion 上个版本
     * @param thisVersion 当前版本
     * @return
     */
    private UpdateStep analyseUpdateStep(UpdateDbXml xml, String lastVersion, String thisVersion) {
        if (lastVersion == null || thisVersion == null) {
            return null;
        }

        // 更新脚本
        UpdateStep thisStep = null;
        if (xml == null) {
            return null;
        }
        List<UpdateStep> steps = xml.getUpdateSteps();
        if (steps == null || steps.size() == 0) {
            return null;
        }

        for (UpdateStep step : steps) {
            if (step.getVersionFrom() == null || step.getVersionTo() == null) {
            } else {
                // 升级来源以逗号分隔
                String[] lastVersionArray = step.getVersionFrom().split(",");

                if (lastVersionArray != null && lastVersionArray.length > 0) {
                    for (int i = 0; i < lastVersionArray.length; i++) {
                        // 有一个配到update节点即升级数据
                        if (lastVersion.equalsIgnoreCase(lastVersionArray[i]) && step.getVersionTo().equalsIgnoreCase(thisVersion)) {
                            thisStep = step;

                            break;
                        }
                    }
                }
            }
        }

        return thisStep;
    }

    /**
     * 创建数据库,获取数据库对应的SQLiteDatabase
     *
     * @param dbname
     * @return 设定文件
     * @throws throws [违例类型] [违例说明]sta
     * @see
     */
    private SQLiteDatabase getDb(String dbname) {
        String dbfilepath = null;
        SQLiteDatabase sqlitedb = null;
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        dbfilepath = parentFile.getAbsolutePath() + File.separator + dbname + ".db";// logic对应的数据库路径
        if (dbfilepath != null) {
            File f = new File(dbfilepath);
            f.mkdirs();
            if (f.isDirectory()) {
                f.delete();
            }
            sqlitedb = SQLiteDatabase.openOrCreateDatabase(dbfilepath, null);
        }

        return sqlitedb;
    }


    /**
     * 解析出对应版本的建表脚本
     *
     * @return
     */
    private CreateVersion analyseCreateVersion(UpdateDbXml xml, String version) {
        CreateVersion cv = null;
        if (xml == null || version == null) {
            return cv;
        }
        //获取到xml中createVersion节点对应的建表信息,这个信息是对应的最新版本的数据库信息
        //其他旧的版本数据库最终都要生成这个信息中指定的所有数据库和表的形式,这个节点可能会有
        //多个,以针对升级到不同版本的数据库升级处理,比如说,现在当前最新版本是10.0,目前市场
        //上6.0 7.0 8.0 9.0 都有相应的用户在使用,当前9.0的用户选择升级只能升级到最新的10.0
        //但是其他版本的用户就不同了,比如6.0用户,他有可能会选择升级到7.0 8.0 9.0 10.0中的
        //任意版本,由于每个版本的数据库结构可能都不相同,那么此时我们旧需要在xml中分别配置针对
        //7.0 8.0 9.0 10.0的四种升级方案,那么此时createVersion节点就存在四个,用户选择升
        //级到那个版本,就选择哪个createVersion节点进行处理
        //
        // 这就是createVersion[i].trim().equalsIgnoreCase(version)这一行判断存在的意义
        List<CreateVersion> createVersions = xml.getCreateVersions();
        if (createVersions != null) {
            for (CreateVersion item : createVersions) {
                //一般来说,一个createVersion设置一个version就可以了,但是有一些比较特殊的情况
                //比如8.0 和9.0 两个版本数据库并没有变化,所以升级到8.0 和9.0所进行的数据库升级
                //操作是一样的,所以可以把这两个版本号写在一块以,号隔开。或者你也可以分开写,复制
                //个一摸一样的节点,只把version值改一下,不过这样比较低级,不如写在一块节省代码
                String[] createVersion = item.getVersion().trim().split(",");

                for (int i = 0; i < createVersion.length; i++) {
                    //选择和当前升级后版本相匹配的数据库升级节点,version是升级后的版本
                    if (createVersion[i].trim().equalsIgnoreCase(version)) {
                        cv = item;

                        break;
                    }
                }
            }
        }

        return cv;
    }

    /**
     * 读取升级xml
     *
     * @param context
     * @return
     */
    private UpdateDbXml readDbXml(Context context) {
        InputStream is = null;
        Document document = null;
        try {
            is = context.getAssets().open(UPDATE_SCRIPT_XML);
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            document = builder.parse(is);
        } catch (Exception e) {
            e.printStackTrace();
            throw new NullPointerException("read update script xml failed,check your asset dir to see if you have a script xml");
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        if (document == null) {
            return null;
        }

        UpdateDbXml xml = new UpdateDbXml(document);

        return xml;
    }

    /**
     * 获取APK版本号
     *
     * @param context 上下文
     * @return 版本号
     * @throws throws [违例类型] [违例说明]
     * @see
     */
    public String getVersionName(Context context) {
        String versionName = null;
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            versionName = info.versionName;

        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        return versionName;
    }

    /**
     * 这个方法是模拟一个情景,当前有一个新版本发布,此时运行于市场上的app通过版本更新
     * 接口请求到了新版本信息,新版本版本号为V003,当前版本版本号为V002,通过这个方法将旧版本
     * 号写入到一个文件中做记录,这个信息可以传递出此次升级是从哪个版本升级到哪个版本
     *
     * @return 保存成功返回true,否则返回false
     * @throws throws [违例类型] [违例说明]
     * @see
     */
    public boolean saveVersionInfo(String lastVersion) {
        boolean ret = false;

        FileWriter writer = null;
        try {
            writer = new FileWriter(new File(parentFile, UPDATE_INFO), false);
            writer.write(lastVersion);
            writer.flush();
            ret = true;
        } catch (IOException e) {
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return ret;
    }

    /**
     * 获取本地版本相关信息,app升级之后需要保存升级之前的版本信息,从而
     * 根据这个信息可以知道,当前app是从哪个本版升级过来的
     *
     * @return 获取数据成功返回true,否则返回false
     * @throws throws [违例类型] [违例说明]
     * @see
     */
    private String lastVersion;

    private boolean getLocalVersionInfo() {
        boolean ret = false;

        File file = new File(parentFile, UPDATE_INFO);

        if (file.exists()) {
            int byteRead = 0;
            byte[] tempBytes = new byte[100];
            StringBuilder stringBuilder = new StringBuilder();
            InputStream in = null;
            try {
                in = new FileInputStream(file);
                while ((byteRead = in.read(tempBytes)) != -1) {
                    stringBuilder.append(new String(tempBytes, 0, byteRead));
                }
                String infos = stringBuilder.toString();
                //lastVersion是当前版本升级之前的版本,当前版本是从这个版本升级过来的
                //这个信息是通过saveVersionInfo保存的
                lastVersion = infos;
                ret = true;
            } catch (Exception e) {

            } finally {
                if (null != in) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    in = null;
                }
            }
        }

        return ret;
    }
}

源码地址:https://github.com/renzhenming/SqliteUpGrade
待续。。

相关文章
|
28天前
|
关系型数据库 MySQL Java
【IDEA】java后台操作mysql数据库驱动常见错误解决方案
【IDEA】java后台操作mysql数据库驱动常见错误解决方案
50 0
|
21小时前
|
存储 SQL Apache
Apache Doris 开源最顶级基于MPP架构的高性能实时分析数据库
Apache Doris 是一个基于 MPP 架构的高性能实时分析数据库,以其极高的速度和易用性著称。它支持高并发点查询和复杂分析场景,适用于报表分析、即席查询、数据仓库和数据湖查询加速等。最新发布的 2.0.2 版本在性能、稳定性和多租户支持方面有显著提升。社区活跃,已广泛应用于电商、广告、用户行为分析等领域。
Apache Doris 开源最顶级基于MPP架构的高性能实时分析数据库
|
1天前
|
缓存 关系型数据库 MySQL
高并发架构系列:数据库主从同步的 3 种方案
本文详解高并发场景下数据库主从同步的三种解决方案:数据主从同步、数据库半同步复制、数据库中间件同步和缓存记录写key同步,旨在帮助解决数据一致性问题。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
高并发架构系列:数据库主从同步的 3 种方案
|
1天前
|
算法 安全 数据库
数据库死锁的解决方案有哪些?
【10月更文挑战第28天】数据库死锁是数据库管理中的一个常见问题
22 15
|
3天前
|
运维 Serverless 数据处理
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
18 1
|
9天前
|
关系型数据库 MySQL 数据库
一个 MySQL 数据库死锁的案例和解决方案
本文介绍了一个 MySQL 数据库死锁的案例和解决方案。
16 3
|
17天前
|
存储 消息中间件 人工智能
ApsaraMQ Serverless 能力再升级,事件驱动架构赋能 AI 应用
本文整理自2024年云栖大会阿里云智能集团高级技术专家金吉祥的演讲《ApsaraMQ Serverless 能力再升级,事件驱动架构赋能 AI 应用》。
|
18天前
|
运维 Serverless 数据处理
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
45 3
|
25天前
|
存储 消息中间件 运维
架构升级的救星!流量回放自动化测试的必备指南
大家好,我是小米,一名29岁的技术宅。今天分享一个物联网领域的实用技能——流量回放自动化测试。系统重构后,测试工作量巨大,本文介绍如何通过日志收集和数据回放进行自动化测试,包括离线、实时和并行回放模式,帮助快速定位Bug,提升测试效率和系统稳定性。欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
30 3
|
23天前
|
存储 SQL 缓存
Apache Doris 3.0 里程碑版本|存算分离架构升级、湖仓一体再进化
从 3.0 系列版本开始,Apache Doris 开始支持存算分离模式,用户可以在集群部署时选择采用存算一体模式或存算分离模式。基于云原生存算分离的架构,用户可以通过多计算集群实现查询负载间的物理隔离以及读写负载隔离,并借助对象存储或 HDFS 等低成本的共享存储系统来大幅降低存储成本。
Apache Doris 3.0 里程碑版本|存算分离架构升级、湖仓一体再进化