Lucid ORM for AdonisJS

website·Indexed 38 days ago

https://lucid.adonisjs.com

The SQL library for AdonisJS, combining a Knex-powered query builder with class-based Active Record models. Lucid provides tools for schema migrations, model factories, database seeders, and complex relationship management including HasOne, HasMany, BelongsTo, and ManyToMany. It supports SQLite, LibSQL, MySQL, PostgreSQL, and MSSQL, and includes utilities for pagination, transactions, and schema generation.

Tokens
57.6K
Snippets
353
Records
385
Agent score
57%

What's inside Lucid

  1. Overview of Lucid Models

    A Lucid model is a TypeScript class that maps to a database table using the Active Record pattern. Each model instance represents a single row. Models provide a typed way to query data, persist records, and implement lifecycle hooks, relationships, computed properties, and serialization rules. Use the database query builder directly only for one-off reports, scripts, or complex joins that project columns not defined on a single model.
    import User from '#models/user'
    
    const user = await User.findOrFail(1)
    user.email = 'virk@adonisjs.com'
    await user.save()
  2. Overview of the Lucid Model Query Builder

    The model query builder, accessed via Model.query(), extends the standard database query builder. It provides all standard database methods (like where, orderBy, join) but adds model-aware capabilities. Key differences include: returning typed model instances instead of plain objects and the ability to query, aggregate, and hydrate relationships in a single chain.
    const posts = await Post
      .query()
      .where('is_published', true)
      .preload('author')
      .withCount('comments')
      .orderBy('created_at', 'desc')
  3. Overview of Lucid database validation rules

    Lucid extends VineJS with two database-aware validation rules: unique and exists. These are macros available on VineString and VineNumber and are registered automatically when the Lucid service provider boots. They can be used in two ways: via an options object for standard queries or via a callback for custom query logic.
    import vine from '@vinejs/vine'
    
    export const createUserValidator = vine.create({
      email: vine.string().email().unique({ table: 'users', column: 'email' }),
      referralCode: vine.string().exists({ table: 'referrals', column: 'code' }),
    })
  4. Overview of Lucid SQL library

    Lucid is the official SQL library for AdonisJS. It implements the Active Record pattern, combining a Knex-powered query builder with class-based models. Unlike code-first ORMs, Lucid treats the database as the source of truth: you write migrations to evolve the schema, and Lucid generates typed schema classes from the live database, which models then extend to provide type safety for columns.
  5. Overview of Lucid Model Hooks

    Hooks are static methods on a Lucid model that execute before or after specific lifecycle events. They allow you to encapsulate model-owned behavior—such as password hashing, audit logging, or cache invalidation—directly within the model class rather than in controllers or services. Hooks can be asynchronous, and throwing an error from a before hook will cancel the database operation and propagate the error to the caller.
  6. Import and use the Lucid Database service

    The db service is the entry point for database operations in Lucid. Use it when you need to query tables directly, run raw SQL, start transactions, or select a specific connection at runtime. It returns query clients that provide builders for selecting, inserting, and raw queries.
    import db from '@adonisjs/lucid/services/db'
    
    // Example: Querying a table directly
    const posts = await db.from('posts').select('id', 'title')
  7. Understand Lucid schema generation overview

    Lucid uses a database-first approach where the database is the source of truth. It introspects database tables to generate a typed database/schema.ts file. Models then extend these generated schema classes to inherit column types automatically. The generated file is overwritten on every generation; manual edits to database/schema.ts will be lost.
  8. Understand Lucid relationship naming and conventions

    Lucid relationships are declared on model classes in app/models/ and are independent of the generated schema. They are named based on which model holds the foreign key:

    • belongsTo: The current model holds the foreign key.
    • hasOne: Another model holds the foreign key, and there is at most one related row.
    • hasMany: Another model holds the foreign key, and there can be many related rows.
    • manyToMany: A separate pivot table holds foreign keys to both models.
    • hasManyThrough: The target model is reached via an intermediate model.

    To avoid circular import errors, related models are passed as a function returning the class (e.g., () => Post).

  9. Define table structure using the Table Builder

    The table builder is used inside createTable or alterTable callbacks within a schema migration to define column-level and table-level structures. It allows developers to specify column types, apply modifiers (like nullability and defaults), and define constraints.
    import { BaseSchema } from '@adonisjs/lucid/schema'
    
    export default class extends BaseSchema {
      async up() {
        this.schema.createTable('posts', (table) => {
          table.increments('id')
          table.string('title').notNullable()
          table.text('body')
          table.integer('user_id').unsigned().references('users.id').onDelete('CASCADE')
          table.timestamps(true, true)
        })
      }
    }
  10. Use the Schema Builder in Lucid migrations

    The schema builder is used to issue DDL statements (CREATE, ALTER, DROP) within migration files. It is accessed via this.schema inside a class that extends BaseSchema.
    import { BaseSchema } from '@adonisjs/lucid/schema'
    
    export default class extends BaseSchema {
      protected tableName = 'users'
    
      async up() {
        this.schema.createTable(this.tableName, (table) => {
          table.increments('id')
          table.string('email').unique().notNullable()
          table.timestamps(true, true)
        })
      }
    
      async down() {
        this.schema.dropTable(this.tableName)
      }
    }
  11. Create a single record with Lucid

    Use the static create method to persist a new record. This method inserts a row, executes all configured model hooks, and returns the created model instance. Alternatively, you can instantiate a model and call save() to perform an insert.
    import User from '#models/user'
    
    // Option 1: Static create
    const user = await User.create({
      email: 'user@example.com',
      password: 'secret',
    })
    
    // Option 2: Instance then save
    const user = new User()
    user.email = 'user@example.com'
    user.password = 'secret'
    await user.save()
  12. Preload relationships eagerly

    Use preload to load a relationship and attach it to every returned model instance. This avoids N+1 query issues by issuing one extra query per relationship regardless of the number of parent rows. You can constrain the relationship query using a callback, chain multiple preloads, or perform nested preloads by calling preload inside a callback.
    // Basic preload
    const users = await User.query().preload('posts')
    
    // Constrained preload
    await User.query().preload('posts', (postsQuery) => {
      postsQuery.where('status', 'published').orderBy('published_at', 'desc')
    })
    
    // Multiple and Nested preloads
    await User
      .query()
      .preload('posts', (postsQuery) => {
        postsQuery.preload('comments', (commentsQuery) => {
          commentsQuery.preload('author')
        })
      })
      .preload('profile')