发布带有数据库的应用程序
Posted
技术标签:
【中文标题】发布带有数据库的应用程序【英文标题】:Ship an application with a database 【发布时间】:2017-12-12 13:49:52 【问题描述】:如果您的应用程序需要一个数据库并且它带有内置数据,那么发布该应用程序的最佳方式是什么?我应该:
预创建 SQLite 数据库并将其包含在 .apk
?
在应用程序中包含 SQL 命令并让它创建数据库并在首次使用时插入数据?
我看到的缺点是:
可能的 SQLite 版本不匹配可能会导致问题,我目前不知道数据库应该放在哪里以及如何访问它。
在设备上创建和填充数据库可能需要很长时间。
有什么建议吗?非常感谢您指向有关任何问题的文档。
【问题讨论】:
***.com/questions/9109438/… 使用SQLiteAssetHelper 【参考方案1】:创建和更新数据库有两种选择。
一种是在外部创建一个数据库,然后将它放在项目的assets文件夹中,然后从那里复制整个数据库。如果数据库有很多表和其他组件,这会更快。 通过更改 res/values/strings.xml 文件中的数据库版本号触发升级。 然后通过在外部创建新数据库替换旧数据库来完成升级在带有新数据库的资产文件夹中,以另一个名称将旧数据库保存在内部存储中,将新数据库从资产文件夹复制到内部存储中,将所有数据从旧数据库(之前已重命名)转移到新数据库中数据库,最后删除旧数据库。您最初可以使用 SQLite Manager FireFox 插件 来执行您的创建 sql 语句来创建数据库。
另一种选择是在内部从 sql 文件创建数据库。这不是那么快,但如果数据库只有几个表,用户可能不会注意到延迟。 通过更改 res/values/strings.xml 文件中的数据库版本号来触发升级。 然后将通过处理升级 sql 文件来完成升级。数据库中的数据将保持不变,除非删除其容器,例如删除表。
下面的示例演示了如何使用这两种方法。
这是一个示例 create_database.sql 文件。内部方法要放在项目的assets文件夹中,或者复制到SQLite Manager的“执行SQL”中,为外部方法创建数据库。(注意:注意关于android 所需的表。)
--Android requires a table named 'android_metadata' with a 'locale' column
CREATE TABLE "android_metadata" ("locale" TEXT DEFAULT 'en_US');
INSERT INTO "android_metadata" VALUES ('en_US');
CREATE TABLE "kitchen_table";
CREATE TABLE "coffee_table";
CREATE TABLE "pool_table";
CREATE TABLE "dining_room_table";
CREATE TABLE "card_table";
这是一个示例 update_database.sql 文件。内部方法放在项目的 assets 文件夹中,或者复制到 SQLite Manager 的“Execute SQL”中,为外部方法创建数据库。(注意:请注意,所有三种类型的 SQL cmets 将被本示例中包含的 sql 解析器忽略。)
--CREATE TABLE "kitchen_table"; This is one type of comment in sql. It is ignored by parseSql.
/*
* CREATE TABLE "coffee_table"; This is a second type of comment in sql. It is ignored by parseSql.
*/
CREATE TABLE "pool_table"; This is a third type of comment in sql. It is ignored by parseSql.
/* CREATE TABLE "dining_room_table"; This is a second type of comment in sql. It is ignored by parseSql. */
CREATE TABLE "card_table"; This is a third type of comment in sql. It is ignored by parseSql.
--DROP TABLE "picnic_table"; Uncomment this if picnic table was previously created and now is being replaced.
CREATE TABLE "picnic_table" ("plates" TEXT);
INSERT INTO "picnic_table" VALUES ('paper');
这是添加到 /res/values/strings.xml 文件中的数据库版本号的条目。
<item type="string" name="databaseVersion" format="integer">1</item>
这是一个访问数据库然后使用它的活动。 (注意:如果数据库代码占用大量资源,您可能希望在单独的线程中运行它。)
package android.example;
import android.app.Activity;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
/**
* @author Danny Remington - MacroSolve
*
* Activity for demonstrating how to use a sqlite database.
*/
public class Database extends Activity
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
DatabaseHelper myDbHelper;
SQLiteDatabase myDb = null;
myDbHelper = new DatabaseHelper(this);
/*
* Database must be initialized before it can be used. This will ensure
* that the database exists and is the current version.
*/
myDbHelper.initializeDataBase();
try
// A reference to the database can be obtained after initialization.
myDb = myDbHelper.getWritableDatabase();
/*
* Place code to use database here.
*/
catch (Exception ex)
ex.printStackTrace();
finally
try
myDbHelper.close();
catch (Exception ex)
ex.printStackTrace();
finally
myDb.close();
这里是数据库助手类,如果需要,可以在其中创建或更新数据库。 (注意:Android 要求您创建一个扩展 SQLiteOpenHelper 的类才能使用 Sqlite 数据库。)
package android.example;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
/**
* @author Danny Remington - MacroSolve
*
* Helper class for sqlite database.
*/
public class DatabaseHelper extends SQLiteOpenHelper
/*
* The Android's default system path of the application database in internal
* storage. The package of the application is part of the path of the
* directory.
*/
private static String DB_DIR = "/data/data/android.example/databases/";
private static String DB_NAME = "database.sqlite";
private static String DB_PATH = DB_DIR + DB_NAME;
private static String OLD_DB_PATH = DB_DIR + "old_" + DB_NAME;
private final Context myContext;
private boolean createDatabase = false;
private boolean upgradeDatabase = false;
/**
* Constructor Takes and keeps a reference of the passed context in order to
* access to the application assets and resources.
*
* @param context
*/
public DatabaseHelper(Context context)
super(context, DB_NAME, null, context.getResources().getInteger(
R.string.databaseVersion));
myContext = context;
// Get the path of the database that is based on the context.
DB_PATH = myContext.getDatabasePath(DB_NAME).getAbsolutePath();
/**
* Upgrade the database in internal storage if it exists but is not current.
* Create a new empty database in internal storage if it does not exist.
*/
public void initializeDataBase()
/*
* Creates or updates the database in internal storage if it is needed
* before opening the database. In all cases opening the database copies
* the database in internal storage to the cache.
*/
getWritableDatabase();
if (createDatabase)
/*
* If the database is created by the copy method, then the creation
* code needs to go here. This method consists of copying the new
* database from assets into internal storage and then caching it.
*/
try
/*
* Write over the empty data that was created in internal
* storage with the one in assets and then cache it.
*/
copyDataBase();
catch (IOException e)
throw new Error("Error copying database");
else if (upgradeDatabase)
/*
* If the database is upgraded by the copy and reload method, then
* the upgrade code needs to go here. This method consists of
* renaming the old database in internal storage, create an empty
* new database in internal storage, copying the database from
* assets to the new database in internal storage, caching the new
* database from internal storage, loading the data from the old
* database into the new database in the cache and then deleting the
* old database from internal storage.
*/
try
FileHelper.copyFile(DB_PATH, OLD_DB_PATH);
copyDataBase();
SQLiteDatabase old_db = SQLiteDatabase.openDatabase(OLD_DB_PATH, null, SQLiteDatabase.OPEN_READWRITE);
SQLiteDatabase new_db = SQLiteDatabase.openDatabase(DB_PATH,null, SQLiteDatabase.OPEN_READWRITE);
/*
* Add code to load data into the new database from the old
* database and then delete the old database from internal
* storage after all data has been transferred.
*/
catch (IOException e)
throw new Error("Error copying database");
/**
* Copies your database from your local assets-folder to the just created
* empty database in the system folder, from where it can be accessed and
* handled. This is done by transfering bytestream.
* */
private void copyDataBase() throws IOException
/*
* Close SQLiteOpenHelper so it will commit the created empty database
* to internal storage.
*/
close();
/*
* Open the database in the assets folder as the input stream.
*/
InputStream myInput = myContext.getAssets().open(DB_NAME);
/*
* Open the empty db in interal storage as the output stream.
*/
OutputStream myOutput = new FileOutputStream(DB_PATH);
/*
* Copy over the empty db in internal storage with the database in the
* assets folder.
*/
FileHelper.copyFile(myInput, myOutput);
/*
* Access the copied database so SQLiteHelper will cache it and mark it
* as created.
*/
getWritableDatabase().close();
/*
* This is where the creation of tables and the initial population of the
* tables should happen, if a database is being created from scratch instead
* of being copied from the application package assets. Copying a database
* from the application package assets to internal storage inside this
* method will result in a corrupted database.
* <P>
* NOTE: This method is normally only called when a database has not already
* been created. When the database has been copied, then this method is
* called the first time a reference to the database is retrieved after the
* database is copied since the database last cached by SQLiteOpenHelper is
* different than the database in internal storage.
*/
@Override
public void onCreate(SQLiteDatabase db)
/*
* Signal that a new database needs to be copied. The copy process must
* be performed after the database in the cache has been closed causing
* it to be committed to internal storage. Otherwise the database in
* internal storage will not have the same creation timestamp as the one
* in the cache causing the database in internal storage to be marked as
* corrupted.
*/
createDatabase = true;
/*
* This will create by reading a sql file and executing the commands in
* it.
*/
// try
// InputStream is = myContext.getResources().getAssets().open(
// "create_database.sql");
//
// String[] statements = FileHelper.parseSqlFile(is);
//
// for (String statement : statements)
// db.execSQL(statement);
//
// catch (Exception ex)
// ex.printStackTrace();
//
/**
* Called only if version number was changed and the database has already
* been created. Copying a database from the application package assets to
* the internal data system inside this method will result in a corrupted
* database in the internal data system.
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
/*
* Signal that the database needs to be upgraded for the copy method of
* creation. The copy process must be performed after the database has
* been opened or the database will be corrupted.
*/
upgradeDatabase = true;
/*
* Code to update the database via execution of sql statements goes
* here.
*/
/*
* This will upgrade by reading a sql file and executing the commands in
* it.
*/
// try
// InputStream is = myContext.getResources().getAssets().open(
// "upgrade_database.sql");
//
// String[] statements = FileHelper.parseSqlFile(is);
//
// for (String statement : statements)
// db.execSQL(statement);
//
// catch (Exception ex)
// ex.printStackTrace();
//
/**
* Called everytime the database is opened by getReadableDatabase or
* getWritableDatabase. This is called after onCreate or onUpgrade is
* called.
*/
@Override
public void onOpen(SQLiteDatabase db)
super.onOpen(db);
/*
* Add your public helper methods to access and get content from the
* database. You could return cursors by doing
* "return myDataBase.query(....)" so it'd be easy to you to create adapters
* for your views.
*/
这是 FileHelper 类,其中包含字节流复制文件和解析 sql 文件的方法。
package android.example;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.channels.FileChannel;
/**
* @author Danny Remington - MacroSolve
*
* Helper class for common tasks using files.
*
*/
public class FileHelper
/**
* Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
* of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
* it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
* path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
* <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
* operation.
*
* @param fromFile
* - InputStream for the file to copy from.
* @param toFile
* - InputStream for the file to copy to.
*/
public static void copyFile(InputStream fromFile, OutputStream toFile) throws IOException
// transfer bytes from the inputfile to the outputfile
byte[] buffer = new byte[1024];
int length;
try
while ((length = fromFile.read(buffer)) > 0)
toFile.write(buffer, 0, length);
// Close the streams
finally
try
if (toFile != null)
try
toFile.flush();
finally
toFile.close();
finally
if (fromFile != null)
fromFile.close();
/**
* Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
* of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
* it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
* path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
* <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
* operation.
*
* @param fromFile
* - String specifying the path of the file to copy from.
* @param toFile
* - String specifying the path of the file to copy to.
*/
public static void copyFile(String fromFile, String toFile) throws IOException
copyFile(new FileInputStream(fromFile), new FileOutputStream(toFile));
/**
* Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
* of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
* it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
* path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
* <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
* operation.
*
* @param fromFile
* - File for the file to copy from.
* @param toFile
* - File for the file to copy to.
*/
public static void copyFile(File fromFile, File toFile) throws IOException
copyFile(new FileInputStream(fromFile), new FileOutputStream(toFile));
/**
* Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
* of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
* it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
* path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
* <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
* operation.
*
* @param fromFile
* - FileInputStream for the file to copy from.
* @param toFile
* - FileInputStream for the file to copy to.
*/
public static void copyFile(FileInputStream fromFile, FileOutputStream toFile) throws IOException
FileChannel fromChannel = fromFile.getChannel();
FileChannel toChannel = toFile.getChannel();
try
fromChannel.transferTo(0, fromChannel.size(), toChannel);
finally
try
if (fromChannel != null)
fromChannel.close();
finally
if (toChannel != null)
toChannel.close();
/**
* Parses a file containing sql statements into a String array that contains
* only the sql statements. Comments and white spaces in the file are not
* parsed into the String array. Note the file must not contained malformed
* comments and all sql statements must end with a semi-colon ";" in order
* for the file to be parsed correctly. The sql statements in the String
* array will not end with a semi-colon ";".
*
* @param sqlFile
* - String containing the path for the file that contains sql
* statements.
*
* @return String array containing the sql statements.
*/
public static String[] parseSqlFile(String sqlFile) throws IOException
return parseSqlFile(new BufferedReader(new FileReader(sqlFile)));
/**
* Parses a file containing sql statements into a String array that contains
* only the sql statements. Comments and white spaces in the file are not
* parsed into the String array. Note the file must not contained malformed
* comments and all sql statements must end with a semi-colon ";" in order
* for the file to be parsed correctly. The sql statements in the String
* array will not end with a semi-colon ";".
*
* @param sqlFile
* - InputStream for the file that contains sql statements.
*
* @return String array containing the sql statements.
*/
public static String[] parseSqlFile(InputStream sqlFile) throws IOException
return parseSqlFile(new BufferedReader(new InputStreamReader(sqlFile)));
/**
* Parses a file containing sql statements into a String array that contains
* only the sql statements. Comments and white spaces in the file are not
* parsed into the String array. Note the file must not contained malformed
* comments and all sql statements must end with a semi-colon ";" in order
* for the file to be parsed correctly. The sql statements in the String
* array will not end with a semi-colon ";".
*
* @param sqlFile
* - Reader for the file that contains sql statements.
*
* @return String array containing the sql statements.
*/
public static String[] parseSqlFile(Reader sqlFile) throws IOException
return parseSqlFile(new BufferedReader(sqlFile));
/**
* Parses a file containing sql statements into a String array that contains
* only the sql statements. Comments and white spaces in the file are not
* parsed into the String array. Note the file must not contained malformed
* comments and all sql statements must end with a semi-colon ";" in order
* for the file to be parsed correctly. The sql statements in the String
* array will not end with a semi-colon ";".
*
* @param sqlFile
* - BufferedReader for the file that contains sql statements.
*
* @return String array containing the sql statements.
*/
public static String[] parseSqlFile(BufferedReader sqlFile) throws IOException
String line;
StringBuilder sql = new StringBuilder();
String multiLineComment = null;
while ((line = sqlFile.readLine()) != null)
line = line.trim();
// Check for start of multi-line comment
if (multiLineComment == null)
// Check for first multi-line comment type
if (line.startsWith("/*"))
if (!line.endsWith(""))
multiLineComment = "/*";
// Check for second multi-line comment type
else if (line.startsWith(""))
if (!line.endsWith(""))
multiLineComment = "";
// Append line if line is not empty or a single line comment
else if (!line.startsWith("--") && !line.equals(""))
sql.append(line);
// Check for matching end comment
else if (multiLineComment.equals("/*"))
if (line.endsWith("*/"))
multiLineComment = null;
// Check for matching end comment
else if (multiLineComment.equals(""))
if (line.endsWith(""))
multiLineComment = null;
sqlFile.close();
return sql.toString().split(";");
【讨论】:
我使用上面的代码来升级我的数据库“upgrade_database.sql 包含插入语句。一些值有分号,比如插入到 table_a 值中('ss','ddd','aaaa;aaa '); "当我运行时,我注意到上面提到的插入没有得到执行,因为值中的分号任何想法如何解决这个问题。 还有第三个选项——从网上复制数据库。我已经这样做了,对于 4 meg db 来说它运行得相当快。它还解决了 2.3 的问题,第一个解决方案(复制 db)不起作用。【参考方案2】:SQLiteAssetHelper
库使这项任务变得非常简单。
添加为 gradle 依赖项很容易(但 Ant/Eclipse 也可以使用 Jar),并且可以在以下位置找到它以及文档:https://github.com/jgilfelt/android-sqlite-asset-helper
注意:该项目不再如上述 Github 链接所述维护。
如文档中所述:
将依赖项添加到模块的 gradle 构建文件中:
dependencies
compile 'com.readystatesoftware.sqliteasset:sqliteassethelper:+'
将数据库复制到资产目录中,位于名为assets/databases
的子目录中。例如:assets/databases/my_database.db
(或者,您可以将数据库压缩成一个 zip 文件,例如 assets/databases/my_database.zip
。这不是必需的,因为 APK 已经作为一个整体进行了压缩。)
创建一个类,例如:
public class MyDatabase extends SQLiteAssetHelper
private static final String DATABASE_NAME = "my_database.db";
private static final int DATABASE_VERSION = 1;
public MyDatabase(Context context)
super(context, DATABASE_NAME, null, DATABASE_VERSION);
【讨论】:
android-sqlite-asset-helper.jar 下载需要哪个凭证? 如果您使用的是 gradle,那么您只需添加依赖项。 zip 仅在您的目标是姜饼 (api 10) 或更低版本时才需要。 请注意,这个库已被废弃,最后一次更新是 4 年前。 库在运行 Android Pie 的设备上使应用程序崩溃。【参考方案3】:在 Android Studio 3.0 中使用数据库文件发送应用程序
使用数据库文件发布应用程序对我来说是个好主意。优点是您不需要进行复杂的初始化,如果您的数据集很大,这有时会花费大量时间。
第 1 步:准备数据库文件
准备好您的数据库文件。它可以是 .db 文件或 .sqlite 文件。如果您使用 .sqlite 文件,您需要做的就是更改文件扩展名。步骤相同。
在本例中,我准备了一个名为 testDB.db 的文件。它有一个表和一些示例数据,如下所示
第 2 步:将文件导入您的项目
如果您还没有资产文件夹,请创建资产文件夹。然后将数据库文件复制并粘贴到此文件夹中
第 3 步:将文件复制到应用的数据文件夹
您需要将数据库文件复制到应用程序的数据文件夹中,以便与它进行进一步的交互。这是复制数据库文件的一次性操作(初始化)。如果多次调用此代码,data 文件夹中的数据库文件将被 assets 文件夹中的数据库文件覆盖。当您想在应用更新期间更新数据库时,此覆盖过程非常有用。
请注意,在应用更新期间,该数据库文件不会在应用的数据文件夹中更改。只有卸载才会删除。
需要将数据库文件复制到/databases
文件夹。打开设备文件资源管理器。输入data/data/<YourAppName>/
位置。这是上面提到的应用程序的默认数据文件夹。默认情况下,数据库文件将放置在该目录下的另一个名为 databases 的文件夹中
现在,复制文件的过程与 Java 所做的非常相似。使用以下代码进行复制粘贴。这是启动代码。它还可以用于将来更新(通过覆盖)数据库文件。
//get context by calling "this" in activity or getActivity() in fragment
//call this if API level is lower than 17 String appDataPath = "/data/data/" + context.getPackageName() + "/databases/"
String appDataPath = context.getApplicationInfo().dataDir;
File dbFolder = new File(appDataPath + "/databases");//Make sure the /databases folder exists
dbFolder.mkdir();//This can be called multiple times.
File dbFilePath = new File(appDataPath + "/databases/testDB.db");
try
InputStream inputStream = context.getAssets().open("testDB.db");
OutputStream outputStream = new FileOutputStream(dbFilePath);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer))>0)
outputStream.write(buffer, 0, length);
outputStream.flush();
outputStream.close();
inputStream.close();
catch (IOException e)
//handle
然后刷新文件夹验证复制过程
第 4 步:创建数据库打开助手
为SQLiteOpenHelper
创建一个子类,包含connect、close、path等。我将它命名为DatabaseOpenHelper
import android.content.Context;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DatabaseOpenHelper extends SQLiteOpenHelper
public static final String DB_NAME = "testDB.db";
public static final String DB_SUB_PATH = "/databases/" + DB_NAME;
private static String APP_DATA_PATH = "";
private SQLiteDatabase dataBase;
private final Context context;
public DatabaseOpenHelper(Context context)
super(context, DB_NAME, null, 1);
APP_DATA_PATH = context.getApplicationInfo().dataDir;
this.context = context;
public boolean openDataBase() throws SQLException
String mPath = APP_DATA_PATH + DB_SUB_PATH;
//Note that this method assumes that the db file is already copied in place
dataBase = SQLiteDatabase.openDatabase(mPath, null, SQLiteDatabase.OPEN_READWRITE);
return dataBase != null;
@Override
public synchronized void close()
if(dataBase != null) dataBase.close();
super.close();
@Override
public void onCreate(SQLiteDatabase db)
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
第 5 步:创建与数据库交互的***类
这将是读取和写入您的数据库文件的类。还有一个示例查询可以打印出数据库中的值。
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
public class Database
private final Context context;
private SQLiteDatabase database;
private DatabaseOpenHelper dbHelper;
public Database(Context context)
this.context = context;
dbHelper = new DatabaseOpenHelper(context);
public Database open() throws SQLException
dbHelper.openDataBase();
dbHelper.close();
database = dbHelper.getReadableDatabase();
return this;
public void close()
dbHelper.close();
public void test()
try
String query ="SELECT value FROM test1";
Cursor cursor = database.rawQuery(query, null);
if (cursor.moveToFirst())
do
String value = cursor.getString(0);
Log.d("db", value);
while (cursor.moveToNext());
cursor.close();
catch (SQLException e)
//handle
第 6 步:测试运行
通过运行以下代码行来测试代码。
Database db = new Database(context);
db.open();
db.test();
db.close();
点击跑步按钮并欢呼!
【讨论】:
【参考方案4】:我的解决方案既不使用任何第三方库,也不强迫您在 SQLiteOpenHelper
子类上调用自定义方法来在创建时初始化数据库。它还负责数据库升级。所需要做的就是继承SQLiteOpenHelper
。
先决条件:
-
您希望随应用程序一起提供的数据库。 它应该包含一个名为
android_metadata
的 1x1 表,除了您的应用独有的表之外,该表的属性 locale
的值为 en_US
。
子类化SQLiteOpenHelper
:
-
子类
SQLiteOpenHelper
。
在SQLiteOpenHelper
子类中创建一个private
方法。此方法包含将数据库内容从“assets”文件夹中的数据库文件复制到应用程序包上下文中创建的数据库的逻辑。
覆盖onCreate
、onUpgrade
和 onOpen
的SQLiteOpenHelper
方法。
说得够多了。这是SQLiteOpenHelper
子类:
public class PlanDetailsSQLiteOpenHelper extends SQLiteOpenHelper
private static final String TAG = "SQLiteOpenHelper";
private final Context context;
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "my_custom_db";
private boolean createDb = false, upgradeDb = false;
public PlanDetailsSQLiteOpenHelper(Context context)
super(context, DATABASE_NAME, null, DATABASE_VERSION);
this.context = context;
/**
* Copy packaged database from assets folder to the database created in the
* application package context.
*
* @param db
* The target database in the application package context.
*/
private void copyDatabaseFromAssets(SQLiteDatabase db)
Log.i(TAG, "copyDatabase");
InputStream myInput = null;
OutputStream myOutput = null;
try
// Open db packaged as asset as the input stream
myInput = context.getAssets().open("path/to/shipped/db/file");
// Open the db in the application package context:
myOutput = new FileOutputStream(db.getPath());
// Transfer db file contents:
byte[] buffer = new byte[1024];
int length;
while ((length = myInput.read(buffer)) > 0)
myOutput.write(buffer, 0, length);
myOutput.flush();
// Set the version of the copied database to the current
// version:
SQLiteDatabase copiedDb = context.openOrCreateDatabase(
DATABASE_NAME, 0, null);
copiedDb.execSQL("PRAGMA user_version = " + DATABASE_VERSION);
copiedDb.close();
catch (IOException e)
e.printStackTrace();
throw new Error(TAG + " Error copying database");
finally
// Close the streams
try
if (myOutput != null)
myOutput.close();
if (myInput != null)
myInput.close();
catch (IOException e)
e.printStackTrace();
throw new Error(TAG + " Error closing streams");
@Override
public void onCreate(SQLiteDatabase db)
Log.i(TAG, "onCreate db");
createDb = true;
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
Log.i(TAG, "onUpgrade db");
upgradeDb = true;
@Override
public void onOpen(SQLiteDatabase db)
Log.i(TAG, "onOpen db");
if (createDb) // The db in the application package
// context is being created.
// So copy the contents from the db
// file packaged in the assets
// folder:
createDb = false;
copyDatabaseFromAssets(db);
if (upgradeDb) // The db in the application package
// context is being upgraded from a lower to a higher version.
upgradeDb = false;
// Your db upgrade logic here:
最后,要获得数据库连接,只需在SQLiteOpenHelper
子类上调用getReadableDatabase()
或getWritableDatabase()
,它将负责创建数据库,从“资产”文件夹中的指定文件复制数据库内容,如果数据库不存在。
简而言之,您可以使用SQLiteOpenHelper
子类来访问资产文件夹中提供的数据库,就像使用onCreate()
方法中使用SQL 查询初始化的数据库一样。
【讨论】:
【参考方案5】:2017 年 11 月,Google 发布了Room Persistence Library。
来自文档:
Room 持久性库在 SQLite 之上提供了一个抽象层 允许流畅的数据库访问,同时充分利用 SQLite。
该库可帮助您在设备上创建应用数据的缓存 那正在运行您的应用程序。此缓存,用作您应用程序的单一缓存 真实来源,允许用户查看密钥的一致副本 应用内的信息,无论用户是否拥有 互联网连接。
Room 数据库在首次创建或打开数据库时有一个回调。您可以使用 create 回调来填充您的数据库。
Room.databaseBuilder(context.applicationContext,
DataDatabase::class.java, "Sample.db")
// prepopulate the database after onCreate was called
.addCallback(object : Callback()
override fun onCreate(db: SupportSQLiteDatabase)
super.onCreate(db)
// moving to a new thread
ioThread
getInstance(context).dataDao()
.insert(PREPOPULATE_DATA)
)
.build()
来自blog post的代码。
【讨论】:
如果您想使用已经存在的 SQLite 发布 APK,可以将其添加到 assets 文件夹并使用此包 github.com/humazed/RoomAsset 执行迁移,将 SQLite 文件数据加载到新的一个。这样,您可以使用现有数据库保存数据填充。【参考方案6】:据我所见,您应该交付一个已经具有表设置和数据的数据库。但是,如果您愿意(并且取决于您拥有的应用程序类型),您可以允许“升级数据库选项”。然后你要做的是下载最新的sqlite版本,获取在线托管的文本文件的最新插入/创建语句,执行语句并将数据从旧数据库传输到新数据库。
【讨论】:
> 从我所看到的情况来看,您应该运送一个已经具有表设置和数据的数据库。是的,但你是怎么做到的?【参考方案7】:目前无法预先创建 SQLite 数据库以随您的 apk 一起提供。您可以做的最好的事情是将适当的 SQL 保存为资源并从您的应用程序中运行它们。是的,这会导致数据重复(相同的信息作为资源和数据库存在),但目前没有其他方法。唯一的缓解因素是 apk 文件被压缩。我的经验是 908KB 压缩到小于 268KB。
下面的帖子有我发现的最好的讨论/解决方案,带有很好的示例代码。
http://groups.google.com/group/android-developers/msg/9f455ae93a1cf152
我将 CREATE 语句存储为字符串资源,以便使用 Context.getString() 读取并使用 SQLiteDatabse.execSQL() 运行它。
我将插入的数据存储在 res/raw/inserts.sql 中(我创建了 sql 文件,7000 多行)。使用上面链接中的技术,我进入了一个循环,逐行读取文件并将数据连接到“INSERT INTO tbl VALUE”并执行另一个 SQLiteDatabase.execSQL()。当它们可以被连接时,保存 7000 个“INSERT INTO tbl VALUE”是没有意义的。
在模拟器上大约需要 20 秒,我不知道在真手机上需要多长时间,但它只发生一次,当用户第一次启动应用程序时。
【讨论】:
第一次运行时从网上拉取 SQL 脚本怎么样?这样就不需要复制数据了。 可以,但设备需要连接到互联网。这在某些应用程序中是一个严重的缺陷。 不要进行 7000+ 次插入,而是像这样进行 100 次左右的批量插入 -INSERT INTO table VALUES(...) VALUES(...) VALUES(...) ...
(1 个插入行应该有 100 个值)。它将更加高效,并将您的启动时间从 20 秒减少到 2 或 3 秒。【参考方案8】:
我终于做到了!!我使用了这个链接help Using your own SQLite database in Android applications,但不得不稍微改变一下。
如果你有很多包,你应该把主包名称放在这里:
private static String DB_PATH = "data/data/masterPakageName/databases";
我更改了将数据库从本地文件夹复制到模拟器文件夹的方法!当该文件夹不存在时,它有一些问题。所以首先,它应该检查路径,如果它不存在,它应该创建文件夹。
在之前的代码中,copyDatabase
方法在数据库不存在时从未被调用,checkDataBase
方法导致异常。所以我稍微改了一下代码。
如果您的数据库没有文件扩展名,请不要使用文件名。
它对我很好,我希望它对你也有用
package farhangsarasIntroduction;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DataBaseHelper extends SQLiteOpenHelper
//The Android's default system path of your application database.
private static String DB_PATH = "data/data/com.example.sample/databases";
private static String DB_NAME = "farhangsaraDb";
private SQLiteDatabase myDataBase;
private final Context myContext;
/**
* Constructor
* Takes and keeps a reference of the passed context in order to access to the application assets and resources.
* @param context
*/
public DataBaseHelper(Context context)
super(context, DB_NAME, null, 1);
this.myContext = context;
/**
* Creates a empty database on the system and rewrites it with your own database.
* */
public void createDataBase()
boolean dbExist;
try
dbExist = checkDataBase();
catch (SQLiteException e)
e.printStackTrace();
throw new Error("database dose not exist");
if(dbExist)
//do nothing - database already exist
else
try
copyDataBase();
catch (IOException e)
e.printStackTrace();
throw new Error("Error copying database");
//By calling this method and empty database will be created into the default system path
//of your application so we are gonna be able to overwrite that database with our database.
this.getReadableDatabase();
/**
* Check if the database already exist to avoid re-copying the file each time you open the application.
* @return true if it exists, false if it doesn't
*/
private boolean checkDataBase()
SQLiteDatabase checkDB = null;
try
String myPath = DB_PATH +"/"+ DB_NAME;
checkDB = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY);
catch(SQLiteException e)
//database does't exist yet.
throw new Error("database does't exist yet.");
if(checkDB != null)
checkDB.close();
return checkDB != null ? true : false;
/**
* Copies your database from your local assets-folder to the just created empty database in the
* system folder, from where it can be accessed and handled.
* This is done by transfering bytestream.
* */
private void copyDataBase() throws IOException
//copyDataBase();
//Open your local db as the input stream
InputStream myInput = myContext.getAssets().open(DB_NAME);
// Path to the just created empty db
String outFileName = DB_PATH +"/"+ DB_NAME;
File databaseFile = new File( DB_PATH);
// check if databases folder exists, if not create one and its subfolders
if (!databaseFile.exists())
databaseFile.mkdir();
//Open the empty db as the output stream
OutputStream myOutput = new FileOutputStream(outFileName);
//transfer bytes from the inputfile to the outputfile
byte[] buffer = new byte[1024];
int length;
while ((length = myInput.read(buffer))>0)
myOutput.write(buffer, 0, length);
//Close the streams
myOutput.flush();
myOutput.close();
myInput.close();
@Override
public synchronized void close()
if(myDataBase != null)
myDataBase.close();
super.close();
@Override
public void onCreate(SQLiteDatabase db)
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
you to create adapters for your views.
【讨论】:
完美,谢谢!只有一条评论,在检查数据库时抛出异常会导致应用程序关闭,因为数据库一开始不会在那里,并且在抛出异常后该方法不会继续。我只是注释掉了 throw new Error("database dose not exist");现在一切正常。【参考方案9】:我修改了课程和问题的答案,并编写了一个允许通过 DB_VERSION 更新数据库的课程。
public class DatabaseHelper extends SQLiteOpenHelper
private static String DB_NAME = "info.db";
private static String DB_PATH = "";
private static final int DB_VERSION = 1;
private SQLiteDatabase mDataBase;
private final Context mContext;
private boolean mNeedUpdate = false;
public DatabaseHelper(Context context)
super(context, DB_NAME, null, DB_VERSION);
if (android.os.Build.VERSION.SDK_INT >= 17)
DB_PATH = context.getApplicationInfo().dataDir + "/databases/";
else
DB_PATH = "/data/data/" + context.getPackageName() + "/databases/";
this.mContext = context;
copyDataBase();
this.getReadableDatabase();
public void updateDataBase() throws IOException
if (mNeedUpdate)
File dbFile = new File(DB_PATH + DB_NAME);
if (dbFile.exists())
dbFile.delete();
copyDataBase();
mNeedUpdate = false;
private boolean checkDataBase()
File dbFile = new File(DB_PATH + DB_NAME);
return dbFile.exists();
private void copyDataBase()
if (!checkDataBase())
this.getReadableDatabase();
this.close();
try
copyDBFile();
catch (IOException mIOException)
throw new Error("ErrorCopyingDataBase");
private void copyDBFile() throws IOException
InputStream mInput = mContext.getAssets().open(DB_NAME);
//InputStream mInput = mContext.getResources().openRawResource(R.raw.info);
OutputStream mOutput = new FileOutputStream(DB_PATH + DB_NAME);
byte[] mBuffer = new byte[1024];
int mLength;
while ((mLength = mInput.read(mBuffer)) > 0)
mOutput.write(mBuffer, 0, mLength);
mOutput.flush();
mOutput.close();
mInput.close();
public boolean openDataBase() throws SQLException
mDataBase = SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.CREATE_IF_NECESSARY);
return mDataBase != null;
@Override
public synchronized void close()
if (mDataBase != null)
mDataBase.close();
super.close();
@Override
public void onCreate(SQLiteDatabase db)
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
if (newVersion > oldVersion)
mNeedUpdate = true;
使用类。
在活动类中,声明变量。
private DatabaseHelper mDBHelper;
private SQLiteDatabase mDb;
在onCreate方法中,编写如下代码。
mDBHelper = new DatabaseHelper(this);
try
mDBHelper.updateDataBase();
catch (IOException mIOException)
throw new Error("UnableToUpdateDatabase");
try
mDb = mDBHelper.getWritableDatabase();
catch (SQLException mSQLException)
throw mSQLException;
如果您将数据库文件添加到文件夹 res/raw 中,则使用该类的以下修改。
public class DatabaseHelper extends SQLiteOpenHelper
private static String DB_NAME = "info.db";
private static String DB_PATH = "";
private static final int DB_VERSION = 1;
private SQLiteDatabase mDataBase;
private final Context mContext;
private boolean mNeedUpdate = false;
public DatabaseHelper(Context context)
super(context, DB_NAME, null, DB_VERSION);
if (android.os.Build.VERSION.SDK_INT >= 17)
DB_PATH = context.getApplicationInfo().dataDir + "/databases/";
else
DB_PATH = "/data/data/" + context.getPackageName() + "/databases/";
this.mContext = context;
copyDataBase();
this.getReadableDatabase();
public void updateDataBase() throws IOException
if (mNeedUpdate)
File dbFile = new File(DB_PATH + DB_NAME);
if (dbFile.exists())
dbFile.delete();
copyDataBase();
mNeedUpdate = false;
private boolean checkDataBase()
File dbFile = new File(DB_PATH + DB_NAME);
return dbFile.exists();
private void copyDataBase()
if (!checkDataBase())
this.getReadableDatabase();
this.close();
try
copyDBFile();
catch (IOException mIOException)
throw new Error("ErrorCopyingDataBase");
private void copyDBFile() throws IOException
//InputStream mInput = mContext.getAssets().open(DB_NAME);
InputStream mInput = mContext.getResources().openRawResource(R.raw.info);
OutputStream mOutput = new FileOutputStream(DB_PATH + DB_NAME);
byte[] mBuffer = new byte[1024];
int mLength;
while ((mLength = mInput.read(mBuffer)) > 0)
mOutput.write(mBuffer, 0, mLength);
mOutput.flush();
mOutput.close();
mInput.close();
public boolean openDataBase() throws SQLException
mDataBase = SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.CREATE_IF_NECESSARY);
return mDataBase != null;
@Override
public synchronized void close()
if (mDataBase != null)
mDataBase.close();
super.close();
@Override
public void onCreate(SQLiteDatabase db)
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
if (newVersion > oldVersion)
mNeedUpdate = true;
http://blog.harrix.org/article/6784
【讨论】:
【参考方案10】:将数据库发送到 apk 中,然后将其复制到 /data/data/...
将使数据库大小加倍(apk 中为 1,data/data/...
中为 1),并且会增加 apk 大小(当然)。所以你的数据库不应该太大。
【讨论】:
它确实会增加 apk 的大小,但不会增加一倍。当它在资产中时,它会被压缩,因此要小得多。将其复制到数据库文件夹后,它会被解压缩。【参考方案11】:Android 已经提供了一种版本感知的数据库管理方法。这种方法已在 BARACUS 框架中用于 Android 应用程序。
It enables you to manage the database along the entire version lifecycle of an app, beeing able to update the sqlite database from any prior version to the current one.
Also, it allows you to run hot-backups and hot-recovery of the SQLite.
我不能 100% 确定,但对特定设备的热恢复可能使您能够在您的应用中发布准备好的数据库。但我不确定可能特定于某些设备、供应商或设备代的数据库二进制格式。
由于内容是 Apache License 2,请随意重用代码的任何部分,which can be found on github
编辑:
如果您只想传送数据,您可以考虑在应用程序首次启动时实例化和持久化 POJO。 BARACUS 对此提供了内置支持(用于配置信息的内置键值存储,例如“APP_FIRST_RUN”加上一个 after-context-bootstrap 挂钩,以便在上下文上运行启动后操作)。这使您能够将紧密耦合的数据与您的应用程序一起提供;在大多数情况下,这适合我的用例。
【讨论】:
【参考方案12】:如果所需数据不是太大(我不知道的限制,取决于很多事情),您也可以从网站/webapp 下载数据(以 XML、JSON 等格式)。接收后,使用接收到的数据执行 SQL 语句,创建表并插入数据。
如果您的移动应用包含大量数据,那么以后使用更准确的数据或更改更新已安装应用中的数据可能会更容易。
【讨论】:
【参考方案13】:我写了一个library 来简化这个过程。
dataBase = new DataBase.Builder(context, "myDb").
// setAssetsPath(). // default "databases"
// setDatabaseErrorHandler().
// setCursorFactory().
// setUpgradeCallback()
// setVersion(). // default 1
build();
它将从assets/databases/myDb.db
文件创建一个数据库。
此外,您还将获得所有这些功能:
从github克隆它。
【讨论】:
【参考方案14】:我正在使用 ORMLite,下面的代码对我有用
public class DatabaseProvider extends OrmLiteSqliteOpenHelper
private static final String DatabaseName = "DatabaseName";
private static final int DatabaseVersion = 1;
private final Context ProvidedContext;
public DatabaseProvider(Context context)
super(context, DatabaseName, null, DatabaseVersion);
this.ProvidedContext= context;
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean databaseCopied = preferences.getBoolean("DatabaseCopied", false);
if (databaseCopied)
//Do Nothing
else
CopyDatabase();
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean("DatabaseCopied", true);
editor.commit();
private String DatabasePath()
return "/data/data/" + ProvidedContext.getPackageName() + "/databases/";
private void CopyDatabase()
try
CopyDatabaseInternal();
catch (IOException e)
e.printStackTrace();
private File ExtractAssetsZip(String zipFileName)
InputStream inputStream;
ZipInputStream zipInputStream;
File tempFolder;
do
tempFolder = null;
tempFolder = new File(ProvidedContext.getCacheDir() + "/extracted-" + System.currentTimeMillis() + "/");
while (tempFolder.exists());
tempFolder.mkdirs();
try
String filename;
inputStream = ProvidedContext.getAssets().open(zipFileName);
zipInputStream = new ZipInputStream(new BufferedInputStream(inputStream));
ZipEntry zipEntry;
byte[] buffer = new byte[1024];
int count;
while ((zipEntry = zipInputStream.getNextEntry()) != null)
filename = zipEntry.getName();
if (zipEntry.isDirectory())
File fmd = new File(tempFolder.getAbsolutePath() + "/" + filename);
fmd.mkdirs();
continue;
FileOutputStream fileOutputStream = new FileOutputStream(tempFolder.getAbsolutePath() + "/" + filename);
while ((count = zipInputStream.read(buffer)) != -1)
fileOutputStream.write(buffer, 0, count);
fileOutputStream.close();
zipInputStream.closeEntry();
zipInputStream.close();
catch (IOException e)
e.printStackTrace();
return null;
return tempFolder;
private void CopyDatabaseInternal() throws IOException
File extractedPath = ExtractAssetsZip(DatabaseName + ".zip");
String databaseFile = "";
for (File innerFile : extractedPath.listFiles())
databaseFile = innerFile.getAbsolutePath();
break;
if (databaseFile == null || databaseFile.length() ==0 )
throw new RuntimeException("databaseFile is empty");
InputStream inputStream = new FileInputStream(databaseFile);
String outFileName = DatabasePath() + DatabaseName;
File destinationPath = new File(DatabasePath());
if (!destinationPath.exists())
destinationPath.mkdirs();
File destinationFile = new File(outFileName);
if (!destinationFile.exists())
destinationFile.createNewFile();
OutputStream myOutput = new FileOutputStream(outFileName);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0)
myOutput.write(buffer, 0, length);
myOutput.flush();
myOutput.close();
inputStream.close();
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource)
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource, int fromVersion, int toVersion)
请注意,代码从 assets 中的 zip 文件中提取数据库文件
【讨论】:
【参考方案15】:如果您使用的是 ROOM,官方文档中已经有一条非常直接的路径https://developer.android.com/training/data-storage/room/prepopulate。 以下是从资产文件重新填充数据库的实际操作:
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
或来自文件:
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
.createFromFile(File("mypath"))
.build()
如果你不使用 Room,我强烈建议你这样做 ?
【讨论】:
以上是关于发布带有数据库的应用程序的主要内容,如果未能解决你的问题,请参考以下文章
是否可以在线发布带有数据库的 asp.net mvc Web 应用程序?
带有本地 db .mdf 文件的 Web 应用程序如何发布到 Azure