Как использовать SQLiteOpenHelper с базой данных на SD-карте?
В соответствии с различными ответами здесь и в веб-расширении приложения, и унаследованный метод getDatabasePath() позволит установить путь хранения базы данных из стандартного места внутренней памяти на вставленную SD-карту большего размера.
Это не работает для меня. Предлагаемая конструкция по-прежнему использует базу данных во внутренней памяти. На самом деле метод getDatabasePath() никогда не вызывается SQLiteOpenHelper.
Я хотел бы запустить это.
Вот что я сделал до сих пор:
1.) Расширение приложения:
public class MyApplication extends Application {
@Override
public File getDatabasePath(String name) {
// Just a test
File file = super.getDatabasePath(name);
return file;
}
@Override
public void onCreate() {
// Just a test
super.onCreate();
}
}
2.) Добавление расширенного приложения в манифест:
<application
...
android:name="MyApplication"
... >
3.) Расширение и использование SQLiteOpenHelper:
public class MySqliteOpenHelper extends SQLiteOpenHelper {
public void onCreate(SQLiteDatabase sqliteDatabase) {
...
}
@Override
public void onUpgrade(SQLiteDatabase sqliteDatabase, int oldVersion, int newVersion) {
...
}
}
4.) Используя расширенный SQLiteOpenHelper в моих действиях обычным способом:
public class MyActivity extends Activity {
private MySqliteOpenHelper mySqliteOpenHelper;
private SQLiteDatabase sqliteDatabase;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
...
mySqliteOpenHelper = new MySqliteOpenHelper(getApplicationContext());
sqliteDatabase = mySqliteOpenHelper.getReadableDatabase();
...
}
@Override
protected void onDestroy() {
if (mySqliteOpenHelper != null) {
mySqliteOpenHelper.close();
mySqliteOpenHelper = null;
}
super.onDestroy();
}
}
Я хочу указать, что расширенный класс Application работает в целом. Я вижу это, потому что вызывается MyApplication.onCreate(). Но MyApplication.getDatabasePath() не вызывается.
Любая помощь приветствуется.
Ответы
Ответ 1
Я обнаружил, что могу использовать полный путь в Android 2.2, но в 2.1 метод Context.openOrCreateDatabase() создал исключение. Чтобы обойти это, я завернул этот метод, чтобы напрямую вызвать SQLiteDatabase.openOrCreateDatabase(). Вот конструктор моего расширенного SQLOpenHelper
public class Database extends SQLiteOpenHelper {
public Database(Context context) {
super(new ContextWrapper(context) {
@Override public SQLiteDatabase openOrCreateDatabase(String name,
int mode, SQLiteDatabase.CursorFactory factory) {
// allow database directory to be specified
File dir = new File(DIR);
if(!dir.exists()) {
dir.mkdirs();
}
return SQLiteDatabase.openDatabase(DIR + "/" + NAME, null,
SQLiteDatabase.CREATE_IF_NECESSARY);
}
}, NAME, null, VERSION);
this.context = context;
}
}
Ответ 2
Перезапись SQLOpenHelper для использования каталога SD-карты, а не контекста, а затем расширение, которое, похоже, работает для меня.
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteException;
import android.util.Log;
/**
* SDCardSQLiteOpenhelper is a class that is based on SQLiteOpenHelper except
* that it does not use the context to get the database. It was written owing to
* a bug in Android 4.0.3 so that using a ContextWrapper to override
* openOrCreateDatabase, as was done with Android 2.3.3, no longer worked. <br>
* <br>
* The mContext field has been replaced by mDir. It does not use lock on the
* database as that method is package private to
* android.database.sqlite.SQLiteDatabase. Otherwise the implementation is
* similar.<br>
* <br>
*
* @see android.database.sqlite.SQLiteOpenHelper
*/
public abstract class SDCardSQLiteOpenHelper {
private static final String TAG = SDCardSQLiteOpenHelper.class
.getSimpleName();
// private final Context mContext;
private final String mName;
private final String mDir;
private final CursorFactory mFactory;
private final int mNewVersion;
private SQLiteDatabase mDatabase = null;
private boolean mIsInitializing = false;
/**
* Create a helper object to create, open, and/or manage a database. This
* method always returns very quickly. The database is not actually created
* or opened until one of {@link #getWritableDatabase} or
* {@link #getReadableDatabase} is called.
*
* @param dir
* the directory on the SD card. It must exist and the SD card
* must be available. The caller should check this.
* @param name
* of the database file, or null for an in-memory database
* @param factory
* to use for creating cursor objects, or null for the default
* @param version
* number of the database (starting at 1); if the database is
* older, {@link #onUpgrade} will be used to upgrade the
* database; if the database is newer, {@link #onDowngrade} will
* be used to downgrade the database
*/
public SDCardSQLiteOpenHelper(String dir, String name,
CursorFactory factory, int version) {
if (version < 1)
throw new IllegalArgumentException("Version must be >= 1, was "
+ version);
// mContext = context;
mDir = dir;
mName = name;
mFactory = factory;
mNewVersion = version;
}
/**
* Return the name of the SQLite database being opened, as given to the
* constructor.
*/
public String getDatabaseName() {
return mName;
}
/**
* Create and/or open a database that will be used for reading and writing.
* The first time this is called, the database will be opened and
* {@link #onCreate}, {@link #onUpgrade} and/or {@link #onOpen} will be
* called.
*
* <p>
* Once opened successfully, the database is cached, so you can call this
* method every time you need to write to the database. (Make sure to call
* {@link #close} when you no longer need the database.) Errors such as bad
* permissions or a full disk may cause this method to fail, but future
* attempts may succeed if the problem is fixed.
* </p>
*
* <p class="caution">
* Database upgrade may take a long time, you should not call this method
* from the application main thread, including from
* {@link android.content.ContentProvider#onCreate
* ContentProvider.onCreate()}.
*
* @throws SQLiteException
* if the database cannot be opened for writing
* @return a read/write database object valid until {@link #close} is called
*/
public synchronized SQLiteDatabase getWritableDatabase() {
if (mDatabase != null) {
if (!mDatabase.isOpen()) {
// darn! the user closed the database by calling
// mDatabase.close()
mDatabase = null;
} else if (!mDatabase.isReadOnly()) {
return mDatabase; // The database is already open for business
}
}
if (mIsInitializing) {
throw new IllegalStateException(
"getWritableDatabase called recursively");
}
// If we have a read-only database open, someone could be using it
// (though they shouldn't), which would cause a lock to be held on
// the file, and our attempts to open the database read-write would
// fail waiting for the file lock. To prevent that, we acquire the
// lock on the read-only database, which shuts out other users.
boolean success = false;
SQLiteDatabase db = null;
// NOT AVAILABLE
// if (mDatabase != null) {
// mDatabase.lock();
// }
try {
mIsInitializing = true;
if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
String path = mDir + "/" + mName;
// db = mContext.openOrCreateDatabase(mName, 0, mFactory,
// mErrorHandler);
db = SQLiteDatabase.openDatabase(path, null,
SQLiteDatabase.CREATE_IF_NECESSARY);
}
int version = db.getVersion();
if (version != mNewVersion) {
db.beginTransaction();
try {
if (version == 0) {
onCreate(db);
} else {
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
onOpen(db);
success = true;
return db;
} finally {
mIsInitializing = false;
if (success) {
if (mDatabase != null) {
try {
mDatabase.close();
} catch (Exception e) {
// Do nothing
}
// NOT AVAILABLE
// mDatabase.unlock();
}
mDatabase = db;
} else {
// NOT AVAILABLE
// if (mDatabase != null) {
// mDatabase.unlock();
// }
if (db != null)
db.close();
}
}
}
/**
* Create and/or open a database. This will be the same object returned by
* {@link #getWritableDatabase} unless some problem, such as a full disk,
* requires the database to be opened read-only. In that case, a read-only
* database object will be returned. If the problem is fixed, a future call
* to {@link #getWritableDatabase} may succeed, in which case the read-only
* database object will be closed and the read/write object will be returned
* in the future.
*
* <p class="caution">
* Like {@link #getWritableDatabase}, this method may take a long time to
* return, so you should not call it from the application main thread,
* including from {@link android.content.ContentProvider#onCreate
* ContentProvider.onCreate()}.
*
* @throws SQLiteException
* if the database cannot be opened
* @return a database object valid until {@link #getWritableDatabase} or
* {@link #close} is called.
*/
public synchronized SQLiteDatabase getReadableDatabase() {
if (mDatabase != null) {
if (!mDatabase.isOpen()) {
// darn! the user closed the database by calling
// mDatabase.close()
mDatabase = null;
} else {
return mDatabase; // The database is already open for business
}
}
if (mIsInitializing) {
throw new IllegalStateException(
"getReadableDatabase called recursively");
}
try {
return getWritableDatabase();
} catch (SQLiteException e) {
if (mName == null)
throw e; // Can't open a temp database read-only!
Log.e(TAG, "Couldn't open " + mName
+ " for writing (will try read-only):", e);
}
SQLiteDatabase db = null;
try {
mIsInitializing = true;
// String path = mContext.getDatabasePath(mName).getPath();
String path = mDir + "/" + mName;
db = SQLiteDatabase.openDatabase(path, mFactory,
SQLiteDatabase.OPEN_READONLY);
if (db.getVersion() != mNewVersion) {
throw new SQLiteException(
"Can't upgrade read-only database from version "
+ db.getVersion() + " to " + mNewVersion + ": "
+ path);
}
onOpen(db);
Log.w(TAG, "Opened " + mName + " in read-only mode");
mDatabase = db;
return mDatabase;
} finally {
mIsInitializing = false;
if (db != null && db != mDatabase)
db.close();
}
}
/**
* Close any open database object.
*/
public synchronized void close() {
if (mIsInitializing)
throw new IllegalStateException("Closed during initialization");
if (mDatabase != null && mDatabase.isOpen()) {
mDatabase.close();
mDatabase = null;
}
}
/**
* Called when the database is created for the first time. This is where the
* creation of tables and the initial population of the tables should
* happen.
*
* @param db
* The database.
*/
public abstract void onCreate(SQLiteDatabase db);
/**
* Called when the database needs to be upgraded. The implementation should
* use this method to drop tables, add tables, or do anything else it needs
* to upgrade to the new schema version.
*
* <p>
* The SQLite ALTER TABLE documentation can be found <a
* href="http://sqlite.org/lang_altertable.html">here</a>. If you add new
* columns you can use ALTER TABLE to insert them into a live table. If you
* rename or remove columns you can use ALTER TABLE to rename the old table,
* then create the new table and then populate the new table with the
* contents of the old table.
*
* @param db
* The database.
* @param oldVersion
* The old database version.
* @param newVersion
* The new database version.
*/
public abstract void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion);
/**
* Called when the database needs to be downgraded. This is stricly similar
* to onUpgrade() method, but is called whenever current version is newer
* than requested one. However, this method is not abstract, so it is not
* mandatory for a customer to implement it. If not overridden, default
* implementation will reject downgrade and throws SQLiteException
*
* @param db
* The database.
* @param oldVersion
* The old database version.
* @param newVersion
* The new database version.
*/
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
throw new SQLiteException("Can't downgrade database from version "
+ oldVersion + " to " + newVersion);
}
/**
* Called when the database has been opened. The implementation should check
* {@link SQLiteDatabase#isReadOnly} before updating the database.
*
* @param db
* The database.
*/
public void onOpen(SQLiteDatabase db) {
}
}
Это было сделано, когда метод, описанный выше Роджером Кейсом, прекратил работу над Android 4.0.3.
Ответ 3
Этот код исправил мою аналогичную проблему, мой класс приложения:
@Override
public File getDatabasePath(String name) {
File result = new File(getExternalFilesDir(null), name);
return result;
}
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) {
return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), factory);
}
Надеюсь, это поможет вам.
Ответ 4
Ну, я думаю, вы не можете этого сделать. Если кто-то знает способ, сообщите нам, как.
Итак, когда вы вызываете
mySqliteOpenHelper.getReadableDatabase();
Все должно быть хорошо, если мы посмотрим на реализацию, мы видим, что:
String path = mContext.getDatabasePath(mName).getPath();
Все хорошо. Но если взглянуть на несколько строк:
return getWritableDatabase();
Таким образом, он на самом деле вызывает другой метод, и если он терпит неудачу, только тогда он пытается использовать getDatabasePath().
Если мы посмотрим на реализацию getWritableDatabase - мы можем ясно видеть, что он не использует getDatabasePath, а вместо этого:
db = mContext.openOrCreateDatabase(mName, 0, mFactory);
Это позволяет нам увидеть, как реализована openOrCreateDatabase, для чего мы рассмотрим ContextImpl.java
if (name.charAt(0) == File.separatorChar) {
String dirPath = name.substring(0, name.lastIndexOf(File.separatorChar));
dir = new File(dirPath);
name = name.substring(name.lastIndexOf(File.separatorChar));
f = new File(dir, name);
} else {
dir = getDatabasesDir();
f = makeFilename(dir, name);
}
Итак, мы можем видеть, что этот вспомогательный метод validateFilePath возвращает File, если он получает полный путь (например,/some/true/full/path) или пытается выполнить concat getDatabasesDir() с именем файла.
Функция getDatabasesDir() использует getDataDirFile(), которая является общедоступной и, возможно, теоретически может быть перезаписана.. но вы должны были бы проверить.
В настоящее время я вижу два решения:
1) Если вам не нужна сила доступа для чтения sqlite db в режиме readonly, getWritableDatabase завершится с ошибкой, и getDatabasePath получит вызов
2) Передайте полный путь в конструктор SQLiteOpenHelper и убедитесь, что db доступен для записи, например:
public class MyDbOpenHelper extends SQLiteOpenHelper {
public MyDbOpenHelper(final Context context) {
super(context, Environment.getExternalStorageDirectory()
+ "/path/to/database/on/sdcard/database.sqlite", null, 1);
}
Это действительно бессмысленно для меня, но, глядя на источники Android (как минимум 2.3.1), похоже, что так оно и реализуется.
Ответ 5
Вызов этой функции вызовет метод onCreate в вспомогательном классе SqliteOpen
public dbOperation open() throws SQLException
{
db = DBHelper.getWritableDatabase();
return this;
}
Метод oncreate похож на этот
public void onCreate(SQLiteDatabase db)
{
try {
db.execSQL(DATABASE_CREATE);
} catch (Exception e) {
e.printStackTrace();
}
}
DATABASE_CREATE - это строка, содержащая запрос на создание базы данных
Ответ 6
Ваша база данных хранится во внутренней памяти, так что другие приложения не могут получить к ней доступ и изменять/повреждать данные.
Путь по умолчанию для базы данных Android является /data/data/APPLICATIONPACKAGENAME/databases/. Ниже приведено довольно хорошее руководство по хранению базы данных в файле, а затем заполнению ее во время выполнения.
Статья