Skip to main content

ORM di Flutter dengan moor

·1164 words·6 mins
Dart Flutter ORM SQlite
oon arfiandwi
Author
oon arfiandwi
keep it simple, s!

Dengan pengalaman memrogram untuk web backend, untuk interaksi basis data sering memilih untuk menggunakan Object-Relational Mapping (ORM) karena beberapa alasan, antara lain:

  • Keamanan, untuk menghindari SQL Injection, ORM sudah dilengkapi dengan sejumlah fitur untuk membersihkan (sanitize) masukan ke sintaksisnya
  • Kemudahan, ya buat sejumlah orang yang belum banyak berinteraksi dengan SQL, memilih ORM karena menggunakan bahasa (pemrograman) yang sama
  • Tambahan fitur seperti cache, yang akan mempercepat proses jika diakses secara berulang

Beberapa waktu belakang ini banyak eksplorasi tentang Flutter, nggak hanya tentang Flutter web, tapi dukungan Flutter juga semakin baik untuk membuat aplikasi Linux. Sayangnya pustaka pendukung belum langsung bisa untuk sekaligus banyak platform.

Contohnya untuk berinteraksi dengan basis data SQLite, di Flutter untuk aplikasi ponsel direkomendasikan plugin sqflite 1, namun paket tersebut tidak/belum mendukung platform web dan Linux.

Sebagai alternatif, pilihannya adalah moor 2, yang juga merupakan plugin untuk berinteraksi dengan basis data SQLite. moor ini anagram (utak-atik huruf) dari room, yang di platform Android merupakan nama pustaka untuk akses basis data. Dan serunya lagi, moor tidak hanya mendukung sintaksis SQL, juga secara mendasar mendukung konsep ORM, dimana kita mendefinisikan struktur tabel serta interaksinya dengan bahasa pemrograman Dart. Buat yang memrogram Dart secara reaktif, pustakan ini juga punya dukungan untuk pemrograman reaktif, dengan menghasilkan keluaran dari query berupa stream yang terus-menerus diperbarui. moor mendukung untuk pembuatan aplikasi Flutter di banyak platform, termasuk juga di platform web dan Linux yang sedang dieksplorasi.

Di sisi lain, dalam menggunakan moor untuk menyimpan data ini tidak ada cara langsung yang mudah (straight forward), dimana kita perlu melakukan beberapa langkah terlebih dahulu. 🤔

Menambahkan paket moor di pubspec.yaml bukan hanya satu atau dua baris seperti kebanyakan paket, tapi 6 baris yang perlu ditambahkan 😂.

dependencies:
  moor: # use the latest version
  sqlite3_flutter_libs: # Also use the latest version.
  path_provider:
  path:

dev_dependencies:
  moor_generator: # use the latest version
  build_runner: 

Di daftar paket tersebut ada moor_generator, selanjutnya akan dibahas mengenai bagaimana moor perlu dilakukan build dahulu untuk membangkitkan kode Dart, misalnya definisi dari tabel kita simpan pada file database.dart selanjutnya menggunakan perintah flutter packages pub run build_runner build akan menghasilkan file database.g.dart, dimana g.dart di situ sudah menjadi kesepakatan untuk menggunakan ekstensi (akhiran nama file) tersebut untuk kode yang dibangkitkan (generated) dari kode dart lain.

Berikut adalah contoh definisi tabel dan basis data menggunakan moor yang disimpan pada file bernama database.dart.

import 'package:moor/moor.dart';

// assuming that your file is called database.dart. This will give an error at first,
// but it's needed for moor to know about the generated code
part 'database.g.dart';

// this will generate a table called "todos" for us. The rows of that table will
// be represented by a class called "Todo".
class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 6, max: 32)();
  TextColumn get content => text().named('body')();
  IntColumn get category => integer().nullable()();
}

// this annotation tells moor to prepare a database class that uses both of the
// tables we just defined. We'll see how to use that database class in a moment.
@UseMoor(tables: [Todos, ])
class MyDatabase {
  
}

Nantinya saat file tersebut dituliskan, akan dianggap error dahulu dia awal pada baris part 'database.g.dart';, karena belum dibangkitkan file database.g.dart.

Bangkitkan (generate) dengan perintah flutter packages pub run build_runner build atau kalo ingin terus menerus membangkitkan secara otomatis pada saat menuliskan struktur tabel dan basis data, gunakan perintah flutter packages pub run build_runner watch.

Setelah selesai membangkitkan file database.g.dart, kita sudah siap menggunakan basis data dengan moor, misalnya dengan menyiapkan kelengkapan class MyDatabase di atas, serta mekanisme untuk koneksi ke basis data. Contoh kodenya seperti berikut.

// These imports are only needed to open the database
import 'package:moor/ffi.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:moor/moor.dart';
import 'dart:io';

LazyDatabase _openConnection() {
  // the LazyDatabase util lets us find the right location for the file async.
  return LazyDatabase(() async {
    // put the database file, called db.sqlite here, into the documents folder
    // for your app.
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return VmDatabase(file);
  });
}

@UseMoor(tables: [Todos, ])
class MyDatabase extends _$MyDatabase {
  // we tell the database where to store the data with this constructor
  MyDatabase() : super(_openConnection());

  // you should bump this number whenever you change or add a table definition. 
  // Migrations are covered later in this readme.
  @override
  int get schemaVersion => 1;
}

Perhatikan bahwa class MyDatabase tersebut merupakan turunan dari class _$MyDatabase sehingga kita bisa mengakses sejumlah fitur dan tabel yang sudah kita definisikan dengan moor sebelumnya (sebelum dibangkitkan).

Untuk contoh menuliskan query basis data menggunakan moor seperti berikut.

// inside the database class, the `todos` getter has been created by moor.
@UseMoor(tables: [Todos, ])
class MyDatabase extends _$MyDatabase {  

  // the schemaVersion getter and the constructor from the previous page
  // have been omitted.
  
  // loads all todo entries
  Future<List<Todo>> get allTodoEntries => select(todos).get();

  // watches all todo entries. The stream will automatically
  // emit new items whenever the underlying data changes.
  Stream<List<Todo>> watchEntries() {
    return select(todos).watch();
  }
}

Pemanggilan method .watch() akan menghasilkan stream yang secara otomatis akan menginformasikan jika ada perubahan data, misalnya data baru.

Misalnya untuk melakukan penambahan data (insert), tambahkan method pada class MyDatabase dengan kode berikut.

// returns the generated id
Future<int> addTodo(TodosCompanion entry) {
  return into(todos).insert(entry);
}

Lalu pemanggilannya seperti berikut.

addTodo(
  TodosCompanion(
    title: Value('Important task'),
    content: Value('Refactor persistence code'),
  ),
);

Dimana TodosCompanion tersebut merupakan salah satu yang dihasilkan pada proses pembangkitan. Lebih lanjut tentang penulisan query di dokumentasi dari moor.

Untuk menggunakan moor hanya sebagai satu instance saja, atau disebut singleton, kita dapat mengikuti panduan yang sudah disediakan 3, dimana salah satu pilihannya adalah menggunakan InheritedWidgets atau menggunakan paket provider 4 terutama bagi yang sudah menggunakan provider sebagai state management.

Di bagian runApp().

void main() {
  runApp(
    Provider<MyDatabase>(
      create: (context) => MyDatabase(),
      child: MyFlutterApp(),
      dispose: (context, db) => db.close(),
   ),
  );
}

Di bagian yang membutuhkan (akses) instance dari basis data.

Provider.of<MyDatabase>(context)

Pada halaman paket moor2 disebutkan mengenai contoh pola untuk implementasi berbagai platforms, di aplikasi Todo bernama moor_shared5. Dimana salah satu yang menarik ada bagaimana menyetel TargetPlatform di moor_shared/lib/plugins/desktop/io.dart.

void setTargetPlatformForDesktop({TargetPlatform platform}) {
  TargetPlatform targetPlatform;
  if (platform != null) {
    targetPlatform = platform;
  }
  if (targetPlatform == null) {
    if (Platform.isMacOS) {
      targetPlatform = TargetPlatform.iOS;
    } else if (Platform.isLinux || Platform.isWindows) {
      targetPlatform = TargetPlatform.android;
    }
  }
  debugDefaultTargetPlatformOverride = targetPlatform;
}

Fungsi setTargetPlatformForDesktop() tersebut dipanggil sebelum runApp() di Flutter.

Pustaka moor ini salah satunya bergantung pada pustaka sqlite3 6 untuk mengakses SQLite melalui dart:ffi, sehingga untuk keperluan penggunaan di Linux, perlu instalasi paket tambahan, misalnya sebagai berikut.

sudo apt install libsqlite3-dev

Ini pembahasan di acara “Flutter Boring Show” tentang penggunaan moor untuk menyimpan data artikel favorit, di episode 25.

(kelanjutannya di episode 27)

Artikel ini ditulis dengan menggunakan moor versi 3.3.1, dimana proses development dari moor ini sangat cepat dan kadang membawa perubahan yang membuat kode sebelumnya tidak bisa lagi digunakan (breaking changes), jadi saat ingin mencoba, pastikan sudah menyesuaikan dengan panduan dari versi moor yang digunakan.

Foto oleh NASA di Unsplash