Lucid SQL ORM Documentation

repository·22.x·Indexed 22 days ago

https://github.com/adonisjs/lucid

A robust SQL ORM for the AdonisJS framework built on the Active Record pattern. It provides comprehensive database management tools, including query building, migrations, data seeding, and model factories. Includes detailed CLI references for commands such as db:create, db:drop, db:seed, migration:run, and make:model.

Tokens
52.4K
Snippets
109
Records
295
Agent score
73%

What's inside @adonisjs/lucid

  1. What is AdonisJS Lucid?

    22.x

    Lucid is a SQL ORM for AdonisJS built on top of Knex.js. It provides a comprehensive suite of tools for database interaction, including:

    • Database Query Builder: A fluent API for building SQL queries.
    • Active Record ORM: Full-featured object-relational mapping.
    • Schema Builder: Tools for database schema management.
    • Migrations: Version control for your database schemas.
    • Seeders: Tools to populate the database with test or initial data.
    • Model Factories: Utilities to generate fake data for testing.
    • Multiple Database Support: Supports MySQL, PostgreSQL, SQLite, MSSQL, Oracle, and LibSQL.
  2. How the Active Record pattern works in Lucid

    22.x

    Lucid uses the Active Record pattern, where models inherit from BaseModel and encapsulate both data and behavior. You define the model structure using TypeScript decorators like @column to map class properties to database columns.

    class User extends BaseModel {
      @column({ isPrimary: true })
      declare id: number
    
      @column()
      declare email: string
    }
  3. Customize schema generation with custom rules

    22.x

    You can override default type mappings and decorators by creating rules files. Rules can be defined at three levels:

    1. Table-specific column rules: Highest priority. Targets a specific column in a specific table.
    2. Table-specific type rules: Targets all columns of a specific database type within a specific table.
    3. Global column rules: Targets a specific column name across all tables.
    4. Global type rules: Targets all columns of a specific database type across all tables.
    5. Default built-in mapping: The fallback behavior.

    Rules are merged using a deep merge strategy; if you provide multiple files via rulesPaths, later files override earlier ones.

    // config/schema_rules.ts
    export default {
      // Global column name rules
      columns: {
        status: {
          tsType: 'UserStatus',
          decorator: '@column()',
          imports: [{ source: '#types/enums', namedImports: ['UserStatus'] }]
        }
      },
    
      // Global type rules
      types: {
        uuid: {
          tsType: 'string',
          decorator: '@column()',
          imports: []
        }
      },
    
      // Table-specific rules
      tables: {
        users: {
          columns: {
            email: {
              tsType: 'string',
              decorator: '@column({ isPrimary: true })',
              imports: []
            }
          }
        }
      }
    }
  4. Handle Factory Relationships

    22.x

    You can define relationships in factories using .relation(name, callback). Once defined, you can use the .with() method to instantiate related models during creation.

    • Has One / Has Many: Use .with('relationName') or .with('relationName', count).
    • Belongs To: Use .with('relationName') to create the parent, or manually merge the foreign key.
    • Many to Many: Use .with('relationName', count) and use the callback to provide pivotAttributes.
    • Nested Relationships: You can chain .with() calls to create deep trees of data.
    // Has Many example
    export const UserFactory = Factory.define(User, ({ faker }) => {
      return { email: faker.internet.email() }
    })
    .relation('posts', () => PostFactory)
    .build()
    
    // Create user with 5 posts
    const user = await UserFactory.with('posts', 5).create()
    
    // Nested: User with posts, and each post has comments
    const user = await UserFactory
      .with('posts', 2, (post) => {
        post.with('comments', 5)
      })
      .create()
    
    // Many to Many with pivot data
    const user = await UserFactory
      .with('roles', 2, (role) => {
        role.pivotAttributes({ expiresAt: DateTime.now().plus({ days: 30 }) })
      })
      .create()
  5. Use States to define factory variations

    22.x

    States allow you to define reusable variations of a factory. You can define a state using .state(name, callback), where the callback receives the model instance and allows you to modify its attributes. Use .apply(name) to activate a state when creating an instance.

    export const UserFactory = Factory.define(User, ({ faker }) => {
      return {
        email: faker.internet.email(),
        username: faker.internet.userName(),
        password: 'secret',
        isActive: true,
        role: 'user'
      }
    })
    .state('admin', (user) => {
      user.role = 'admin'
    })
    .state('inactive', (user) => {
      user.isActive = false
    })
    .build()
    
    // Use states
    const admin = await UserFactory.apply('admin').create()
    const verifiedAdmin = await UserFactory
      .apply('admin', 'inactive')
      .create()
  6. How the Query Builder pattern works in Lucid

    22.x

    Lucid provides a fluent, chainable API for building SQL queries directly via the Database object. This allows for expressive query construction without writing raw SQL.

    Database
      .from('users')
      .where('status', 'active')
      .orderBy('created_at', 'desc')
  7. Implement Soft Deletes in Models

    22.x

    To implement soft deletes, add a timestamp column (e.g., deleted_at) to your model and use the boot method to filter out deleted records by default using a before('find', ...) hook.

    To interact with soft-deleted records:

    • Use .withTrashed() to include deleted records in a query.
    • Use .onlyTrashed() to retrieve only deleted records.
    class User extends BaseModel {
      @column.dateTime()
      declare deletedAt: DateTime | null
    
      static boot() {
        super.boot()
    
        this.before('find', (query) => {
          query.whereNull('deleted_at')
        })
      }
    }
    
    // Soft delete
    user.deletedAt = DateTime.now()
    await user.save()
    
    // Include deleted
    User.query().withTrashed()
    User.query().onlyTrashed()
  8. Use SimplePaginator for lightweight pagination

    22.x

    SimplePaginator provides a lightweight pagination mechanism that does not calculate the total count of records, making it more performant for large datasets where you only need to know if a next page exists.

    Properties

    • all(): Returns all rows in the current page.
    • perPage: Number of items per page.
    • currentPage: Current page number.
    • firstPage: Always 1.
    • hasPages: Boolean indicating if there is more than one page.
    • hasMorePages: Boolean indicating if there is a next page.
    • isEmpty: Boolean indicating if there are no results.

    Methods

    • getUrl(page: number): Returns the URL for a specific page.
    • getUrls(): Returns an object containing URLs for first, last, and optionally next and previous pages.
    • getQueryString(page: number): Returns the query string for a specific page.
    • toJSON(): Converts the paginator to a JSON object containing meta and data keys.
  9. Perform data migrations with defer()

    22.x

    When you need to transform or migrate data as part of a schema change (e.g., moving data from an old column to a new one), use this.defer(). This ensures the data operations run after the schema changes are applied.

    Example: Adding a status column and populating it based on an existing is_active boolean column, then dropping the old column.

    export default class extends BaseSchema {
      protected tableName = 'users'
    
      async up() {
        // Add new column
        this.schema.alterTable(this.tableName, (table) => {
          table.string('status').defaultTo('active')
        })
    
        // Migrate data
        this.defer(async (db) => {
          const users = await db.from('users').select('*')
    
          for (const user of users) {
            await db.from('users')
              .where('id', user.id)
              .update({
                status: user.is_active ? 'active' : 'inactive'
              })
          }
        })
    
        // Drop old column
        this.schema.alterTable(this.tableName, (table) => {
          table.dropColumn('is_active')
        })
      }
    
      async down() {
        // ... reverse logic ...
      }
    }
  10. Best Practice: Use Read/Write mode for replicas

    22.x

    When working with database replicas, you can specify the mode to direct queries to either the primary (write) or replica (read) instance.

    // Read from replica
    const users = await Database.connection('primary', { mode: 'read' })
      .from('users')
      .select('*')
    
    // Write to primary
    await Database.connection('primary', { mode: 'write' })
      .table('users')
      .insert({ email: 'user@example.com' })
  11. Create a new table migration

    22.x

    Use the make:migration command to generate a new migration file. You can specify a connection or target a specific table using flags.

    • To create a new table: node ace make:migration create_users_table
    • To specify a connection: node ace make:migration create_users_table --connection=mysql
    • To create a migration that alters an existing table: node ace make:migration add_phone_to_users --table=users
    node ace make:migration create_users_table
    node ace make:migration create_users_table --connection=mysql
    node ace make:migration add_phone_to_users --table=users