Skip to content

ActiveRecord-style ORM for Dart. PostgreSQL, MySQL, SQLite with auto-migrations, query builder, relations, and soft deletes.

License

Notifications You must be signed in to change notification settings

duxt-base/duxt_orm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DuxtOrm

ActiveRecord-style ORM for Dart with GORM-like auto-migrations.

Supports PostgreSQL, MySQL, and SQLite.

Installation

dependencies:
  duxt_orm: ^0.3.0

Quick Start

1. Define Your Model

import 'package:duxt_orm/duxt_orm.dart';

class User extends Entity {
  int? _id;
  String? email;
  String? name;
  bool isActive;

  User({int? id, this.email, this.name, this.isActive = true}) : _id = id;

  @override dynamic get id => _id;
  @override set id(dynamic value) => _id = value as int?;

  @override
  Map<String, dynamic> toMap() => {
    'id': id,
    'email': email,
    'name': name,
    'is_active': isActive ? 1 : 0,
  };

  factory User.fromRow(Map<String, dynamic> row) => User(
    id: row['id'] as int?,
    email: row['email'] as String?,
    name: row['name'] as String?,
    isActive: (row['is_active'] as int?) == 1,
  );

  static void register() {
    Entity.registerModel<User>(
      User.fromRow,
      schema: {
        'id': Column.integer().primaryKey().autoIncrement(),
        'email': Column.string(255).unique().notNull(),
        'name': Column.string(100).notNull(),
        'is_active': Column.boolean().defaultValue(true),
        'created_at': Column.dateTime().nullable(),
        'updated_at': Column.dateTime().nullable(),
      },
    );
  }
}

2. Initialize and Migrate

// Register models
User.register();
Post.register();

// Initialize with config
await DuxtOrm.init(DuxtConfig.database);

// Auto-create tables
await DuxtOrm.migrate();

3. Use It

final users = Model<User>();

// Create
final user = User(email: 'test@example.com', name: 'Test');
await user.save();

// Query all
final all = await users.all();

// Find by ID
final found = await users.find(1);

// Query with conditions
final admins = await users.where('role', 'admin').get();

// Query builder
final results = await users.query()
    .where('is_active', 1)
    .orderByDesc('created_at')
    .limit(10)
    .get();

// Update
found?.name = 'Updated Name';
await found?.save();

// Delete (soft delete by default)
await found?.destroy();

Configuration

Configure in your duxt.config.dart:

class DuxtConfig {
  static const database = (
    driver: 'postgres',  // 'postgres', 'mysql', 'sqlite'
    host: String.fromEnvironment('DB_HOST', defaultValue: 'localhost'),
    port: int.fromEnvironment('DB_PORT', defaultValue: 5432),
    database: String.fromEnvironment('DB_NAME', defaultValue: 'myapp'),
    username: String.fromEnvironment('DB_USER', defaultValue: ''),
    password: String.fromEnvironment('DB_PASS', defaultValue: ''),
    path: 'data/app.db',  // SQLite only
    ssl: false,
  );
}

Soft Deletes

Soft deletes are enabled by default. destroy() sets deleted_at instead of removing the row.

final posts = Model<Post>();
final post = await posts.find(1);

// Soft delete
await post!.destroy();
print(post.trashed); // true

// Restore
await post.restore();

// Permanently delete
await post.forceDelete();

// Query scopes
final all = await posts.withTrashed().get();       // Include trashed
final trashed = await posts.onlyTrashed().get();    // Only trashed

Opt out per model:

Entity.registerModel<LogEntry>(LogEntry.fromRow, softDeletes: false, schema: { ... });

Lifecycle Hooks

Override hooks to run logic around persistence:

class Post extends Entity {
  @override
  Future<void> beforeSave() async {
    slug ??= title?.toLowerCase().replaceAll(' ', '-');
  }

  @override
  Future<void> afterDelete() async {
    await FileStorage.delete('posts/$id');
  }
}

Available hooks: beforeSave(), afterSave(), beforeDelete(), afterDelete().

Relations

Define Relations

class Post extends Entity {
  Category? get category => getRelation<Category>('category');
  List<Comment> get comments => getRelation<List<Comment>>('comments') ?? [];

  static void register() {
    Entity.registerModel<Post>(Post.fromRow, schema: { ... });
    Entity.registerRelation<Post>('category', BelongsTo<Category>(foreignKey: 'category_id'));
    Entity.registerRelation<Post>('comments', HasMany<Comment>(foreignKey: 'post_id'));
  }
}

Eager Loading

// Load relations (no N+1 queries)
final posts = await Model<Post>().include(['category', 'comments']).get();
print(posts.first.category?.name);

// Nested dot notation
final posts = await Model<Post>().include(['author.profile', 'comments.user']).get();
print(posts.first.author?.profile?.bio);

Relation Types

  • HasMany<T>(foreignKey: ...) — One-to-many
  • HasOne<T>(foreignKey: ...) — One-to-one
  • BelongsTo<T>(foreignKey: ...) — Inverse of has-many/has-one
  • BelongsToMany<T>(pivotTable: ..., foreignPivotKey: ..., relatedPivotKey: ...) — Many-to-many

Many-to-Many Pivot Methods

await post.attach('tags', tagId);
await post.detach('tags', tagId);
await post.sync('tags', [1, 2, 3]);

Table Name Convention

Table names are auto-inferred from class names:

Class Name Table Name
User users
UserProfile user_profiles
Category categories

Override with tableName in registerModel().

Schema Definition

Column Types

Column.integer()        // INTEGER
Column.bigInteger()     // BIGINT
Column.string(255)      // VARCHAR(255)
Column.text()           // TEXT
Column.boolean()        // BOOLEAN
Column.dateTime()       // TIMESTAMP
Column.date()           // DATE
Column.time()           // TIME
Column.decimal(10, 2)   // DECIMAL(10, 2)
Column.float()          // FLOAT
Column.json()           // JSON
Column.binary()         // BLOB

Column Modifiers

Column.integer().primaryKey()           // PRIMARY KEY
Column.integer().autoIncrement()        // AUTO_INCREMENT
Column.string(255).notNull()            // NOT NULL
Column.string(255).nullable()           // NULL (default)
Column.string(255).unique()             // UNIQUE
Column.boolean().defaultValue(true)     // DEFAULT true
Column.integer().references('users')    // FOREIGN KEY

Query Builder

Where Clauses

final users = Model<User>();

users.where('is_active', 1)
users.where('age', 18, '>=')

users.query()
    .where('is_active', 1)
    .orWhere('role', 'admin')
    .whereNull('deleted_at')
    .whereIn('id', [1, 2, 3])
    .whereBetween('age', 18, 65)
    .whereLike('name', '%john%')
    .get();

Ordering & Pagination

users.query().orderByDesc('created_at').skip(20).take(10).get();
users.query().latest().get();  // created_at DESC

Aggregates

await users.where('is_active', 1).count();
await users.query().sum('total');
await users.query().avg('age');
await users.query().max('price');
await users.query().min('price');

Bulk Operations

await users.query().where('is_active', 0).delete();
await users.query().where('id', 1).increment('views');

Transactions

await DuxtOrm.transaction((tx) async {
  await tx.query('INSERT INTO users (email, name) VALUES (?, ?)',
    ['test@example.com', 'Test']);
  await tx.query('INSERT INTO profiles (user_id, bio) VALUES (?, ?)',
    [1, 'Hello!']);
});

Raw Queries

final result = await DuxtOrm.raw(
  'SELECT * FROM users WHERE email = ?',
  ['test@example.com'],
);

Documentation

Full docs at duxt.dev/duxt-orm.

License

MIT

About

ActiveRecord-style ORM for Dart. PostgreSQL, MySQL, SQLite with auto-migrations, query builder, relations, and soft deletes.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published