Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy loading after draft 15 #108

Closed
SimonDatapas opened this issue Nov 22, 2021 · 4 comments
Closed

Lazy loading after draft 15 #108

SimonDatapas opened this issue Nov 22, 2021 · 4 comments
Labels
question Further information is requested

Comments

@SimonDatapas
Copy link

SimonDatapas commented Nov 22, 2021

For some use-cases in our apps we need to use a lot of relations. For some of those cases we'd like the guarantee of having the relations loaded and made use of a way of lazy loading those relations.

import {Model} from "@vuex-orm/core";
import Post from "./Post";

export default class User extends Model {
    static entity = 'User';

    static fields() {
        return {
            id: this.uid(),
            name: this.string(''),
            posts: this.hasMany(Posts, 'user_id')
        };
    }

    get lazyPosts() {
        if (this.posts === undefined) {
            this._database.store.$repo(User).with('posts').load([this]);
        }

        return this.posts;
    }

    get lazyComments() {
        return this.lazyPosts.reduce((carry, post) => carry.concat(post.lazyComments), []);
    }
}

We are aware that lazy loading brings a bit of overhead and should be avoided by lots of data, but we do have some usecases were we'd still like the guarantee of having the relation loaded or else loading.

How should we deal with these types of situations?
Will this be suported (in the future)?

@kiaking kiaking added the question Further information is requested label Nov 24, 2021
@kiaking
Copy link
Member

kiaking commented Nov 24, 2021

This is tough one and I definitely think worth discussing about it.

Because of Vue's reactivity nature, lazy loading will cause quite a huge n+1 problem in the app. For example, if you call user.lazyPosts while you are looping through 100 users object, that will create 100 loading, which is 100 full loop on Post records.

If you have only few items, I think performance wouldn't get affected. But it could lead to issues which is also very tough to debug.

One thing I can think of is to create composable function that handles this situation. Let's say you have a component that requires passed in users to always have posts relation, you could create this kind of composable function.

import { computed } from 'vue'
import { useStore } from 'vuex'
import User from '@/models/User'

export function useUsersWithPosts(users) {
  const store = useStore()
  const userRepo = store.$repo(User)

  // We should load data newly since `load` method
  // is going to mutate the given objects.
  const usersWithPosts = computed(() => {
    const items = userRepo.revive(users)

    userRepo.with('posts').load(items)

    return items
  })

  return {
    users: usersWithPosts
  }
}

Then inside your component, you could this function like:

export default {
  props: {
    users: { type: Array, required: true }
  },

  setup(props) {
    const { users } = useUsersWithPosts(computed(() => props.users))

    // Now `users` have posts relation.

    return {
      users
    }
  }
}

It doesn't look that intuitive, but this is what it should happen. We should always load relationship as minimal as possible.

@SimonDatapas
Copy link
Author

Thank you for the reaction.

This would be a solution that could lazy load relations, however I feel this should be functionality that belongs to the model and should not be placed in a separate structure using the composition API.

Thinking more deeply about our use-case for lazy loading relationships, while prototyping an app that uses models with relations nesting deep until 10 levels down, we pass on those models and relations to underlying Vue components. While prototyping a few levels deep, you keep switching to the root where all relations should be loaded and the components where you are trying out and using the relations, resulting in errors when the relations is not loaded.
For this reason, we started implementing the lazy relations.

At this time all (or most) relationships in our project seem to be preloaded in any case due to performance necessities. I will start a refactor in my project today to see whether the last use-cases should/can be preloaded as well.

For the prototyping phase of a project feedback on unloaded relations would be very helpful. Perhaps Vuex-orm could give a warning in the console whenever a relation is being called that has not yet been loaded?

@cuebit
Copy link
Member

cuebit commented Nov 26, 2021

I feel the biggest concern for most ORM-esque libraries is that lazy-loading depends entirely on the design at the domain model level. That is to say, if Active Record (AR) is a design pattern at its core (which Vuex ORM no longer is) then all your data source logic is driven at the model level too – making it possible to communicate directly with data sources. AR also has its pitfalls, particularly when it comes to dependency injection on SSR. We found the biggest suffering to be at the hands of Nuxt users. Thus, the Repository pattern fast became a likely candidate to adopt for the official v1 release. The additional drawback of implementing lazy-loading at the library level is memory consumption. With the Repository pattern, your data source logic is decoupled from models and models are essentially data carriers, so any reference to relations will be held in memory since the pattern requires Repo-to-Model data allocation/mapping.

While prototyping a few levels deep, you keep switching to the root where all relations should be loaded and the components where you are trying out and using the relations, resulting in errors when the relations is not loaded. For this reason, we started implementing the lazy relations.

If you're not concerned about SSR compatibility, you can still instantiate repositories from models and provide local functionality to communicate with them for data mapping. For example:

// @/models/Model.js

import { Model as BaseModel } from '@vuex-orm/core'
import { store } from 'path/to/store'

export class Model extends BaseModel {
  $repo(model) {
    return store.$repo(model ?? this.$self())
  }
}
// @/models/User.js

import { Model } from './Model'

export class User extends Model {
  get lazyPosts() {
    if (this.posts === undefined) {
      return this.$repo().with('posts').load([this])
    }

    return this.posts
  }
}

While this is an over-simplistic workaround, I would recommend @kiaking's approach, which supports the purpose of separating, clearly and in one direction, the dependency between the work domain and the data allocation or mapping.

For the prototyping phase of a project feedback on unloaded relations would be very helpful. Perhaps Vuex-orm could give a warning in the console whenever a relation is being called that has not yet been loaded?

See #96

@SimonDatapas
Copy link
Author

Thank you both Kia and Cue for your answers and insights.

I have refeactored our project to not use lazy-loading at all but to preload where needed. While refactoring I found 1 or 2 use-cases where relations were not yet pre-loaded, wich (after refactoring) caused an improvement in performance.

For now I could not find a reason to need support for lazy-loading.

#96 however would be a very nice addition! (Thanks for the refferal!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants