Elegant Help

Elegant ORM: Getting Started

Introduction

The Elegant ORM (Object-Relational Mapper) transforms database tables into intuitive TypeScript classes, letting you work with database records as objects rather than writing raw SQL. This object-oriented approach makes your code more maintainable, readable, and enjoyable to write.

With Elegant ORM, each database table is represented by a Model class. These models provide methods to query, insert, update, and delete records while maintaining type safety and providing a clean, expressive API.

Why Use an ORM?

Type Safety - Define your data structures once, and TypeScript ensures you're always working with the correct types.

Productivity - Write less code. Instead of crafting SQL for every operation, use intuitive methods that handle the heavy lifting.

Maintainability - Business logic stays in your application code, not scattered across SQL strings throughout your codebase.

Flexibility - When you need raw SQL power, it's always available. Elegant ORM complements rather than replaces SQL.

Generating Model Classes

To get started, let's create an Elegant model. Models typically live in the resources/database/models directory and extend the Model class. You may use the make:model Elegant command to generate a new model:

elegant make:model User

Generate corresponding type definitions for models based on database schema

elegant make:types

Inspecting Model Structure

When working with complex models, understanding their complete structure—including all properties and connections—can be challenging through code inspection alone. The model:show command offers a quick solution by displaying a comprehensive summary of a model's attributes and relationships in an easy-to-read format:

elegant model:show

Elegant Model Conventions

Models generated by the elegant make:model command will be placed in the resources/database/models directory. Let's examine a basic model class and discuss some of Elegant's key conventions:

import { Model } from '@pristine/elegant' class User extends Model {}

Elegant follows sensible conventions to minimize configuration:

  • Table Names - By default, the model class name is converted to snake_case and pluralized (e.g., UserProfileuser_profiles)

  • Primary Keys - Assumes an auto-incrementing column named id

  • Timestamps - Automatically manages created_at and updated_at columns

Table Names

By default, Elegant automatically determines which database table to use for each model through a naming convention. The framework converts the model's class name into lowercase "snake_case" and pluralizes it. For example, a User model maps to the users table, and a SocialMediaLinks model maps to the social_media_links table. When your database table doesn't follow this pattern, you can override the automatic mapping by defining a table property directly on your model:

import { Model } from '@pristine/elegant' class User extends Model { protected $table = 'super_users' }

Primary Keys

By default, Elegant expects every database table to include a primary key column called id. To use a different column as your primary key, define a protected primaryKey property on your model:

import { Model } from '@pristine/elegant' class User extends Model { protected $primaryKey = 'user_id' }

Timestamps

By default, Elegant looks for created_at and updated_at columns in your model's database table. The framework automatically populates these columns whenever you create or modify a model. To disable this automatic timestamp management, set the timestamps property on your model to false:

import { Model } from '@pristine/elegant' class User extends Model { protected $timestamps = false }

If you need to customize the names of the columns used to store the timestamps, you may define created_at and updated_at protected properties on your model:

import { Model } from '@pristine/elegant' class User extends Model { protected $created_at = 'creation_date' protected $updated_at = 'last_updated' }

Database Connections

All Elegant models use your application's default database connection unless configured otherwise. To specify which connection a model should use, define a connection property on that model:

import { Model } from '@pristine/elegant' class User extends Model { protected $connection = 'postgres' }

Default Attribute Values

When you create a new model instance, its attributes start empty by default. To set initial values for specific attributes, define an attributes property on your model. Values in the attributes array must be in their raw database format—the same format used when retrieving data directly from the database:

import { Model } from '@pristine/elegant' class User extends Model { protected $attributes = { phone: '555-555-5555', isActive: true } }

Configuring Strictness

Elegant provides configuration options to control its behavior and strictness across different scenarios. The lazyLoading option determines whether lazy loading is enabled. This is particularly useful for development environments—you can disable lazy loading during development to catch relationship loading issues early, while keeping it enabled in production to maintain normal operation even if lazy-loaded relationships slip through. Configure this setting in your elegant.config.js file.

The strictAttributes option makes Elegant throw an exception when you attempt to populate a non-fillable attribute. Enabling this during local development helps catch errors early when you try to set attributes that haven't been added to the model's fillable array:

export default { default: 'mysql', connections: {/** connection configurations **/}, models: { lazyLoading: false, strictAttributes: true, directory: 'resources/database/models' } }

Retrieving Models

After creating a model and its corresponding database table, you can begin querying data. Each Elegant model functions as an advanced query builder, providing a fluent interface for interacting with its associated table. The all method fetches every record from the model's table:

// user.model.ts import { Model } from '@pristine/elegant' class UserModel extends Model {} export default new UserModel()
// user.service.ts import User from './user.model' export const getUsers = () => User.all()

Elegant Models as Query Builders

Elegant models function as comprehensive query builders, giving you access to a full suite of database query methods directly from your model classes. This design means you can chain query methods fluently to construct complex database operations without dropping down to raw SQL.

Available Query Methods

Since Elegant models inherit query builder functionality, you can leverage numerous methods for filtering, sorting, aggregating, and manipulating data. These methods work seamlessly with your model instances, allowing you to build sophisticated queries using an expressive, readable syntax.

Common Query Operations Include:

  • Filtering: Apply conditions to narrow down results based on column values

  • Sorting: Order results by one or more columns in ascending or descending order

  • Limiting: Restrict the number of records returned

  • Aggregation: Calculate sums, averages, counts, and other statistical values

  • Joining: Combine data from multiple related tables

  • Grouping: Organize results by shared attribute values

Example Usage:

// Retrieve active users ordered by creation date const users = await User.where('status', 'active') .orderBy('created_at', 'desc') .limit(10) .get(); // Count users by role const adminCount = await User.where('role', 'admin').count(); // Complex query with multiple conditions const results = await User.where('age', '>', 18) .where('verified', true) .orWhere('premium', true) .get();

All query builder methods available in Elegant can be chained together and executed on your model classes, providing a powerful and intuitive interface for database interactions. For a complete reference of available methods, consult the Query Builder documentation section.

Refreshing Models

When you have an existing Elegant model instance loaded from the database, you can update it with the latest data using the fresh and refresh methods. The fresh method fetches a new copy of the model from the database without modifying the current instance:

User.where('name','jack').first() User.refresh()

The refresh method updates the current model instance with fresh data from the database. Additionally, any loaded relationships are also refreshed:

User.where('name','jack').first() User.name = 'john' User.refresh() User.name // "john"

Finding by Primary Key

// Find user with ID 1 const user = await User.find(1); if (user) { console.log(user.email); }
// Find or throw an error if not found const user = await User.findOrFail(1);

Adding Constraints

// Find all active users const activeUsers = await User.where('active', true).get();
// Find users with complex conditions const vipUsers = await User .where('subscription_type', 'premium') .where('status', 'active') .orderBy('created_at', 'desc') .limit(10) .get();

Creating and Updating Models

Creating New Records

There are several ways to create new model instances:

Method 1: Create and save

const user = new User(); user.name = 'Jane Doe'; user.email = 'jane@example.com'; await user.save();

Method 2: Create with attributes

const user = await User.create({ name: 'John Smith', email: 'john@example.com' });

Method 3: Mass assignment

const users = await User.createMany([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]);

Updating Records

Update a single model

const user = await User.find(1); user.name = 'Updated Name'; await user.save();

Update using query

await User .where('status', 'inactive') .update({ status: 'archived' });

Update or create

const user = await User.updateOrCreate( { email: 'user@example.com' }, // Search criteria { name: 'User Name', active: true } // Values to update/create );

Deleting Models

Delete Single Records

Delete after retrieving

const user = await User.find(1); await user.delete();

Delete by primary key

await User.destroy(1);

Delete multiple by IDs

await User.destroy([1, 2, 3]);

Delete with Constraints

Delete all inactive

users await User.where('active', false).delete();

Delete with conditions

await User .where('created_at', '<', oldDate) .where('verified', false) .delete();

Relationships

Define relationships between models to work with related data:

export class User extends Model { // One-to-many relationship posts() { return this.hasMany(Post, 'user_id'); } // One-to-one relationship profile() { return this.hasOne(Profile, 'user_id'); } // Many-to-many relationship roles() { return this.belongsToMany(Role, 'user_roles'); } } // Access relationships const user = await User.find(1); const posts = await user.posts().get(); const profile = await user.profile().first();

Working with Timestamps

Elegant automatically manages created_at and updated_at timestamps:

const user = await User.create({ name: 'Jane Doe', email: 'jane@example.com' }); console.log(user.created_at); // Current timestamp console.log(user.updated_at); // Current timestamp // Update the model user.name = 'Jane Smith'; await user.save(); console.log(user.updated_at); // Updated to current timestamp

Disable timestamps if your table doesn't use them:

export class Log extends Model { protected $table = 'logs'; public timestamps = false; }

Advanced Features

Using Different Conections

Specify which database connection a model should use:

export class AnalyticsEvent extends Model { protected connection = 'analytics'; protected $table = 'events'; }

Custom Queries

Drop down to raw SQL when needed:

const users = await User.raw( 'SELECT * FROM users WHERE created_at > ?', [lastWeek] );

Eager Loading

Optimize queries by loading relationships upfront:

// N+1 problem - makes many queries const users = await User.all(); for (const user of users) { const posts = await user.posts().get(); // Query per user } // Eager loading - single query const users = await User.with('posts').get(); for (const user of users) { console.log(user.posts); // Already loaded }

Best Practices

  1. Use Type Definitions - Define all model properties with TypeScript types for better IDE support

  2. Eager Load Relationships - Avoid N+1 query problems by using eager loading

  3. Validate Data - Implement validation in your models before saving

  4. Use Transactions - Wrap related operations in transactions to maintain data integrity

Next Steps

24 October 2025