Objection.js

repository·main·Indexed 27 days ago

https://github.com/vincit/objection.js

An SQL-friendly ORM for Node.js built on Knex.js. It provides a relational query builder that bridges the gap between raw SQL and a full ORM, offering tools for managing relations, eager loading, and graph operations. Features include a QueryBuilder that returns Model instances, support for snake_case to camelCase mapping, and factory functions like ref(), raw(), val(), and fn() for complex SQL construction.

Tokens
73.4K
Snippets
237
Records
343
Agent score
91%

What's inside objection.js

  1. Overview of Objection.js

    main

    Objection.js is a relational query builder for Node.js built on top of knex. It provides a powerful set of tools for working with SQL relations while allowing you to use the full power of your underlying database engine. It is not a traditional, fully object-oriented ORM; instead, it focuses on query-based interaction.

    Key Features:

    • Declarative model and relationship definitions.
    • Support for fetching, inserting, updating, and deleting objects.
    • Mechanisms for eager loading, graph inserts, and graph upserts.
    • Transaction support.
    • Official TypeScript support.
    • Optional JSON schema validation.
    • Ability to store complex documents as single rows.
  2. Use the QueryBuilder to interact with models

    main

    The QueryBuilder is the primary component in Objection.js used to fetch or modify database items. It acts as a wrapper around the knex QueryBuilder.

    Key characteristics:

    • Inheritance: It possesses all methods available in a knex QueryBuilder.
    • Model Instances: Unlike knex, which returns plain JavaScript objects, Objection's QueryBuilder returns instances of your Model subclasses.
    • Thenable: It is a 'thenable' object, meaning it can be used directly with await or returned from a promise to be chained like a standard Promise.
  3. Understand Objection.js error types

    main

    Objection.js throws four primary categories of errors:

    1. ValidationError: Thrown when input from the outside world is invalid (e.g., model instances, POJOs, relation expressions, or object graphs). Use the type property to distinguish between specific validation failures.
    2. NotFoundError: Thrown when the throwIfNotFound() method is called on a query but no results are returned.
    3. Database Errors: Errors defined by the db-errors library. These can be accessed directly via the objection package.
    4. Basic JavaScript Error: Indicates a programming or logic error. These should typically be treated as internal server errors (500) and fixed in the code.
  4. Write subqueries using functions

    main

    You can write subqueries in Objection.js by passing a function to query building methods (similar to Knex.js). The function receives a query builder instance which you use to construct the subquery.

    const peopleOlderThanAverage = await Person.query().where(
      'age',
      '>',
      builder => {
        builder.avg('age').from('persons');
      }
    );
    
    console.log(peopleOlderThanAverage);
  5. Define a Model in Objection.js

    main

    To represent a database table, create a class that inherits from the Model class. Each model instance represents a single row in that table.

    Key requirements and behaviors:

    • tableName: A static getter that returns the name of the database table. This is the only required property.
    • idColumn: A static getter that specifies the column(s) used to uniquely identify rows. It defaults to 'id'. You can provide a single string or an array of strings for composite keys.
    • No Global State: Objection.js uses class-based configuration. There is no global 'objection instance', allowing you to use different configurations or databases in the same application.
    • Schema Management: Do not define database schema (indexes, columns, etc.) in the model. Use migrations (e.g., Knex.js) to manage the database schema separately.
    const { Model } = require('objection');
    
    class MinimalModel extends Model {
      static get tableName() {
        return 'someTableName';
      }
    }
    
    module.exports = MinimalModel;
  6. Set default values using jsonSchema

    main

    You can define default values for model properties by using the default key within the jsonSchema static property. This is useful for ensuring that new model instances have predefined values for specific fields.

    class Person extends Model {
      static get jsonSchema() {
        return {
          type: 'object',
          properties: {
            gender: {
              type: 'string',
              enum: ['Male', 'Female', 'Other'],
              default: 'Female'
            }
          }
        };
      }
    }
  7. Use modifiers to customize eager loading

    main

    You can use modifiers() to apply named, reusable query logic to your relations. Modifiers can be defined as static getters on your Models.

    1. Define modifiers in the Model class using static get modifiers().
    2. Apply modifiers in the query string using the relationName(modifierName) syntax.
    3. Pass arguments to modifiers by using the .modifiers() method on the QueryBuilder.
  8. Best practices and warnings for upsertGraph

    main

    While upsertGraph is powerful, it should be used judiciously:

    1. Avoid the 'MongoDB' Trap: Do not use upsertGraph simply because you want a document-style API for a relational database. It is intended to save significant amounts of code in complex scenarios, not as a default for every update.
    2. Complexity Risk: As you add more options to UpsertGraphOptions, the code becomes harder to read and maintain.
    3. Concurrency Issues: Overusing upsertGraph on large graphs can lead to race conditions where one user's large upsert overrides another user's changes. Aim to update the minimum amount of rows and columns necessary.
    4. Security: Use the allowGraph() method to limit which relations can be modified via upsertGraph to prevent unauthorized data manipulation.
    5. Atomicity: Always use a transaction when performing upsertGraph operations to ensure the entire graph operation succeeds or fails as a single unit.
  9. Insert data with Insert queries

    main

    Create new records by chaining the .insert(data) method to a query. The returned value is the newly created Model instance. You can include subqueries or knex.raw expressions within the data object.

    // Basic insert
    const jennifer = await Person.query().insert({
      firstName: 'Jennifer',
      lastName: 'Lawrence'
    });
    
    // Insert with a subquery for a field
    const jenniferWithAvg = await Person.query().insert({
      firstName: 'Average',
      lastName: 'Person',
      age: Person.query().avg('age')
    });
  10. Extend the QueryBuilder in TypeScript

    main

    To use a custom QueryBuilder with TypeScript, you must define specific type properties on your custom builder class to ensure compatibility with Objection's internal types. These properties should be defined once in a shared BaseModel to avoid repetition.

    Note: The documentation suggests considering modifiers as an alternative to extending the query builder.

    import { Model, Page } from 'objection';
    
    class MyQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
      // These are necessary for TypeScript support.
      ArrayQueryBuilderType!: MyQueryBuilder<M, M[]>;
      SingleQueryBuilderType!: MyQueryBuilder<M, M>;
      MaybeSingleQueryBuilderType!: MyQueryBuilder<M, M | undefined>;
      NumberQueryBuilderType!: MyQueryBuilder<M, number>;
      PageQueryBuilderType!: MyQueryBuilder<M, Page<M>>;
    
      myCustomMethod(something: number): this {
        doSomething(something);
        return this;
      }
    }
    
    class BaseModel extends Model {
      // Both of these are needed for TypeScript.
      QueryBuilderType!: MyQueryBuilder<this>;
      static QueryBuilder = MyQueryBuilder;
    }
    
    // Usage
    class Person extends BaseModel {
      static tableName = 'persons';
    }
    
    await Person.query().where('id', 1).myCustomMethod(1).where('foo', 'bar');