Ошибки при тестировании хранилища базы данных - SQLBrite, SQLDelight

При тестировании моей БД я получаю следующие ошибки:

  • SQLiteDiskIOException: ошибка ввода-вывода диска (HTC Desire 620)
  • SQLiteReadOnlyDatabaseException: попытка написать базу данных только для чтения (Moto g2)

В зависимости, видимо, от устройства, на котором я тестирую. Ошибка не происходит, когда я запускаю приложение. Тем не менее, если я не могу протестировать приложение, возможно, что-то не так с моим кодом.

Приложение использует две библиотеки, которые должны хорошо сочетаться с SQLDelight и SQLBrite, что может сделать этот вопрос несколько конкретным.

Для лучшего понимания происходящего я приведу краткое описание файлов в моем пакете данных.

-+-data-+
 |      |-manager-+
 |      |         |-LocationManager
 |      |         |-RunManager
 |      |-model-+
 |      |       |-Location
 |      |       |-Run
 |-DatabaseContract
 |-DataRepository
 |-MyDBHelper

Файлы Location а также Run являются моделями строк, сгенерированными SQLDelight. LocationManager а также RunManager включить генерацию sqlStatements для вставки или удаления данных из соответствующей таблицы. Ниже RunManager LocationMangager выглядит похожим.

public class RunManager {

    public final Run.InsertRun insertRace;
    public final Run.DeleteRun deleteRun;
    public final Run.DeleteRunWhereTimeSmallerThan deleteRunWhereTimeSmallerThan;

    public RunManager(SQLiteDatabase db) {
        insertRace = new Run.InsertRun(db);
        deleteRun = new Run.DeleteRun(db);
        deleteRunWhereTimeSmallerThan = new Run.DeleteRunWhereTimeSmallerThan(db);
    }

}

MyDBHelper расширяет SQLiteOpenHelper стандартным способом.

public class MyDbHelper extends SQLiteOpenHelper {

    private static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "runner.db";

    private static MyDbHelper INSTANCE = null;

    private MyDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    public static MyDbHelper getInstance(Context context) {
        if (INSTANCE == null) {
            INSTANCE = new MyDbHelper(context);
        }
        return INSTANCE;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        // create table (omitted)
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        // upgrade table (omitted)
    }
}

Хранилище данных объединяет различные операции вставки и запроса.

public class DataRepository implements DatabaseContract {

    private static DataRepository INSTANCE = null;

    BriteDatabase briteDatabase;
    LocationManger locationManger;
    RunManager runManager;

    private DataRepository(Context context) {
        SqlBrite sqlBrite = new SqlBrite.Builder().build();
        MyDbHelper helper = MyDbHelper.getInstance(context);
        briteDatabase = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());
        locationManger = new LocationManger(briteDatabase.getWritableDatabase());
        runManager = new RunManager(briteDatabase.getWritableDatabase());
    }

    public static DataRepository getInstance(Context context) {
        if (null == INSTANCE) {
            INSTANCE = new DataRepository(context);
        }
        return INSTANCE;
    }

    @Override
    public Observable<List<Run>> getAllRun() {
        return briteDatabase.createQuery(
                Run.TABLE_NAME,
                Run.SELECT_ALL
        ).mapToList(Run.MAPPER::map);
    }

    @Override
    public Observable<List<Location>> getLocationsForRun(long id) {
        return briteDatabase.createQuery(
                Location.TABLE_NAME, Location.FACTORY.selectAllByRace(id).statement
        ).mapToList(Location.MAPPER::map);
    }

    @Override
    public long insertRun(double distance, long duration, double avgSpeed, long timestamp) {
        runManager.insertRace.bind(distance, duration, avgSpeed, timestamp);
        return briteDatabase.executeInsert(Run.TABLE_NAME, runManager.insertRace.program);
    }

    @Override
    public void deleteRun(long id) {
        runManager.deleteRun.bind(id);
        briteDatabase.executeUpdateDelete(Run.TABLE_NAME, runManager.deleteRun.program);
    }

    @Override
    public void deleteRunWhereTimestampSmallerThan(long timestamp) {
        runManager.deleteRunWhereTimeSmallerThan.bind(timestamp);
        briteDatabase.executeUpdateDelete(Run.TABLE_NAME, runManager.deleteRunWhereTimeSmallerThan.program);
    }

    @Override
    public long insertLocation(long raceId, double lat, double lng, double alt, long timestamp) {
        locationManger.insertLocation.bind(raceId, lat, lng, alt, timestamp);
        return briteDatabase.executeInsert(Location.TABLE_NAME, locationManger.insertLocation.program);
    }

    public Observable<List<SingleRun>> getAllSingleRunModels() {
        return briteDatabase.createQuery(
                Run.TABLE_NAME,
                Run.SELECT_ALL
        ).mapToList(Run.MAPPER::map)
        // omitted

}

Теперь к основной части вопроса. На данный момент я написал следующие тестовые примеры и столкнулся с ошибками, перечисленными вверху. Интересно, что когда я запускаю тесты отдельно, я не получаю никаких ошибок, все тесты проходят.

@RunWith(AndroidJUnit4.class)
@LargeTest
public class DataRepositoryTest {

    private Context context;
    private DataRepository mDataRepository;

    @Before
    public void setUp() {
        context = InstrumentationRegistry.getTargetContext();
        context.deleteDatabase(MyDbHelper.DATABASE_NAME);
        mDataRepository = DataRepository.getInstance(InstrumentationRegistry.getTargetContext());
    }

    @Test
    public void testPreConditions() {
        Assert.assertNotNull(context);
        Assert.assertNotNull(mDataRepository);
    }

    @Test
    public void testInsertRace() { // this test failes when all tests are run.
        long raceID1 = mDataRepository.insertRun(5.0, 35, 3.5, 1000);
        Assert.assertEquals(1, raceID1);
        long raceID2 = mDataRepository.insertRun(10.0, 70, 3.5, 2000);
        Assert.assertEquals(2, raceID2);
        long locationID1 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1000);
        Assert.assertEquals(1, locationID1);
        long locationID2 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1001);
        Assert.assertEquals(2, locationID2);
        long locationID3 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1002);
        Assert.assertEquals(3, locationID3);
        long locationID4 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1003);
        Assert.assertEquals(4, locationID4);
        long locationID5 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2000);
        Assert.assertEquals(5, locationID5);
        long locationID6 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2001);
        Assert.assertEquals(6, locationID6);
        long locationID7 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2002);
        Assert.assertEquals(7, locationID7);
        long locationID8 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2003);
        Assert.assertEquals(8, locationID8);
    }

    @Test
    public void testRaceObservable() {
        long raceID1 = mDataRepository.insertRun(5.0, 35, 3.5, 1000);
        Run run1 = Run.FACTORY.creator.create(raceID1, 5.0, 35l, 3.5, 1000l);
        Assert.assertEquals(1, raceID1);
        long raceID2 = mDataRepository.insertRun(10.0, 70, 3.5, 2000);
        Run run2 = Run.FACTORY.creator.create(raceID2, 10.0, 70l, 3.5, 2000l);
        Assert.assertEquals(2, raceID2);
        List<Run> expectedResult = Arrays.asList(run1, run2);
        Assert.assertEquals(expectedResult, mDataRepository.getAllRun().blockingFirst());
    }


}

Я предполагаю, что это как-то связано с доступом к БД из разных потоков, но я не знаю, как решить проблему.

1 ответ

Решение

Ваша проблема в том, что когда setUp работает во второй раз, DataRepository.getInstance возвращает старый репозиторий данных, то есть он не создает новый SQLiteOpenHelper, При удалении базы данных вам также необходимо очистить свои синглтоны для DataRepository и MyDbHelper.

В качестве альтернативы вообще не используйте синглтоны:

@Before
public void setUp() {
    context = InstrumentationRegistry.getTargetContext();
    context.deleteDatabase(MyDbHelper.DATABASE_NAME);
    mDataRepository = new DataRepository(InstrumentationRegistry.getTargetContext());
}

// In DataRepository.java
DataRepository(Context context) {
    SqlBrite sqlBrite = new SqlBrite.Builder().build();
    MyDbHelper helper = new MyDbHelper(context);
    briteDatabase = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());
    locationManger = new LocationManger(briteDatabase.getWritableDatabase());
    runManager = new RunManager(briteDatabase.getWritableDatabase());
}

// In MyDbHelper.java
MyDbHelper(Context context) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
Другие вопросы по тегам