Search code examples
flutterauto-incrementdaosqflite

Why Flutter Floor DAO findAll returns entity set with null ids?


This is a Flutter project using Floor (package equivalent of Jetpack's Room). I have an entity with an auto increment id (note that this is pre null-safety code, in the answer I start now with post null safety snippet):

const String ACTIVITIES_TABLE_NAME = 'activities';

@Entity(
  tableName: ACTIVITIES_TABLE_NAME,
  indices: [
    Index(value: ['start'])
  ],
)
class Activity {
  @PrimaryKey(autoGenerate: true)
  int id;
  @ColumnInfo(name: 'device_name')
  final String deviceName;
  @ColumnInfo(name: 'device_id')
  final String deviceId;
  final int start;

  Activity({
    this.deviceName,
    this.deviceId,
    this.start,
  });
}

I have this DAO:

@dao
abstract class ActivityDao {
  @Query('SELECT * FROM $ACTIVITIES_TABLE_NAME ORDER BY start DESC')
  Future<List<Activity>> findAllActivities();

  @insert
  Future<int> insertActivity(Activity activity);
}

In my app I recorded five activities so far, hunky-dory.

_database =
    await $FloorAppDatabase.databaseBuilder('app_database.db').build();
...
    _activity =
       Activity(deviceName: device.name, deviceId: device.id.id, start: _lastRecord.millisecondsSinceEpoch);
    final id = await _database.activityDao.insertActivity(_activity);
    _activity.id = id;

Then in another view I list them, but there is a lurking problem.

final data = await _database.activityDao.findAllActivities();

When I query the entities with this dead simple method, all the five or so activities in my DB are returned with an id field filled with null. That makes the entity completely useless because any other operation I would like to perform on it fails due to lack of actual id. Am I doing something wrong?

I mostly have experience with MySQL, Postgres and non SQLite RDBMS. As I understand in SQLite every row has a unique rowid, and by declaring my auto increment id primary key field basically I alias that rowid? Whatever it is, I need the id. It cannot be null.


I'm debugging the guts of Floor.

  Future<List<T>> queryList<T>(
    final String sql, {
    final List<dynamic> arguments,
    @required final T Function(Map<String, dynamic>) mapper,
  }) async {
    final rows = await _database.rawQuery(sql, arguments);
    return rows.map((row) => mapper(row)).toList();
  }

At this point the rows still have the ids filled in properly, although the entities in the rows are just a list of dynamic values. The rows.map supposed to map them to the entity objects, and that cannot carry the id over for some reason? Can this be a Floor bug?


Ok, now I see that in the generated database.g.dart the mapper does not have the id:

  static final _activitiesMapper = (Map<String, dynamic> row) => Activity(
      deviceName: row['device_name'] as String,
      deviceId: row['device_id'] as String,
      start: row['start'] as int);

That explains it why id is null then. But this is generated code, how can I tell Floor I need the id? Or why should I tell it, it has to be there by default, who wouldn't want to know the primary key of an object?


Solution

  • Ok, so this is because I didn't have the the id as a constructor parameter. I didn't have that because it's an auto increment field and I'm not the one who determines it. However without having it as a constructor argument, the generated code cannot pass it along with the mapper as a parameter, so it leaves it out from the mapper. So I added the id as a constructor argument.

    Post null-safety version:

    class Activity {
      @PrimaryKey(autoGenerate: true)
      int? id;
      @ColumnInfo(name: 'device_name')
      final String deviceName;
      @ColumnInfo(name: 'device_id')
      final String deviceId;
      final int start;
      final int end;
    
      Activity({
        this.id,
        required this.deviceName,
        required this.deviceId,
        required this.start,
        this.end: 0,
      });
    }
    

    I also add this Floor GitHub issue here, it looks like in the future there might be an annotation: https://github.com/vitusortner/floor/issues/527


    Pre null-safety version:

    import 'package:meta/meta.dart';
    
    class Activity {
      @PrimaryKey(autoGenerate: true)
      int id;
      @ColumnInfo(name: 'device_name')
      @required
      final String deviceName;
      @ColumnInfo(name: 'device_id')
      @required
      final String deviceId;
      final int start;
    
      Activity({
        this.id: null,
        this.deviceName,
        this.deviceId,
        this.start,
      });
    }
    

    The compiler is a little iffy about the null, but after the flutter packages pub run build_runner build the database.g.dart's mappers have the id as well.