An annotation-based ORM for Flutter inspired by the Room persistence library. This library is based on sqflite and wrapped it for better APIs. Supports Android, iOS and MacOS.
- Simple APIs for create DB, Entities
- Simple APIs for CRUD operations
- Supports transactions
- Supports custom type converters
- Supports migrations
- Supports embedded fields
- Supports foreign keys
Usage example:
- notes: Simple flutter notes project working on Android/iOS
The library is in Beta and isn't completely stable.
In your flutter project add the dependency:
dependencies:
...
flutter_orm:
sqflite:
dev_dependencies:
...
flutter_orm_generator:
build_runner:
For help getting started with Flutter, view the online documentation.
import 'package:flutter_orm/flutter_orm.dart';
Note that, if you make any change in structure of database(include DB
, Dao
, Entity
and etc), you have to run below command for apply changes.
flutter pub run build_runner build --delete-conflicting-outputs;
For create an entity or table, you can use @Entity
annotation on a class.
You can set tableName
and indices
for this table, in this annotation.
If you don't set tableName
, the class name will be set as the default name.
You have to set primary key for table by using @PrimaryKey
annotation.
The entity must have exactly one primary key and only integer primary key can be auto generated.
Auto generated primary key must be nullable.
All properties of the class that this annotation applied on, map to a column in the table unless properties that @Ignore
annotation are applied on. Default column name is property name. If you want to change it use @Column
annotation and set name
property.
Also you can set default value for columns using defaultValue
property of @Column
annotation.
@Entity(tableName: 'notes', indices: [
Index(columns: ['text'], unique: true)
])
class Note {
@PrimaryKey(autoGenerate: true)
final int? id;
final String text;
final bool isEdited;
final DateTime createDate;
final DateTime? updateDate;
@Column(name: 'lat')
final double? latitude;
@Column(name: 'lng')
final double? longitude;
@Ignore()
final String? ignoreTest;
@Column(name: 'defaultValueTest1', defaultValue: 'test')
final String? defaultValueTest1;
@Column(name: 'defaultValueTest2', defaultValue: '0')
final int? defaultValueTest2;
Note({
this.id,
required this.text,
required this.isEdited,
required this.createDate,
this.updateDate,
this.latitude,
this.longitude,
this.ignoreTest,
this.defaultValueTest1,
this.defaultValueTest2,
});
}
Foreign keys allows you to specify constraints across entities such that SQLite will ensure that the relationship is valid when you modify the database.
For define foreign key in entities, you have to set foreignKeys
property of @Entity
annotation and pass a list of ForeignKey
instances to it.
A foreign key constraint can be deferred until the transaction is complete. This is useful if you are doing bulk inserts into the database
in a single transaction. By default, foreign key constraints are immediate but you can change this value by setting deferred
to true
.
@Entity()
class User {
@PrimaryKey(autoGenerate: true)
final int? id;
final String name;
User({
this.id,
required this.name,
});
}
@Entity(
foreignKeys: [
ForeignKey(
entity: User,
parentColumns: ['id'],
childColumns: ['userId'],
onDelete: ForeignKeyAction.CASCADE,
onUpdate: ForeignKeyAction.CASCADE,
deferred: false,
)
],
)
class Note {
@PrimaryKey(autoGenerate: true)
final int? id;
final String text;
final int userId;
Note({
this.id,
required this.text,
required this.userId,
});
}
ForeignKey
class has these properties:
entity
: The parent Entity to reference. It must be a class annotated with@Entity
and referenced in the same database.parentColumns
: The list of column names in the parent Entity. Number of columns must match the number of columns specified inchildColumns
.childColumns
: The list of column names in the current Entity. Number of columns must match the number of columns specified inparentColumns
.onDelete
: Action to take when the parent Entity is deleted from the database. Default value ofonDelete
isForeignKeyAction.NO_ACTION
.onUpdate
: Action to take when the parent Entity is updated in the database. Default value ofonUpdate
isForeignKeyAction.NO_ACTION
.deferred
: A foreign key constraint can be deferred until the transaction is complete. This is useful if you are doing bulk inserts into the database in a single transaction. By default, foreign key constraints are immediate but you can change it by setting this field totrue
.
By default, SQLite does not enforce foreign keys. This is usually necessary explicitly turning it on.
@DB(
name: 'note_db',
version: 1,
entities: [Note, User],
)
abstract class NoteDB {
@OnConfigure()
Future<void> onConfigure(Database db) async {
db.execute('PRAGMA foreign_keys = ON;');
}
}
If you want to use an object that you have defined yourself, in your entity, you can use @Embedded
annotation.
Suppose that the Note
entity has an address
property that is an object itself.
In this situation, you can use @Embedded
annotation on that field.
Embedded objects also can have embedded fields.
Embedded object fields, can have @Column
and @Ignore
annotation, but they can't have @PrimaryKey
.
You should note that the fields of embedded object, merge with entity fields in table.
For example, for the below entity, created table has these columns: id
, text
, address_lat
and address_lng
.
If you have noticed, embedded object fields has a prefix in its column name that is field name by default.
You can change this prefix by change prefix
property of @Embedded
annotation.
@Entity()
class Note {
@PrimaryKey(autoGenerate: true)
final int? id;
final String text;
@Embedded()
final Address? address;
Note({
this.id,
required this.text,
required this.isEdited,
this.address,
});
}
class Address {
@Column(name: 'lat')
final double latitude;
@Column(name: 'lng')
final double latitude;
Address({
required this.latitude,
required this.latitude,
});
}
For create a database, you can use @DB
annotation on an abstract class.
Database entities(tables) must be defined in this annotation.
Configuration methods like OnConfigure
, OnOpen
, OnUpgrade
and OnDowngrade
for actions like Migration can define in this class.
@DB(
name: 'note_db',
version: 1,
entities: [Note],
)
abstract class NoteDB {
@OnUpgrade()
Future<void> onUpgrade(Database db, int oldVersion, int newVersion) async {
print('onUpgrade');
}
@OnDowngrade()
Future<void> onDowngrade(Database db, int oldVersion, int newVersion) async {
print('onDowngrade');
}
@OnConfigure()
Future<void> onConfigure(Database db) async {
print('onConfigure');
}
@OnOpen()
Future<void> onOpen(Database db) async {
print('onOpen');
}
}
For initialize databases and generate methods for access to databases, you have to define an abstract class with @DBBuilder
annotation and pass databases class to databases
paramater of @DBBuilder
annotation.
You don't need to do anything extra.
@DBBuilder(databases: [NoteDB])
abstract class DatabaseBuilder {}
After define this class, you can create and access to database like this:
NoteDB db = await DBContext.getNoteDB();
Both DB
and DBBuilder
class, must be defined in separated files.
Also generated files for these classes, must be bind to original file using part
keyword.
part 'filename.dbbuilder.dart'
@DBBuilder(databases: [NoteDB])
abstract class DatabaseBuilder {}
part 'filename.db.dart'
@DB(
name: 'note_db',
version: 1,
entities: [Note],
)
abstract class NoteDB {}
For Create Dao
(Data Access Object) you can use @Dao
annotation. @Dao
annotation must apply on an abstract class.
In this class you can define methods for access and manipulate data in tables.
@Dao()
abstract class NoteDao {
// methods for access and manipulate data
}
After define this class, you have to define an abstract method in your db class for access to this dao:
@DB(
name: 'note_db',
version: 1,
entities: [Note],
)
abstract class NoteDB {
NoteDao noteDao();
}
For access to dao, do like this:
NoteDB noteDB = await DBContext.getNoteDB();
final dao = noteDB.noteDao();
int count = await dao.count() ?? 0;
For insert data in your tables, you can use @Insert
annotation.
Methods that annotated with this annotation can have only one input parameter that is an Entity
or List of an Entity
and cannot be nullable.
If the input parameter is Entity
, the return type can be Future<void>
or Future<int?>
, but if the input parameter is List of Entity
, the return type only can be Future<void>
.
You can set OnConflictStrategy
for when conflict occurs.
@Dao()
abstract class NoteDao {
@Insert(onConflict: OnConflictStrategy.ignore)
Future<void> insert(Note note);
@Insert(onConflict: OnConflictStrategy.ignore)
Future<void> bulkInsert(List<Note> notes);
}
For update data in your tables, you can use @Update
annotation.
Methods that annotated with this annotation can have only one input parameter that is an Entity
or List of an Entity
and cannot be nullable.
If the input parameter is Entity
, the return type can be Future<void>
or Future<int?>
, but if the input parameter is List of Entity
, the return type only can be Future<void>
.
You can set OnConflictStrategy
for when conflict occurs.
@Dao()
abstract class NoteDao {
@Update(onConflict: OnConflictStrategy.replace)
Future<void> update(Note note);
@Update(onConflict: OnConflictStrategy.replace)
Future<void> bulkUpdate(List<Note> notes);
}
For delete data in your tables, you can use @Delete
annotation.
Methods that annotated with this annotation can have only one input parameter that is an Entity
and cannot be nullable.
The return type can be Future<void>
or Future<int?>
.
@Dao()
abstract class NoteDao {
@Delete()
Future<void> delete(Note note);
}
For define raw query to access or manipulate your data, you can use @Query
annotation.
You have to pass your raw query as a String to query
property of this annotation. Also, you have to detect the return type, yourself.
You can fetch raw results as List<Map<String, Object?>>?
by using RawData
class as the return type.
Also, you can fetch results as a custom class. In this case you must pass that custom class to the @SingularReturnType
annotation.
For better query, you can have any number of input parameters and you can pass these parameters to query using the colon(:)
symbol.
@Dao()
abstract class NoteDao {
@Query("select * from Note")
Future<List<Note>> all();
@Query("delete from Note")
Future<void> deleteAll();
@Query("select * from Note where id= :id")
Future<Note?> getById(int id);
@Query("select count(*) from Note")
Future<int?> count();
@Query("select * from Note where isEdited= :isEdited")
Future<List<Note>> getNotes(bool isEdited);
@Query("select * from Note where id IN (:ids)")
Future<List<Note>> getNotesByIds(List<int> ids);
@Query("select * from Note where lat IN (:lats)")
Future<List<Note>> getNotesByLatitudes(List<double> lats);
@Query("select * from Note where text LIKE '%:search%'")
Future<RawData> search(String search);
@Query("select lat from Note")
Future<List<double>> getLatitudes();
@Query("select id, text, lat, createDate from Note")
@SingularReturnType(CustomNote)
Future<List<CustomNote>> getCustomNotes();
@Query("select id, text, lat, createDate from Note where id= :id")
@SingularReturnType(CustomNote)
Future<CustomNote?> getCustomNoteById(int id);
}
Transactions can define using @Transactional
annotation.
For define transaction you have to use other dao methods name as sequentialActions
and pass required parameters by @InsertParam
, @UpdateParam
, @DeleteParam
and @QueryParam
annotations. For example if the first method of the sequentialActions
property, is an insert
method, you have to define an input parameter with @InsertParam
annotation and correct type based on original method.
@QueryParam
has two arguments. The first is method name and the second is parameter name in original method.
The return type of these methods must be Future<void>
.
@Dao()
abstract class NoteDao {
@Insert(onConflict: OnConflictStrategy.ignore)
Future<void> insert(Note note);
@Query("delete from Note where id = :id")
Future<void> deleteById(int id);
@Transactional(sequentialActions: ['insert', 'deleteById'])
Future<void> insertAndDeleteById(
@InsertParam() Note newNote,
@QueryParam('deleteById', 'id') int noteId,
);
}
If there are types that doesn't support internally, you can define these types yourself, using @TypeConverter
annotation.
You can define a class and write methods for convert these types to a supported type. these methods must annotated with @TypeConverter
annotation.
class Converters {
@TypeConverter()
static DateTime to(String value) {
return DateTime.parse(value);
}
@TypeConverter()
static String from(DateTime dateTime) {
return dateTime.toIso8601String();
}
}
Note that you have to write both methods(from
and to
). After define these methods, you can use DateTime in your entities as a field.
Note that DateTime
class, is support internally and you don't need define TypeConverter
for that.
After that, you have to pass this class to your database class in @TypeConverters
annotation.
@DB(
name: 'note_db',
version: 1,
entities: [Note],
)
@TypeConverters(Converters)
abstract class NoteDB {
// DAOs
}
- int
- num
- String
- Uint8List
- bool
- double
- DateTime