Skip to content

CRUD Operations

This document explains how to perform Create, Read, Update, and Delete (CRUD) operations using models in ReActive Record.

Creating Records

Creating a Single Record

You can create records in two ways:

  1. Using the static create() method to create and save in one step:
typescript
const user = await User.create({
  email: 'john@example.com',
  name: 'John Doe'
})
  1. Creating an instance, assigning values, and then calling save():
typescript
const user = new User()
user.email = 'john@example.com'
user.name = 'John Doe'
await user.save()

You can also use the fill() and merge() methods to update model properties before saving:

typescript
const user = new User()
user.fill({ email: 'john@example.com' }) // fills only undefined properties
user.merge({ name: 'John Doe' })         // merges and overwrites properties
await user.save()

Both creating a new instance of the constructor and using ReactiveModel.create achieve the same result. The second approach can be useful when you need to perform additional operations or validations before saving.

Creating Multiple Records

Use createMany() to create and save multiple records at once:

typescript
const users = await User.createMany([
  { email: 'john@example.com', name: 'John Doe' },
  { email: 'jane@example.com', name: 'Jane Doe' }
])

Reading Records

Find by Primary Key

Use find() to find a record by its primary key. Returns undefined if not found.

typescript
const user = await User.find(1)

Find or Fail

Use findOrFail() to find a record by primary key or throw an error if not found.

typescript
try {
  const user = await User.findOrFail(1)
} catch (error) {
  // Handle not found error
}

Find by Property

Use findBy() to find a record by a specific property. Returns undefined if not found.

typescript
const user = await User.findBy('email', 'john@example.com')

Find Many by Property

Use findManyBy() to find multiple records by a property value.

typescript
const admins = await User.findManyBy('role', ['admin', 'moderator'])

First Record

Use first() to get the first record or undefined if none exist.

typescript
const firstUser = await User.first()

All Records

Use all() to get all records.

typescript
const allUsers = await User.all()

Updating Records

Updating an Instance

Modify properties on a model instance and call save() to persist changes:

typescript
const user = await User.find(1)
if (user) {
  user.name = 'New Name'
  await user.save()
}

Checking for Pending Changes

Use the dirty property to check if there are unsaved changes:

typescript
if (user.dirty) {
  console.log('Pending changes:', user.pending)
}

Updating Multiple Records

You can update multiple records using the query builder's new update() method, which fetches matching records and updates them:

typescript
await User.query()
  .where(user => user.role === 'guest')
  .update({ role: 'member' })

Deleting Records

Deleting an Instance

Call delete() on a model instance to delete it:

typescript
const user = await User.find(1)
if (user) {
  await user.delete()
}

Finding Multiple Records by ID

Use findMany() to find multiple records by their primary keys:

typescript
const users = await User.findMany([1, 2, 3])

Mass Deletion

You can delete multiple records using the query builder's delete() method:

typescript
await User.query()
  .where(user => user.status === 'inactive')
  .delete()

Finding or Creating Records

First or New

Use firstOrNew() to get the first matching record or create a new instance (without saving):

typescript
const user = await User.firstOrNew(
  { email: 'john@example.com' }, // search criteria
  { name: 'John Doe' }           // attributes for new instance
)

First or Create

Use firstOrCreate() to get the first matching record or create and save a new one:

typescript
const user = await User.firstOrCreate(
  { email: 'john@example.com' }, // search criteria
  { name: 'John Doe' }           // attributes for new instance
)

Update or Create

Use updateOrCreate() to update a matching record or create a new one:

typescript
const user = await User.updateOrCreate(
  { email: 'john@example.com' },  // search criteria
  { name: 'Updated Name' }        // attributes to update/create
)

Model State and Deletion

When a model is deleted using the delete() method:

  1. The record is removed from the database
  2. The model instance becomes read-only
  3. All properties remain readable but cannot be modified
  4. Any attempt to modify properties or call methods like save(), merge(), fill(), or reset() will throw a ReactiveModelDeletedException
  5. Attempting to subscribe to events will throw a ReactiveModelUnsubscribableException

Example handling deleted state:

typescript
const user = await User.find(1)
if (user) {
  await user.delete()
  
  // Properties are still readable
  console.log(user.id, user.name) // OK
  
  try {
    user.name = 'New Name' // Throws ReactiveModelDeletedException
  } catch (error) {
    console.log('Cannot modify deleted model')
  }
  
  try {
    user.onChange(() => {}) // Throws ReactiveModelUnsubscribableException
  } catch (error) {
    console.log('Cannot subscribe to deleted model events')
  }
}

Reactivity and CRUD

Models are reactive by default. You can listen to changes on records using:

Example:

typescript
const user = await User.find(1)
if (user) {
  // Listen to all changes
  user.onChange((newState) => {
    console.log('User updated:', newState)
  })

  // Listen to specific property
  user.onPropertyChange('email', (newValue, oldValue) => {
    console.log('Email changed:', { newValue, oldValue })
  })

  // Listen to detailed changes
  user.onDelta((delta) => {
    console.log('Changes:', delta)
  })
}

Additional Methods

Instance Methods

  • fill() - Fill undefined properties
  • merge() - Merge properties
  • reset() - Reset pending changes to clear unsaved modifications
  • clone() - Create a new instance with the same properties as the current one, excluding the primary key

Static Methods

Best Practices

  1. Use appropriate methods for your use case:

    • createMany() when creating multiple records at once
    • find() when undefined is an acceptable result
    • findOrFail() when the record must exist
    • firstOrCreate() to ensure a record exists
    • updateOrCreate() to upsert records
  2. Handle errors appropriately:

    typescript
    try {
      const user = await User.findOrFail(1)
    } catch (error) {
      if (error instanceof MissingReactiveModelRecordError) {
        // Handle not found case
      }
    }
  3. Check model state:

    typescript
    // Check for unsaved changes
    if (model.dirty) {
      console.log('Pending changes:', model.pending)
    }
    
    // Check if model is deleted
    try {
      await model.save()
    } catch (error) {
      if (error instanceof ReactiveModelDeletedException) {
        console.log('Cannot modify deleted model')
      }
    }
  4. Leverage reactivity effectively:

    typescript
    // Use onChange for general updates
    model.onChange((newState, oldState) => {
      updateUI(newState)
    })
    
    // Use onPropertyChange for specific fields
    model.onPropertyChange('status', (newValue, oldValue) => {
      updateStatusIndicator(newValue)
    })
    
    // Use onDelta for detailed change tracking
    model.onDelta((changes) => {
      logChanges(changes)
    })
  5. Clean up event listeners when no longer needed:

    typescript
    const onChange = (state) => { /* ... */ }
    model.onChange(onChange)
    // Later when done:
    model.offChange(onChange)

For more information on advanced querying and relationships, see: