I've got feathers.js functioning with mysql via sequelize. This is working, I can collect data from tables. Next step is to define 'joins' in the models.
I have a table with a column 'status_id' and 'country_id'. These columns reference to an id in a metadata table. In SQL I would right:
SELECT status.description, country.description, detail
FROM details
INNER JOIN metadata status
ON (details.status_id = status.id AND status.type = 'status'
INNER JOIN metadata country
ON (details.country_id =country.id AND country.type = 'country')
This metadata table won't be big in this case so hence this approach. It does give flexibility I need.
What do I need to do to make this in feathters.js?
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.
来源:https://stackoverflow.com/questions/42841810/feathers-js-sequelize-service-with-relations-between-two-models