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

PATCH doesn't trigger Sequelize instance hooks #155

Closed
skinofstars opened this issue Aug 21, 2017 · 26 comments
Closed

PATCH doesn't trigger Sequelize instance hooks #155

skinofstars opened this issue Aug 21, 2017 · 26 comments
Labels

Comments

@skinofstars
Copy link

skinofstars commented Aug 21, 2017

This refers to Sequelize hooks. Not Feathers hooks.

Doing a PATCH request doesn't trigger Sequelize inbuilt hooks, even when the service.options.raw is false.

The problem seems to be because we don't instance.update, rather we apply a Model.update

https://github.com/feathersjs-ecosystem/feathers-sequelize/blob/v3.1.0/lib/index.js#L219

This means the Sequelize hooks triggered are the bulk actions (afterBulkCreate/afterBulkUpdate) rather than the expected instance actions (afterCreate/afterUpdate).

@DesignByOnyx
Copy link
Contributor

So, this has kind of been a recurring theme with PRs lately: people are creating multiple code paths to satisfy some edge conditions or micro-optimizations. This creates code that looks like this:

if (someCondition) {
  doThingsOneWay();
} else {
  doThingsAnotherWay();
}

These extra code paths do not get tested by the shared adapter tests. This ends up affecting people who accidentally trigger one code path instead of the other without knowing - causing new and unexpected behaviors to "just appear one day".

My advise to you is to implement your code in both the "single" and "bulk" hooks. This should be very simple to do and will allow feathers-sequelize to continue to have a single code path for patches:

function someCustomFunction (instance) {
  // do custom stuff here
  return Promise.resolve();
}
MyModel.hook('afterCreate', (instance, options) => {
  return someCustomFunction(instance);
});
MyModel.hook('afterBulkCreate', (instances, options) => {
  return Promise.all(instances.map(someCustomFunction));
});

@skinofstars
Copy link
Author

skinofstars commented Aug 23, 2017

I totally get that you want the code to be simple and stay out the way. The issue I've got with that is that it's acting unexpectedly. One would expect an action on a single record to perform an instance update, and a bulk action to trigger a bulk hook.

Worse still, these class hooks lose the instance context. So if the original service.Model has its schema set, this info is lost. That's all the more frustrating (impossible?) if you're chaining hooks or using hooks on nested instances.

@lijoantony
Copy link

lijoantony commented Dec 16, 2017

It may not be possible to implement the same code in bulk hook instead of single variant, as they might not have access to the instance. For example beforeUpdate(instance, options) vs beforeBulkUpdate(options). Especially true when it's a library which got broken, when I switched from PUT to PATCH method.

@lijoantony
Copy link

@DesignByOnyx @skinofstars Any advice to implement the functionality of beforeUpdate hook using beforeBulkUpdate since beforeBulkUpdate doesn't have instance param?

My project uses the library sequelize-paper-trail to implement some important features, which unfortunately got broken when I switched from PUT to PATCH method.

@stale
Copy link

stale bot commented Apr 25, 2018

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Apologies if the issue could not be resolved. FeathersJS ecosystem modules are community maintained so there may be a chance that there isn't anybody available to address the issue at the moment. For other ways to get help see here.

@stale stale bot added the wontfix label Apr 25, 2018
@skinofstars
Copy link
Author

skinofstars commented Apr 25, 2018

I don't think this should be closed. New users should be aware that the adapter will make Sequelize act differently to its design and existing documentation.

@stale stale bot removed the wontfix label Apr 25, 2018
@daffl
Copy link
Member

daffl commented Apr 25, 2018

Then we need some actionable steps and someone to implement them. I'm not even sure if that means a documentation or code update in this case.

@skinofstars
Copy link
Author

skinofstars commented Apr 26, 2018

Personally, I think it's a code change needed. It's not acting as expected and breaking core Sequelize functionality.

We currently maintain our own monkey patched version of this module in order to be Sequelize compatible, but we're refactoring to just use Sequelize directly.

@daffl
Copy link
Member

daffl commented Apr 26, 2018

Been talking to @alexisabril about a similar thing. Using Sequelize directly in a custom service is definitely starting to look like a more flexible, straightforward and viable approach going forward. Then this adapter could just provide some utility functions and default functionality but you are forced to use the Sequelize models directly.

This adapter with all its edge cases and special requirements is definitely one of the more painful Feathers modules to maintain at the moment.

@stale
Copy link

stale bot commented Jul 22, 2018

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Apologies if the issue could not be resolved. FeathersJS ecosystem modules are community maintained so there may be a chance that there isn't anybody available to address the issue at the moment. For other ways to get help see here.

@stale stale bot added the wontfix label Jul 22, 2018
@stale stale bot closed this as completed Jul 29, 2018
@chrisbag
Copy link

I have stumbled into this problem too, was wondering why the Sequelize hooks were not being executed.
I am happy to have the answer but it is unfortunated that no workaround or fix was offered since 2017

@skinofstars
Copy link
Author

If you aren't too far down the rabbit hole, then I'd recommend moving any event hooks out of Sequelize and in to the Feathers hooks system. It's easier to maintain and more apparent to the next dev looking at it.

@chrisbag
Copy link

Hello @skinofstars.
Thanks for the fast answer. I actually just came up with a quick workaround. Basically, the problem is that feathers-sequelize does not trigger the beforeUpdate hook but the beforeBulkUpdate as correctly identified in the thread.
You can simply tell beforeBulkUpdate to trigger individual hooks.

  
  {beforeBulkUpdate: function(options) {
            options.individualHooks = true
    },
    //https://github.com/feathersjs-ecosystem/feathers-sequelize/issues/155
    beforeUpdate: function(instance: any) {
      transformInstance(instance)
    }
}

I get your point about easier and clearer code using the feathers Hooks system but Sequelize Models just seem like the right place to be doing data manipulation or Validation.

@DesignByOnyx
Copy link
Contributor

I second @skinofstars recommendation. If you're going to use sequelize, use it just as a query generator/executor. Don't bother with its hooks, eager data loading, or anything else - always favor using feathers hooks for all things business related... which includes data manipulation and validation, relational data loading ("eager" loading), caching, etc.

Furthermore - this is a bit out of scope... but I even advise against sequelize in general. Years ago I was a huge proponent of sequelize, I have contributed to this very repo, and I used to be the unofficial go-to guy in Slack for most things feathers-sequelize. I strongly recommend against it as it's bloated, a bit dated, rought with long-standing bugs, difficult to use, and overall has a diametrically opposing set of goals than feathers when it comes to dealing with data IMO.

@edwardsmarkf
Copy link
Contributor

edwardsmarkf commented Oct 21, 2021 via email

@chrisbag
Copy link

I second @skinofstars recommendation. If you're going to use sequelize, use it just as a query generator/executor. Don't bother with its hooks, eager data loading, or anything else - always favor using feathers hooks for all things business related... which includes data manipulation and validation, relational data loading ("eager" loading), caching, etc.

Furthermore - this is a bit out of scope... but I even advise against sequelize in general. Years ago I was a huge proponent of sequelize, I have contributed to this very repo, and I used to be the unofficial go-to guy in Slack for most things feathers-sequelize. I strongly recommend against it as it's bloated, a bit dated, rought with long-standing bugs, difficult to use, and overall has a diametrically opposing set of goals than feathers when it comes to dealing with data IMO.

@DesignByOnyx Thanks for the tips. I do find that Sequelize can be quite a pain, especially when you move away from Vanilla queries and I am always having a hard time finding the info I want in their doc. However I must admit I really how Sequelize integrates directly within Feathers, offering great querying functionality (pagination, filtering, sorting).
What would you recommend instead of Sequelize which would integrate well in feathers ? Knex ? I don't really have any other experience with ORMs but to me it feels that we would have the same pains with other ORMs
What do you mean by opposing goals between feathers and Sequelize ?

Ryan - do you have a recommendation for something other than sequliize? a year ago i thought that MariaDB was taking over sequelize, or at least that was mentioned on one of their presentations. Thank you, Mark Edwards

On Wed, Oct 20, 2021 at 5:55 PM Ryan Wheale @.***> wrote: I second @skinofstars https://github.com/skinofstars recommendation. If you're going to use sequelize, use it just as a query generator/executor. Don't bother with its hooks, eager data loading, or anything else - always favor using feathers hooks for all things business related... which includes data manipulation and validation, relational data loading ("eager" loading), caching, etc. Furthermore - this is a bit out of scope... but I even advise against sequelize in general. Years ago I was a huge proponent of sequelize, I have contributed to this very repo, and I used to be the unofficial go-to guy in Slack for most things feathers-sequelize. I strongly recommend against it as it's bloated, a bit dated, rought with long-standing bugs, difficult to use, and overall has a diametrically opposing set of goals than feathers when it comes to dealing with data IMO. — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub <#155 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAWJ3YTBKCIEGIHD7FWPJRDUH5QHDANCNFSM4DXVZ7HA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

To me MariaDb is more of a DBMS than an ORM, so they are not really comparable.

@skinofstars
Copy link
Author

@chrisbag When I was hitting this problem, I was also using Postgres schema within a multi-tenant application, so I needed the requesting context. I'm not sure if your workaround resolves that issue. If so, please send that info back to 2017 😂

Like others, I've stopped using Sequelize. Too much bloat, too many issues. I now use Knex, it includes all the same Feathers support you're looking for, but in a lighter and more stable package.

@DaddyWarbucks
Copy link
Contributor

I second @skinofstars recommendation. If you're going to use sequelize, use it just as a query generator/executor. Don't bother with its hooks, eager data loading, or anything else - always favor using feathers hooks for all things business related... which includes data manipulation and validation, relational data loading ("eager" loading), caching, etc.

^^ This can be said with almost any adapter or tool integrated with Feathers. Favor feathers hooks over the built in methods/hooks. The only thing I disagree with here is the "relational data loading ("eager" loading)" when querying/sorting. You should use something like https://github.com/feathersjs-ecosystem/batch-loader for joining data onto the result. But, when you need to query/sort across tables, you should probably use sequelize.include. This can be accomplished with hooks, for example https://daddywarbucks.github.io/feathers-fletching/hooks.html#joinquery. But there are loads of caveats compared to doing a true RDMS join.

I generally recommend a sequelize include that looks like this

params.sequelize = {
  include: [
    model: MyModel,
    // empty array tells Sequelize not to actually "join" the result,
    // but still makes it available for querying/sorting. 
    attributes: []
  ]
}

You can also checkout https://daddywarbucks.github.io/feathers-fletching/hooks.html#sequelizejoinquery to help with join query/sort. I don't mean to plug my own libraries, but that is the hook I use to do what we are describing, which is to use Sequelize as little as possible.

@chrisbag I don't want to speak for Ryan (and he probably has a more eloquent answer), but I can give my opinion on what he means about Sequelize and Feathers having opposing goals. Sequelize favors models where Feathers favors services. I think it is best described in an example:

// This is how you would fetch an author for a book in Sequelize. Sequelize stuffs all of its methods, hooks,
// helpers into its models. It's almost a monolithic structure. 
// Uh oh... does this method join on the author's PW?
book.getAuthor();

// In Feathers, we use the service for everything. The service replaces the model and the service
// interface is the only thing we should use to access the data. Doing so ensures all of our actions
// run through our app logic, such as protecting the author's PW
const author = await app.service('authors').get(book.author_id)

The goal of Sequelize is to be a robust ORM where all the config, logic, relationships, etc live in the ORM. Feathers breaks all of that out into services.

@DesignByOnyx
Copy link
Contributor

DesignByOnyx commented Oct 21, 2021

I think Beau (DaddyWarbucks) captured my sentiments correctly. I should have probably specified "ORMs" in general as having an opposing agenda with feathers. It just happens that sequelize is the most "ORM-y" integration with feathers. ORMs provide a whole lot of complex functionality and API for managing data, but "the feathers way" would favor simple small bite-sized hooks for achieving the same functionality. When you drink the service-based koolaid for long enough (ala feathers), you start to really resent the difficulty and bloat of ORMs.

The trap you fall into is when you try to use the ORM for all of it's "goodness" - I mean it's there, you should use it, right? You spend hours and hours trying to bend the ORM to your will. Then you finally get everything working correctly by turning the raw option on - thus disabling most of the features of the ORM. Then you begin to notice your DB queries getting slower and slower for no obvious reason. When you have no hair left on your head... some random thread on the internet makes you aware of this obscure option to keep queries fast (eg. separate: true). Then you go to investigate what that option is doing under the hood and you realize that the ORM switches modes to something which almost exactly matches the way feathers hooks work. At this point you have to buy a new computer because you threw your last one out the window, and you embrace the feathers way for all of it's amazingness.

@chrisbag
Copy link

chrisbag commented Oct 22, 2021

@DaddyWarbucks @DesignByOnyx
Thanks a lot for your detailed answers which provide me with a better understanding of feathers.
I also get your point on the difference between services and Models. Specifically using sequelize to get associations does not run the feathers hooks (which in same cases can lead to security issues on top of just lost functionality). Therefore it would require to almost duplicate some of the feathers hooks into sequelize hooks.

I use sequelize.include a lot to attach associations for the find method with the downside that obviously the feathers hooks of the association are not executed. Imagine you have a model which is related to 3 other models and you want to attach associations. If you use the feathers way of attach data through a hook, it will generate 3 independant queries which does not seem optimal. The same would probably apply to attach association on a Find query.
I see @DaddyWarbucks that you recommend Batch loader for joining queries when no sort. What would be your stance for both these use cases in terms of efficiency Feathers hooks association through service call vs Sequelize join query ?

I checked out feathers-fletching doc, there are a lot of interesting use cases which I have been confronted to.
Will definitely test our your recommendations on new features.

The trap you fall into is when you try to use the ORM for all of it's "goodness" - I mean it's there, you should use it, right? You spend hours and hours trying to bend the ORM to your will. Then you finally get everything working correctly by turning the raw option on - thus disabling most of the features of the ORM.

Clearly something I have experienced :)

@skinofstars
Copy link
Author

skinofstars commented Oct 22, 2021

Not sure how those guys do it, but my approach is generally to map a service to a table. Then I'd have services that compose actions using these data services.

If I do have to start doing some light associations, then I'd use a dedicated service that more often than not uses a raw query (I'm mostly just using Knex for sanitisation at this point).

Once the queries start getting gnarly, or I want pagination, caching, etc, I move them to database views... and we're back to clean services.

We as coders do too much data munging in code, it is often be more efficient and cleaner to have that handled by the database before it ever gets to our tangled web of JS.

@DesignByOnyx
Copy link
Contributor

it will generate 3 independant queries which does not seem optimal

That's what separate: true does inside sequelize, and it's both necessary and optimal to "fix" some very bad performance issues, especially when eager-loading lots of data. Under the hood sequelize will perform separate queries and join all the data in memory. I've had performance go from ~10s to ~200ms by doing separate queries for complex data joins - in my case it was 6+ separate queries. It was at the point I realized that sequelize was [virtually] useless in the context of feathers and that my assumptions about what was "optimal" were quite wrong. When you move to something like batch-loader, you get all the service-level hooks, which is very good and maybe even required for your application. You can further implement service-level caching to make these separate queries very very fast.

Once the queries start getting gnarly, or I want pagination, caching, etc, I move them to database views.

I used to do this (and it's still a good thing to do), but I have found that writing complex views was a separate nightmare, more maintenance, and not very accessible to developers who weren't strong in SQL. Furthermore, it's only possible with SQL databases In today's world I find myself combining data from multiple stores across multiple services/data-centers/et al - which is only possible using hooks.

In summary, it is my experience that "do it in a hook" was always the better decision, and I wasted a lot of time of my life figuring that out. Hopefully I can save others from wasting theirs.

@DaddyWarbucks
Copy link
Contributor

feathers-batchloader is based on DataLoader. I definitely recommend anyone interested in it to watch this video: https://www.youtube.com/watch?v=OQTnXNCDywA. I always recommend doing joins via the service. Even when I do use sequelize.include to do some joins for query/sort, I always use attributes: [] so that the joins are not actually appended to the result. The include is only used for query/sort, not for actually populating the join onto the result

Using batchloader is very performant. Checkout the guide for some more info about how batching/caching works: https://github.com/feathersjs-ecosystem/batch-loader/blob/master/docs/guide.md

With the new batchloader v2 coming out, its even easier and more feathers-like to use. And paired with Node's AsyncLocalStorage its 💋 👌 .

Heres a vid of how I am using batchloader as of late: https://www.loom.com/share/bb5103effd654d18afb3aff86519965b

@DaddyWarbucks
Copy link
Contributor

You can also checkout this little demo app: https://test-feathers-client-joins.herokuapp.com/

@skinofstars
Copy link
Author

skinofstars commented Oct 26, 2021

Following on from this, I spent and afternoon writing up a service to use fastJoin with batch-loader. I was surprised to find I ended up with pages of code that made a giant IN statement. That might work for a small data set, but once you've got a table with more than a few hundred thousand rows, that's not gonna work great. Whilst I love the idea of mixing data sources, I think this isn't the hammer for all nails.

@DaddyWarbucks
Copy link
Contributor

DaddyWarbucks commented Oct 26, 2021

There are others here that can speak on the performance for larger datasets better than I can. I have had great performance for datasets that large, but I have never directly compared it to the equivalent Sequalize joins. But, as for the "pages of code", I can lend some advice. Give batch-loader v2 a try. The main goal of that rewrite is to be easier to use and require less manual setup. You need to install it from the v2 branch at the moment. I think it is 99% complete and will not change. I just haven't released it yet because I think we are wanting to release it at the same time as Feathers v5 and feathers-schema. The main thing it is lacking is updated docs. But the below should get you going. You can also read the source code, it is pretty basic code that is just lazily creating batchloaders for you.

// Package.json
"@feathers-plus/batch-loader": "https://github.com/feathersjs-ecosystem/batch-loader.git#v2",
// app.hooks.js
const { AppLoader } = require('@feathers-plus/batch-loader');

const initLoader = (context) => {
  if (Object.prototype.hasOwnProperty.call(context.params, 'loader')) {
    return context;
  }

  const loader = new AppLoader({ app: context.app });
  context.params.loader = loader;

  return context;
};

module.exports = {
  before: {
    all: [initLoader]
  }
}
//some resolvers/hooks file
const resolvers = {
  user: (post, context) => {
    // The appLoader is always available to you. No need to setup a
    // usersLoader here. appLoader lazily creates/memoizes loaders as
    // you call them
    // Also be sure to pass the loader along to get the best performance
    return context.params.loader.service('users').load({ id: post.user_id  }, { loader: context.params.loader })
  }
}

You can also checkout my video above using feathers v5 and AsyncLocalStorage for an even nicer way to setup loaders.

You may also want to checkout https://daddywarbucks.github.io/feathers-fletching/hooks.html#withresult, which is basically just a less verbose fastJoin, or checkout the upcoming feathers-schema. Both require a bit less code than fastJoin but are basically the same concept.

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

Successfully merging a pull request may close this issue.

7 participants