简体   繁体   中英

Seperately log Room sqlite database operations in text file

I would like to log all database insert , delete , update operations in a text file .

Scenario:

I use room local database and api, when user is offline I would like to keep record of operations in a text file since after syncing those records wil be deleted. And just to make sure if something goes wrong and syncronization fails, I will still have access to text file . Or is there any other recommended approach?

If you want some form of textual representation what could potentially drive a restore, rather than taking copies of the database then you could utilise TRIGGERS that record(log) the data to a table used for logging.

However, currently Room doesn't support building creating triggers from annotations. So they need to be built and added by running suitable code. Running such code would probably be via a CallBack and probably the onCreate callback.

Here's a simple (as in just a single table) example that logs Inserts into the main table (Table1) recording them into the LogTable:-

First the supportive LogTable , that includes some supportive Constants and methods :-

@Entity(tableName = LogTable.TABLE_NAME)
public class LogTable {
    public static final int ACTION_AFTER_INSERT = 11;
    public static final int ACTION_BEFORE_UPDATE = 1;
    public static final int ACTION_AFTER_UPDATE = 2;
    public static final int ACTION_BEFORE_DELETE = 3;
    public static final String SEPARATOR = "~";

    public static final String TABLE_NAME = "log";
    public static final String ID_COL = TABLE_NAME + BaseColumns._ID;
    public static final String TIMESTAMP_COL = "_timestamp";
    public static final String TABLENAME_COL = "_tablename";
    public static final String ACTION_COL = "_action";
    public static final String DATA_COL = "_data";

    @PrimaryKey
    @ColumnInfo(name = ID_COL)
    Long id;
    @ColumnInfo(name = TIMESTAMP_COL,defaultValue = "(strftime('%s','now'))")
    Long timestamp;
    @ColumnInfo(name = TABLENAME_COL)
    String tableName;
    @ColumnInfo(name = ACTION_COL)
    int action;
    @ColumnInfo(name = DATA_COL)
    String data;

    public static String actionAsString(int action) {
        switch (action) {
            case ACTION_AFTER_INSERT:
                return "INSERT";
            case ACTION_AFTER_UPDATE:
                return "AFTER UPDATE";
            case ACTION_BEFORE_UPDATE:
                return "BEFORE UPDATE";
            case ACTION_BEFORE_DELETE:
                return "DELETE";
            default:
                return "UNKNOWN";
        }
    }
}
  • the log table is designed to cater for any table and hence the tablename. The data column will contain a potentially usable list of the values.

The main table Table1 which includes it's INSERT trigger SQL (you need similar for DELETE and UPDATE (perhaps before and after)) :-

public class Table1 {
    public static final String TABLE_NAME = "t1";
    public static final String ID_COLUMN = TABLE_NAME + BaseColumns._ID;
    public static final String NAME_COLUMN = TABLE_NAME + "_name";
    public static final String OTHER_COLUMN = TABLE_NAME + "_other";

    public static final String LOGCOLVALUES =
            "'" + TABLE_NAME + "',"
            + LogTable.ACTION_AFTER_INSERT + ","
            + "'" + ID_COLUMN+  "='|| new." + ID_COLUMN + "||'" + LogTable.SEPARATOR
            +  NAME_COLUMN + "='|| new." + NAME_COLUMN + "||'" + LogTable.SEPARATOR
            + OTHER_COLUMN + "='|| new." + OTHER_COLUMN
            ;


    @PrimaryKey
    @ColumnInfo(name = ID_COLUMN)
    Long id;
    @ColumnInfo(name = NAME_COLUMN)
    String name;
    @ColumnInfo(name = OTHER_COLUMN)
    String other;

    public static final String INSERT_TRIGGER_SQL =
            "CREATE TRIGGER IF NOT EXISTS " + TABLE_NAME + "_insert " +
                    "AFTER INSERT ON " + Table1.TABLE_NAME +
                    " BEGIN " +
                    " INSERT INTO " + LogTable.TABLE_NAME +
                    "(" +
                    LogTable.TABLENAME_COL + "," +
                    LogTable.ACTION_COL + "," +
                    LogTable.DATA_COL +
                    ")" +
                    " VALUES(" +
                    LOGCOLVALUES +
                    "); END;";

}
  • The LOGCOLVALUES constant generates the relatively complex VALUES clause.
  • the new. refers to the data being updated. Noting that for an UPDATE new. refers to the updated value whilst old. refers to the data before the update. For an INSERT only new. is applicable, for DELETE only old.

For the above the trigger SQL, when resolved, is :-

CREATE TRIGGER IF NOT EXISTS t1_insert AFTER INSERT ON t1 BEGIN  INSERT INTO log(_tablename,_action,_data) VALUES('t1',11,'t1_id='|| new.t1_id||'~t1_name='|| new.t1_name||'~t1_other='|| new.t1_other); END;

The Dao's used for the example in AllDao are simply :-

@Dao
abstract class AllDao {

    @Insert
    abstract long insert(Table1 table1);
    @Query("SELECT * FROM log")
    abstract List<LogTable> getAllLogs();
}

The @Database class TheDatabase , where the TRIGGER is build in the onCreate method is :-

@Database(entities = {Table1.class,LogTable.class},version = 1)
abstract class TheDatabase extends RoomDatabase {

    abstract AllDao getAllDao();

    private static volatile TheDatabase instance = null;

    public static TheDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context,TheDatabase.class,"my.db")
                    .addCallback(cb)
                    .allowMainThreadQueries()
                    .build();
        }
        return instance;
    }

    private static Callback cb = new Callback() {
        @Override
        public void onCreate(SupportSQLiteDatabase db) {
            super.onCreate(db);
            db.execSQL(Table1.INSERT_TRIGGER_SQL);
        }

        @Override
        public void onOpen(SupportSQLiteDatabase db) {
            super.onOpen(db);
        }

        @Override
        public void onDestructiveMigration(SupportSQLiteDatabase db) {
            super.onDestructiveMigration(db);
        }
    };
}
  • obviously with a full implementation then each "main" table would have 3 triggers (update, insert and delete).

Putting all the above into a working example is MainActivity , which inserts 10 rows and then extracts data from the log :-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDao dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        db = TheDatabase.getInstance(this);
        dao = db.getAllDao();

        Table1 t1 = new Table1();
        for(int i=0;i < 10; i++) {
            t1.name = "Test"+i;
            t1.other = "TestOtherData"+i;
            dao.insert(t1);
        }
        for(LogTable l: dao.getAllLogs()) {
            Log.d(
                    "LOGTABLEINFO",
                    "ID = " + l.id +
                            " TS = " + l.timestamp +
                            " Table =" + l.tableName +
                            " Action = " + LogTable.actionAsString(l.action) +
                            " Values = " + l.data
            );
        }
    }
}

RESULT

When run (twice) then the log contains :-

2021-07-24 10:26:58.830 D/LOGTABLEINFO: ID = 1 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=1~t1_name=Test0~t1_other=TestOtherData0
2021-07-24 10:26:58.830 D/LOGTABLEINFO: ID = 2 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=2~t1_name=Test1~t1_other=TestOtherData1
2021-07-24 10:26:58.830 D/LOGTABLEINFO: ID = 3 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=3~t1_name=Test2~t1_other=TestOtherData2
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 4 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=4~t1_name=Test3~t1_other=TestOtherData3
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 5 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=5~t1_name=Test4~t1_other=TestOtherData4
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 6 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=6~t1_name=Test5~t1_other=TestOtherData5
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 7 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=7~t1_name=Test6~t1_other=TestOtherData6
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 8 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=8~t1_name=Test7~t1_other=TestOtherData7
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 9 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=9~t1_name=Test8~t1_other=TestOtherData8
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 10 TS = 1627084992 Table =t1 Action = INSERT Values = t1_id=10~t1_name=Test9~t1_other=TestOtherData9
2021-07-24 10:26:58.831 D/LOGTABLEINFO: ID = 11 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=11~t1_name=Test0~t1_other=TestOtherData0
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 12 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=12~t1_name=Test1~t1_other=TestOtherData1
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 13 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=13~t1_name=Test2~t1_other=TestOtherData2
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 14 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=14~t1_name=Test3~t1_other=TestOtherData3
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 15 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=15~t1_name=Test4~t1_other=TestOtherData4
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 16 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=16~t1_name=Test5~t1_other=TestOtherData5
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 17 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=17~t1_name=Test6~t1_other=TestOtherData6
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 18 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=18~t1_name=Test7~t1_other=TestOtherData7
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 19 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=19~t1_name=Test8~t1_other=TestOtherData8
2021-07-24 10:26:58.832 D/LOGTABLEINFO: ID = 20 TS = 1627086418 Table =t1 Action = INSERT Values = t1_id=20~t1_name=Test9~t1_other=TestOtherData9

Supplementary

Taking the above a little further here's an example with all 4 Triggers, the Triggers being built by methods in the LogTable Entity/Class :-

Table1 has been simplified as LogTable builds the triggers based upon the trigger action, the table name and the table's columns. It is now:-

@Entity(tableName = TABLE_NAME)
public class Table1 {
    public static final String TABLE_NAME = "t1";
    public static final String ID_COLUMN = TABLE_NAME + BaseColumns._ID;
    public static final String NAME_COLUMN = TABLE_NAME + "_name";
    public static final String OTHER_COLUMN = TABLE_NAME + "_other";

    @PrimaryKey
    @ColumnInfo(name = ID_COLUMN)
    Long id;
    @ColumnInfo(name = NAME_COLUMN)
    String name;
    @ColumnInfo(name = OTHER_COLUMN)
    String other;

}

LogTable has been changed to be :-

@Entity(tableName = LogTable.TABLE_NAME)
public class LogTable {
    public static final int ACTION_AFTER_INSERT = 11;
    public static final int ACTION_BEFORE_UPDATE = 1;
    public static final int ACTION_AFTER_UPDATE = 2;
    public static final int ACTION_BEFORE_DELETE = 3;
    public static final String SEPARATOR = "~";

    public static final String TABLE_NAME = "log";
    public static final String ID_COL = TABLE_NAME + BaseColumns._ID;
    public static final String TIMESTAMP_COL = "_timestamp";
    public static final String TABLENAME_COL = "_tablename";
    public static final String ACTION_COL = "_action";
    public static final String DATA_COL = "_data";

    @PrimaryKey
    @ColumnInfo(name = ID_COL)
    Long id;
    @ColumnInfo(name = TIMESTAMP_COL,defaultValue = "(strftime('%s','now'))")
    Long timestamp;
    @ColumnInfo(name = TABLENAME_COL)
    String tableName;
    @ColumnInfo(name = ACTION_COL)
    int action;
    @ColumnInfo(name = DATA_COL)
    String data;

    public static String actionAsString(int action) {
        switch (action) {
            case ACTION_AFTER_INSERT:
                return "AFTER INSERT";
            case ACTION_AFTER_UPDATE:
                return "AFTER UPDATE";
            case ACTION_BEFORE_UPDATE:
                return "BEFORE UPDATE";
            case ACTION_BEFORE_DELETE:
                return "AFTER DELETE";
            default:
                return "UNKNOWN";
        }
    }
    public static String buildTriggerSQL(int action, String tableName, String[] columns) {
        return buildTriggerSQL(action,tableName,columns,false);
    }

    public static String buildTriggerSQL(int action, String tableName, String[] columns, boolean logResult) {

        // Prepare the appropriate old or new for extracting data
        String retrieve;
        switch (action) {
            case ACTION_AFTER_INSERT: case ACTION_AFTER_UPDATE:
                retrieve = "new.";
                break;
            default:
                retrieve = "old.";
        }
        StringBuilder sb = new StringBuilder().append("CREATE TRIGGER IF NOT EXISTS ");
        // TriggerName
        sb
                // Trigger Name
                .append("_")
                .append(tableName)
                .append("_")
                .append(actionAsString(action).replace(' ','_'))
                // Trigger Action ON clause
                .append(" ").append(actionAsString(action)).append(" ON  ").append(tableName)
                .append(" BEGIN INSERT INTO ").append(TABLE_NAME).append("(")
                // The Log Table columns to be set (ID and timestamp generated by defaults)
                .append(TABLENAME_COL).append(",").append(ACTION_COL).append(",").append(DATA_COL).append(") ")
                // The fixed VALUES to be inserted
                .append(" VALUES(")
                .append("'").append(tableName).append("',")
                .append(action).append(",")
        ;
        // Build the values to be extracted from the changed table for the log table's data column
        boolean afterFirst = false;
        for (String s: columns) {
            // if not first line add separator between the previous and next
            if (afterFirst) {
                sb.append("||'").append(SEPARATOR).append("'||");
            }
            sb.append("'").append(s).append("='|| ")
                    .append(retrieve).append(s);
            afterFirst = true;
        }
        if (logResult) Log.d("LOGTBL_BLDTRGSQL","Trigger SQL = \n\t" + sb.append("); END;").toString());
        return sb.append("); END;").toString();
    }
}
  • TRIGGERS are named
  • _<the_tablename>_<the_action_with_spaces_replaced_by_underscores>

AllDao has been modified to include update and delete and also getById :-

@Dao
abstract class AllDao {

    @Insert
    abstract long insert(Table1 table1);
    @Query("SELECT * FROM log")
    abstract List<LogTable> getAllLogs();
    @Update
    abstract int update(Table1 table1);
    @Delete
    abstract int delete(Table1 table1);
    @Query("SELECT * FROM t1 WHERE t1_id =:id")
    abstract Table1 getTable1ById(long id);
}

TheDatabase now builds all 4 TRIGGERS and is :-

@Database(entities = {Table1.class,LogTable.class},version = 1)
abstract class TheDatabase extends RoomDatabase {

    abstract AllDao getAllDao();

    private static volatile TheDatabase instance = null;

    public static TheDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context,TheDatabase.class,"my.db")
                    .addCallback(cb)
                    .allowMainThreadQueries()
                    .build();
        }
        return instance;
    }

    private static Callback cb = new Callback() {
        @Override
        public void onCreate(SupportSQLiteDatabase db) {
            super.onCreate(db);
            String[] table1Columns = new String[]{Table1.ID_COLUMN,Table1.NAME_COLUMN,Table1.OTHER_COLUMN};
            db.execSQL(LogTable.buildTriggerSQL(LogTable.ACTION_AFTER_INSERT,Table1.TABLE_NAME,table1Columns));
            db.execSQL(LogTable.buildTriggerSQL(LogTable.ACTION_BEFORE_UPDATE,Table1.TABLE_NAME,table1Columns));
            db.execSQL(LogTable.buildTriggerSQL(LogTable.ACTION_AFTER_UPDATE,Table1.TABLE_NAME,table1Columns));
            db.execSQL(LogTable.buildTriggerSQL(LogTable.ACTION_BEFORE_DELETE,Table1.TABLE_NAME,table1Columns));
        }

        @Override
        public void onOpen(SupportSQLiteDatabase db) {
            super.onOpen(db);
        }

        @Override
        public void onDestructiveMigration(SupportSQLiteDatabase db) {
            super.onDestructiveMigration(db);
        }
    };
}

Finally MainActivity includes update and delete :-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDao dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        db = TheDatabase.getInstance(this);
        dao = db.getAllDao();

        Table1 t1 = new Table1();
        for(int i=0;i < 10; i++) {
            t1.name = "Test"+i;
            t1.other = "TestOtherData"+i;
            dao.insert(t1);
        }
        t1.id = 1L;
        t1.other = "Updated Other Data";
        dao.update(t1);
        dao.delete(dao.getTable1ById(5L));
        for(LogTable l: dao.getAllLogs()) {
            Log.d(
                    "LOGTABLEINFO",
                    "ID = " + l.id +
                            " TS = " + l.timestamp +
                            " Table =" + l.tableName +
                            " Action = " + LogTable.actionAsString(l.action) +
                            " Values = " + l.data
            );
        }
    }
}

Results :-

2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 1 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=1~t1_name=Test0~t1_other=TestOtherData0
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 2 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=2~t1_name=Test1~t1_other=TestOtherData1
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 3 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=3~t1_name=Test2~t1_other=TestOtherData2
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 4 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=4~t1_name=Test3~t1_other=TestOtherData3
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 5 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=5~t1_name=Test4~t1_other=TestOtherData4
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 6 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=6~t1_name=Test5~t1_other=TestOtherData5
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 7 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=7~t1_name=Test6~t1_other=TestOtherData6
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 8 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=8~t1_name=Test7~t1_other=TestOtherData7
2021-07-25 13:53:33.800 D/LOGTABLEINFO: ID = 9 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=9~t1_name=Test8~t1_other=TestOtherData8
2021-07-25 13:53:33.801 D/LOGTABLEINFO: ID = 10 TS = 1627185213 Table =t1 Action = AFTER INSERT Values = t1_id=10~t1_name=Test9~t1_other=TestOtherData9
2021-07-25 13:53:33.801 D/LOGTABLEINFO: ID = 11 TS = 1627185213 Table =t1 Action = BEFORE UPDATE Values = t1_id=1~t1_name=Test0~t1_other=TestOtherData0
2021-07-25 13:53:33.801 D/LOGTABLEINFO: ID = 12 TS = 1627185213 Table =t1 Action = AFTER UPDATE Values = t1_id=1~t1_name=Test9~t1_other=Updated Other Data
2021-07-25 13:53:33.801 D/LOGTABLEINFO: ID = 13 TS = 1627185213 Table =t1 Action = AFTER DELETE Values = t1_id=5~t1_name=Test4~t1_other=TestOtherData4

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM