Getting started

Relationship management is the key strength of Pinia ORM. By defining relationships, Pinia ORM uses the relationships you define to construct data when storing, modifying, and fetching from Pinia Store.

Defining Relationships

Relationships are defined as a field in your model classes. They use relationship attributes such as hasOne and hasMany. Pinia ORM supports many types of relationships you'd expect from an ORM. In our example below we define a "One to Many" relationship between the User model and the Post model:

class User extends Model {
  static entity = 'users'
  static fields () {
    return {
      id: this.attr(null),
      name: this.string(''),
      posts: this.hasMany(Post, 'userId')
    }
  }
}
class Post extends Model {
  static entity = 'posts'
  static fields () {
    return {
      id: this.attr(null),
      userId: this.attr(null),
      title: this.string('')
    }
  }
}

Pinia ORM provides a variety of relationship attributes. Please see corresponding documentation to learn how to define the relationships you would like to construct.

Composite Key support

The relations hasOne, belongsTo & hasMany are also supporting composite keys if you have models with combined primary key. So you can define something like this:

class User extends Model {
  static entity = 'users'
  static primaryKey = ['id', 'secondId']
  static fields () {
    return {
      id: this.attr(null),
      secondId: this.attr(null),
      name: this.string(''),
      posts: this.hasMany(Post, ['userId', 'userSecondId'])
    }
  }
}
class Post extends Model {
  static entity = 'posts'
  static fields () {
    return {
      id: this.attr(null),
      userId: this.attr(null),
      userSecondId: this.attr(null),
      title: this.string('')
    }
  }
}

Loading Relationships

Relationships must be "eagerly loaded" by adding the with(...) method to your repository's query chain. For example, let's say your User model has the following relationship to the Post model:

class User extends Model {
  static entity = 'users'
  static fields () {
    return {
      id: this.attr(null),
      name: this.string(''),
      posts: this.hasMany(Post, 'userId')
    }
  }
}
class Post extends Model {
  static entity = 'posts'
  static fields () {
    return {
      id: this.attr(null),
      body: this.string(''),
      userId: this.attr(null),
    }
  }
}

To retrieve the User model with any related Post, you chain the with('posts') method to your query. The argument posts is the key name where the relationship is defined.

const user = useRepo(User).with('posts').first()
/*
  {
    id: 1,
    name: 'John Doe',
    posts: [
      { id: 1, userId: 1, title: '...' },
      { id: 2, userId: 1, title: '...' }
    ]
  }
*/

Of course, you may chain more than one with(...) method to retrieve multiple relationships.

const user = useRepo(User).with('posts').with('phone').first()

Loading Nested Relationships

To load nested relationships, you may pass constraints to the 2nd argument. For example, let's load all of the book's authors and all of the author's personal contacts:

const books = useRepo(Book).with('author', (query) => {
  query.with('contacts')
}).get()

Learn more about the constraining query in the next section.

Constraining a Relationship Query

Sometimes you may wish to load a relationship, but also specify additional query conditions for the loading query. Here's an example:

const users = useRepo(User).with('posts', (query) => {
  query.where('published', true)
}).get()

In this example, it will only load posts where the post's published filed matches the true first. You may call other query builder methods to further customize the loading operation:

const users = useRepo(User).with('posts', (query) => {
  query.orderBy('createdAt', 'desc')
}).get()

Lazy Relationship Loading

Sometimes you may need to load a relationship after the model has already been retrieved. For example, this may be useful if you need to decide whether to load related models dynamically. You may use the load method on a repository to load relationships on the fly in such a case.

const userRepo = useRepo(User)
// Retrieve models without relationships.
const users = userRepo.all()
// Load relationships on the fly.
userRepo.with('posts').load(users)

You may set any additional query constraints as usual to the with method.

userRepo.with('posts', (query) => {
  query.orderBy('createdAt', 'desc')
}).load(users)

Note that the load method will mutate the given models, therefore, it is advised to use the method within a single computed property.

import { mapRepos } from 'pinia-orm'
import User from '@/models/User'
export default {
  data () {
    return {
      condition: false
    }
  },
  computed: {
    ...mapRepos({
      userRepo: User
    }),
    users () {
      const users = this.userRepo.all()
      if (this.condition) {
        this.userRepo.with('posts').load(users)
      }
      return users
    }
  }
}

Loading All Relationships

To load all relationships, you may use the withAll and withAllRecursive methods.

The withAll method will load all model level relationships. Note that any constraints will be applied to all top-level relationships.

// Fetch models with all top-level relationships.
useRepo(User).withAll().get()
// As above, with a constraint.
useRepo(User).withAll((query) => {
  // This constraint will apply to all of the relationship User has. For this
  // example, all relationship will be sorted by `createdAt` field.
  query.orderBy('createdAt')
}).get()

The withAllRecursive method will load all model level relationships and sub relationships recursively. By default, the maximum recursion depth is 3 when an argument is omitted.

// Fetch models with relationships recursively.
useRepo(User).withAllRecursive().get()
// As above, limiting to 2 levels deep.
useRepo(User).withAllRecursive(2).get()

Inserting Relationships

You may use save method to save a record with its nested relationships to the store. When saving new records into the store via save method, Pinia ORM automatically normalizes and stores data that contains any nested relationships in it's data structure. For example, let's say you have the User model that has a relationship to the Post model:

class User extends Model {
  static entity = 'users'
  static fields () {
    return {
      id: this.attr(null),
      name: this.string(''),
      posts: this.hasMany(Post, 'userId')
    }
  }
}

When you save a user record containing post records under the posts key, Pinia ORM decouples the user and post records before saving them to the store.

// The user record.
const user = {
  id: 1,
  name: 'John Doe',
  posts: [
    { id: 1, userId: 1, title: '...' },
    { id: 2, userId: 1, title: '...' }
  ]
}
// Save the user record.
useRepo(User).save(user)
// The result inside the store.
{
  entities: {
    users: {
      1: { id: 1, name: 'John Doe' }
    },
    posts: {
      1: { id: 1, userId: 1, title: '...' },
      2: { id: 2, userId: 1, title: '...' }
    }
  }
}

Note that Pinia ORM automatically generates any missing foreign keys during the normalization process. In this example, the Post model has a foreign key of userId that corresponds to the id key of the User model. If you save post records without userId, it will still get populated:

// The user record.
const user = {
  id: 1,
  name: 'John Doe',
  posts: [
    { id: 1, title: '...' }, // <- No foreign key.
    { id: 2, title: '...' }  // <- No foreign key.
  ]
}
// Save the user record.
useRepo(User).save(user)
// The result inside the store.
{
  entities: {
    users: {
      1: { id: 1, name: 'John Doe' }
    },
    posts: {
      1: { id: 1, userId: 1, title: '...' }, // Foreign key generated.
      2: { id: 2, userId: 1, title: '...' }  // Foreign key generated.
    }
  }
}

Depending on the relationship types, there may be a slight difference in behavior when inserting data. Please refer to the specific relationship type docs for more details.

Updating Relationships

The save method also updates models that already exist in the store, including any nested relationships within the given records. Let's reuse our User and Post example:

class User extends Model {
  static entity = 'users'
  static fields () {
    return {
      id: this.attr(null),
      name: this.string(''),
      posts: this.hasMany(Post, 'userId')
    }
  }
}

When you save the user record, relationships will be saved in a normalized form inside the store.

// Existing data in the store.
{
  entities: {
    users: {
      1: { id: 1, name: 'John Doe' }
    },
    posts: {
      1: { id: 1, userId: 1, title: 'Title A' },
      2: { id: 2, userId: 1, title: 'Title B' }
    }
  }
}
// The user record.
const user = {
  id: 1,
  name: 'Jane Doe',
  posts: [
    { id: 1, userId: 1, title: 'Title C' },
    { id: 2, userId: 2, title: 'Title D' }
  ]
}
// Insert the user record.
useRepo(User).save(user)
// The result inside the store.
{
  entities: {
    users: {
      1: { id: 1, name: 'Jane Doe' } // <- Updated.
    },
    posts: {
      1: { id: 1, userId: 1, title: 'Title C' }, // <- Updated.
      2: { id: 2, userId: 1, title: 'Title D' }  // <- Updated.
    }
  }
}

Deleting Relationships

If you delete a record then all relations with that record are staying untouched. But sometimes you want all relations to be deleted with that record. Of course you could save the deleted record id and manually delete the related relations but you have also the option to define through onDelete method the behaviour for relations on delete. onDelete can have two options cascade and set null

class User extends Model {
  static entity = 'users'
  static fields () {
    return {
      id: this.attr(null),
      number: this.string(''),
      // If this record is deleted all comments with that userId are also deleted
      comments: this.hasMany(Comment, 'userId').onDelete('cascade')
      // If you just want to lose the binding you can also define 'set null' 
      // comments: this.hasMany(Comment, 'userId').onDelete('set null')
    }
  }
}

There exist also a decorator @OnDelete for this