1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-24 09:13:04 +02:00

Introduce new database version (2)

- Change the unique constraint for measurements to include userId in
  addition to datetime. This way two users can now have measurements
  with the same date and time (hour and minute).
- Enforce that the userId references an existing user.
This commit is contained in:
Erik Johansson
2018-01-24 21:39:22 +01:00
parent 48921a4807
commit 212b32f632
5 changed files with 372 additions and 3 deletions

View File

@@ -0,0 +1,195 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "166a2a83c723c4117edaf1d107ac5194",
"entities": [
{
"tableName": "scaleMeasurements",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `datetime` INTEGER, `weight` REAL NOT NULL, `fat` REAL NOT NULL, `water` REAL NOT NULL, `muscle` REAL NOT NULL, `lbw` REAL NOT NULL, `waist` REAL NOT NULL, `hip` REAL NOT NULL, `bone` REAL NOT NULL, `comment` TEXT, FOREIGN KEY(`userId`) REFERENCES `scaleUsers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateTime",
"columnName": "datetime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "fat",
"columnName": "fat",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "water",
"columnName": "water",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "muscle",
"columnName": "muscle",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "lbw",
"columnName": "lbw",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "waist",
"columnName": "waist",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "hip",
"columnName": "hip",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "bone",
"columnName": "bone",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_scaleMeasurements_datetime_userId",
"unique": true,
"columnNames": [
"datetime",
"userId"
],
"createSql": "CREATE UNIQUE INDEX `index_scaleMeasurements_datetime_userId` ON `${TABLE_NAME}` (`datetime`, `userId`)"
}
],
"foreignKeys": [
{
"table": "scaleUsers",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "scaleUsers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `username` TEXT, `birthday` INTEGER, `bodyHeight` INTEGER NOT NULL, `scaleUnit` INTEGER NOT NULL, `gender` INTEGER NOT NULL, `initialWeight` REAL NOT NULL, `goalWeight` REAL NOT NULL, `goalDate` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userName",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bodyHeight",
"columnName": "bodyHeight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scaleUnit",
"columnName": "scaleUnit",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gender",
"columnName": "gender",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "initialWeight",
"columnName": "initialWeight",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "goalWeight",
"columnName": "goalWeight",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "goalDate",
"columnName": "goalDate",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"166a2a83c723c4117edaf1d107ac5194\")"
]
}
}

View File

@@ -0,0 +1,110 @@
/* Copyright (C) 2018 Erik Johansson <erik@ejohansson.se>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package com.health.openscale;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.db.SupportSQLiteOpenHelper;
import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
import android.arch.persistence.room.testing.MigrationTestHelper;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.health.openscale.core.database.AppDatabase;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class DatabaseMigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public DatabaseMigrationTest() {
helper = new MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws Exception {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
ContentValues users = new ContentValues();
for (int i = 1; i < 4; ++i) {
users.put("id", i);
users.put("username", String.format("test%d", i));
users.put("bodyHeight", i * 50);
users.put("scaleUnit", 0);
users.put("gender", 0);
users.put("initialWeight", i * 25);
users.put("goalWeight", i * 20);
assertNotSame(-1, db.insert("scaleUsers", SQLiteDatabase.CONFLICT_ABORT, users));
}
ContentValues measurement = new ContentValues();
for (int i = 2; i < 5; ++i) {
for (int j = 0; j < 2; ++j) {
measurement.put("userId", i);
measurement.put("enabled", j);
measurement.put("comment", "a string");
for (String type : new String[]{"weight", "fat", "water", "muscle", "lbw", "waist", "hip", "bone"}) {
measurement.put(type, i * j);
}
assertNotSame(-1, db.insert("scaleMeasurements", SQLiteDatabase.CONFLICT_ABORT, measurement));
}
}
// Prepare for the next version.
db.close();
// Re-open the database with version 2 and provide MIGRATION_1_2 as the migration process.
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, AppDatabase.MIGRATION_1_2);
// MigrationTestHelper automatically verifies the schema changes.
Cursor cursor = db.query("SELECT * FROM scaleMeasurements ORDER BY id, userId");
assertEquals(2 * 2, cursor.getCount());
cursor.moveToFirst();
for (int i = 2; i < 4; ++i) {
for (int j = 0; j < 2; ++j) {
assertEquals(i, cursor.getInt(cursor.getColumnIndex("userId")));
assertEquals(j, cursor.getInt(cursor.getColumnIndex("enabled")));
assertEquals("a string", cursor.getString(cursor.getColumnIndex("comment")));
for (String type : new String[]{"weight", "fat", "water", "muscle", "lbw", "waist", "hip", "bone"}) {
assertEquals((float) i * j, cursor.getFloat(cursor.getColumnIndex(type)));
}
cursor.moveToNext();
}
}
assertTrue(cursor.isAfterLast());
}
}

View File

@@ -16,11 +16,14 @@
package com.health.openscale.core;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.text.format.DateFormat;
import android.util.Log;
@@ -81,7 +84,17 @@ public class OpenScale {
alarmHandler = new AlarmHandler();
btCom = null;
fragmentList = new ArrayList<>();
appDB = Room.databaseBuilder(context, AppDatabase.class, "openScale.db").allowMainThreadQueries().build();
appDB = Room.databaseBuilder(context, AppDatabase.class, "openScale.db")
.allowMainThreadQueries()
.addCallback(new RoomDatabase.Callback() {
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
db.setForeignKeyConstraintsEnabled(true);
}
})
.addMigrations(AppDatabase.MIGRATION_1_2)
.build();
measurementDAO = appDB.measurementDAO();
userDAO = appDB.userDAO();

View File

@@ -16,18 +16,62 @@
package com.health.openscale.core.database;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.TypeConverters;
import android.arch.persistence.room.migration.Migration;
import android.support.annotation.NonNull;
import com.health.openscale.core.datatypes.ScaleMeasurement;
import com.health.openscale.core.datatypes.ScaleUser;
import com.health.openscale.core.utils.Converters;
@Database(entities = {ScaleMeasurement.class, ScaleUser.class}, version = 1)
@Database(entities = {ScaleMeasurement.class, ScaleUser.class}, version = 2)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract ScaleMeasurementDAO measurementDAO();
public abstract ScaleUserDAO userDAO();
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.beginTransaction();
try {
// Drop old index on datetime only
database.execSQL("DROP INDEX index_scaleMeasurements_datetime");
// Rename old table
database.execSQL("ALTER TABLE scaleMeasurements RENAME TO scaleMeasurementsOld");
// Create new table with foreign key
database.execSQL("CREATE TABLE scaleMeasurements"
+ " (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
+ " userId INTEGER NOT NULL, enabled INTEGER NOT NULL,"
+ " datetime INTEGER, weight REAL NOT NULL, fat REAL NOT NULL,"
+ " water REAL NOT NULL, muscle REAL NOT NULL, lbw REAL NOT NULL,"
+ " waist REAL NOT NULL, hip REAL NOT NULL, bone REAL NOT NULL,"
+ " comment TEXT, FOREIGN KEY(userId) REFERENCES scaleUsers(id)"
+ " ON UPDATE NO ACTION ON DELETE CASCADE)");
// Create new index on datetime + userId
database.execSQL("CREATE UNIQUE INDEX index_scaleMeasurements_datetime_userId"
+ " ON scaleMeasurements (datetime, userId)");
// Copy data from the old table, ignoring those with invalid userId (if any)
database.execSQL("INSERT INTO scaleMeasurements"
+ " SELECT * FROM scaleMeasurementsOld"
+ " WHERE userId IN (SELECT id from scaleUsers)");
// Delete old table
database.execSQL("DROP TABLE scaleMeasurementsOld");
database.setTransactionSuccessful();
}
finally {
database.endTransaction();
}
}
};
}

View File

@@ -18,6 +18,7 @@ package com.health.openscale.core.datatypes;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
@@ -26,7 +27,13 @@ import com.j256.simplecsv.common.CsvColumn;
import java.util.Date;
@Entity(tableName = "scaleMeasurements", indices = {@Index(value = {"datetime"}, unique = true)})
@Entity(tableName = "scaleMeasurements",
indices = {@Index(value = {"datetime", "userId"}, unique = true)},
foreignKeys = @ForeignKey(
entity = ScaleUser.class,
parentColumns = "id",
childColumns = "userId",
onDelete = ForeignKey.CASCADE))
public class ScaleMeasurement implements Cloneable {
@PrimaryKey(autoGenerate = true)