Feathers.js / Sequelize -> Service with relations between two models

半城伤御伤魂 提交于 2019-12-05 05:49:45
Ryan Wheale

Having helped a lot of people with this same issue, I have learned that the solution comes in two parts:

#1 - embrace the ORM
Most of people's problems come from a lack of understanding of sequelize. In order to help you, you will first need to have an understanding how sequelize associations work and how to perform queries using the "include" option (aka "eager loading"). I recommend reading all of the contents of those links a couple times, and then one more time for good measure; this is the steepest part of the sequelize learning curve. If you have never used an ORM, let it do a lot of the heavy lifting for you!

#2 - setting sequelize options from a feathers hook
Once you understand how the "include" option works with sequelize, you will want to set that option from a "before" hook in feathers. Feathers will pass the value of hook.params.sequelize as the options parameter for all sequelize method calls. This is what your hook might look like:

// GET /my-service?name=John&include=1
function (hook) {
   if (hook.params.query.include) {
      const AssociatedModel = hook.app.services.fooservice.Model;
      hook.params.sequelize = {
         include: [{ model: AssociatedModel }]
      };
      // delete any special query params so they are not used
      // in the WHERE clause in the db query.
      delete hook.params.query.include;
   }
   return Promise.resolve(hook);
}

Underneath the hood, feathers will call your models find method sort of like this:

// YourModel is a sequelize model
const options = Object.assign({ where: { name: 'John' }}, hook.params.sequelize);
YourModel.findAndCount(options);

Noteworthy:
The old v1.x feathers generators (before March 2017) do not generate code which is friendly for sequelize. This has been fixed in the new v2.x generators. If you are pretty far into your project from before March 2017, then do not use the new generators. Please join the Slack Channel and join the sequelize room for help. I keep an eye on things there and can help you. If you just started your project and haven't gotten very far, then I highly suggest starting fresh with the new generators. Run this command (and follow these instructions):

$ feathers --version              # see what version you are using
$ npm install -g @feathersjs/cli    # install latest version of the CLI

As of v4 of Sequelize, the classMethods property has been removed See this reference. This means that the example in @Edgar's answer will not work.

The associate method must be added directly to the model, rather than being wrapped in a classMethods property. From the Sequelize docs:

Previous:

const Model = sequelize.define('Model', {
    ...
}, {
    classMethods: {
        associate: function (model) {...}
    },
    instanceMethods: {
        someMethod: function () { ...}
    }
});

New:

const Model = sequelize.define('Model', {
    ...
});

// Class Method
Model.associate = function (models) {
    ...associate the models
};

// Instance Method
Model.prototype.someMethod = function () {..}

Ok I've done some adjusting of the code. To keep things readable for everyone, I'm going to step over to actual table examples. I have a table 'sections' and a table 'categories' (this is a more simple example). The section has a foreign key with the category. So this is what I've done for that so far:

category-model.js

classMethods: {
    associate() {
        category.hasOne(sequelize.models.sections, {
            as: 'category',
            foreignKey: 'category_id'
        });
    },
},

section-model.js

classMethods: {
    associate() {
        section.belongsTo(sequelize.models.categories, {
            allowNull: true
        });
    },
},

services\index.js

...
app.set('models', sequelize.models);
...
Object.keys(sequelize.models).forEach(function(modelName) {
    if ("associate" in sequelize.models[modelName]) {
        sequelize.models[modelName].associate();
    }
});

I'm using Jetbrains webstorm in this case. So I did a npm start, and my table now has the correct foreign key, so that part works. Also, the display of the data is still correct. The query which gets the sections still is working. If I had a different coding in the index.js, then npm start didn't fail, but the sections query failed.

Next is up: hooks. This is where I'm getting some confusion now. Some sites say it's in the 'find definition' (let's call it that for now, but this is in my vue component mounted section). Then you it's explained, well showed, the include is in that part, but it does nothing, well the code still runs, but I don't see category information when I get the sections through postman. I will then have e.g.

serviceSections.find({
    include: [{
        model: serviceCategories.Model
    }],
    query: {
        $sort: {
            section_description_en: 1
        }
    }
}).then(page => {
    page.data.reverse();
    this.listSections = page.data;
})

serviceCategories is defined as "appFeathers.service('categories');". As mentioned it does nothing. So jumping back to the explanation I had gotten here, it says '.. from a before hook ..'. I found the hooks\index.js file, for categories and for services. But here I make mistakes. I made this adjustment first in the categories on then in the sections one

exports.before = {
    ...
    find: [{
        function (hook) {
            if (hook.params.query.include) {
                const AssociatedModel = hook.app.services.category.Model;
                hook.params.sequelize = {
                    include: [{ model: AssociatedModel }]
                };
            }
            return Promise.resolve(hook);
        }
    }],
    ...
};

This is giving code 500 fn.bind error.

Just thought to post the progress, this doesn't mean I stop looking for the last step (or a missing step).

I forgot one thing. My check to see if it is complete is to open F12 in chrome, go to the VUE plugin and expand 'listSections' which is the result of "serviceSections.find". I expect to see category column in it but maybe that is the wrong expectation. I also don't see a 'join' in the select of my debugger

Edit some time later:

Okay, so I messed around. Eventually I also came across this post on how to retrieve data from many to many relationships. Reading this, I figured the intention on adjusting the "hooks\index.js" was correct, but that meant my code wasn't. So trying different combinations of that post, and the tips provided above, I now have this

section\hooks\index.js

...
exports.before = {
    all: [],
    find: [
        getCategory()
    ],
    get: [
        getCategory()
    ],
    create: [],
    update: [],
    patch: [],
    remove: []
};
...

function getCategory() {
    return function (hook) {
        const category = hook.app.services.categories.Model;
        hook.params.sequelize = {
            include: [{ model: category }]
        };
        return Promise.resolve(hook);
    };
}

This is working in postman with GET, because of putting it in the 'get' part of 'before' section, and it's working in VUE for I put the function in the 'find' part of 'before'.

Mutliple Joins

Okay, I needed multiple joins in to the 'sections'. I have a status too. This is coming from my metadata table. So this meant making the same association to sections in the metadata-model. I did it like this:

metadata-model.js

classMethods: {
    associate() {
        metadata.hasOne(sequelize.models.sections, {
            as: 'satus',
            foreignKey: 'status_id',
            targetKey: 'status_id'
        });
    }, },

Here I started to always put in the foreignKey attribute. The targetKey is the name of the column in the other tabel. Handy if you need to change it. The 'as' attribute is an alias, I like to use this nearly always at least when using it multiple times. In the section-model I made changes to.

section-model.js

classMethods: {
    associate() {
        section.belongsTo(sequelize.models.categories, {
            allowNull: false,
            as: 'category'
        });
        section.belongsTo(sequelize.models.metadata, {
            allowNull: false,
            as: 'status'
        });
    }, },

To finalize this, I changed the hooks function. I first tried too functions, but that didn't work, so I merged the two.

section\hooks\index.js

function getRelatedInfo() {
    return function (hook) {
        hook.params.sequelize = {
            include: [
                {
                    model: hook.app.services.categories.Model,
                    as: 'category'
                },{
                    model: hook.app.services.metadata.Model,
                    as: 'status',
                    where: {
                        type: 'status'
                    }
                }
            ]
        };
        return Promise.resolve(hook);
    };
}

As you see I changed the function name. It is important to use the alias again, it didn't work other wise. In the metadata part, I put a filter. I don't want to load the entire table when looking up. Okay, I join on an id so it really is one on one, but this way if for some reason the metadata entry changes to a different type, I still have the selection on 'status' and can make a warning of incorrect rows.

The final part would be if you want the inner or left outer joins. I triggered that by the 'required: true' attribute within the hooks sections.

VUE.js part

The last part is to push it to a vue component. I do that in the mounted section. I have this code for that.

const socket = io();
const appFeathers = feathers()
    .configure(feathers.socketio(socket))
    .configure(feathers.hooks());
const serviceSections = appFeathers.service('sections');
const serviceArticles = appFeathers.service('articles');
const serviceMetadata = appFeathers.service('metadata');
...
mounted() {
    serviceArticles.find({
        include: [{
            model: serviceMetadata.Model,
            as: 'country',
            query: {
                $select: [
                    'country_icon'
                ]
            }
        }],
        query: {
            $sort: {
                article_description_en: 1
            },
            $select: [
                'id',
                ['article_description_en', 'article_description'],
                'article_length',
                'article_ascend',
                'article_code'
            ]
        }
    }).then(page => {
        this.listTrails = page.data;
    })
}

What I do here is filter undesired columns from the array. I also rename a few. The '*_en' ones are multilingual, so I need to use a variable for it. The include is repeated again to get related columns from the joins.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!