-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
4.0 Model definition syntax #6524
Comments
So the attribute configuration is defined as an attribute |
The decorator proposal is in flux, so I can't say whether that will be a future ECMAScript standard. That's basically what mobx do right now, and it works well. In the meantime, this is how I'm handling loading models in v4. I'd appreciate your advice if you think this abuses the current API in any way. Model loader (influenced by Sequelize CLI 2.3; updated to match my environment) // Checks whether we have the right env vars set; exits if not
require('./env')('DB_NAME', 'DB_USER', 'DB_PASSWORD');
import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';
// Delay returns a promise that resolves after the number of
// milliseconds passed have elapsed. This is useful for 'suspending'
// an awaited statement inside of an async function (in other words -
// to simulate a pause)
import delay from 'delay';
// Path to look for model files
const modelPath = path.join(__dirname, 'db_models');
// Open a new Sequelize client
const sequelize = new Sequelize(
process.env.DB_NAME, // database name
process.env.DB_USER, // username
process.env.DB_PASSWORD, // password
{
host: 'percona', // points to a linked Docker host
dialect: 'mysql',
},
);
// Load each model file
export const models = Object.assign({}, ...fs.readdirSync(modelPath)
.filter(function (file) {
return (file.indexOf('.') !== 0) && (file.slice(-3) === '.js');
})
.map(function (file) {
const model = require(path.join(modelPath, file)).default;
return {
[model.name]: model.init(model.fields, {
sequelize,
}),
};
})
);
// Load model associations
for (const model of Object.keys(models)) {
typeof models[model].associate === 'function' && models[model].associate(models);
}
// Ping checks that the database server can be connected to. It attempts
// to connect every second for 60 seconds, before giving up and throwing
// an error back up to the chain. This function is especially useful when using
// Docker Compose with a linked DB, which may not always be ready before
// the API server starts
export async function ping() {
for (let i = 1; i <= 60; i++) {
try {
await sequelize.authenticate();
return;
} catch (e) {
console.log(`DB connection attempt #${i} failed, retrying...`);
await delay(1000);
}
}
throw new Error("Couldn't connect to database!");
} Sample Model definition (in this case, 'Session') export default class Session extends Model {
static fields = {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
},
browser: {
type: Sequelize.TEXT,
},
expiresAt: {
type: Sequelize.DATE,
defaultValue() {
return moment().add(30, 'days').toDate();
},
},
whatever: {
type: Sequelize.VIRTUAL,
get() {
return 'it worked!';
},
},
idAgain: {
type: Sequelize.VIRTUAL,
get() {
return this.get('id');
},
}
};
// Session middleware that Koa uses to create a `ctx.session` object
// for all subsequent middleware. (method appears at class-level and
// *not* on the instance
static middleware() {
// We'll return this as an async function, so that it can be invoked
// in the Koa config and bound to the currently loaded model
return async function (ctx, next) {
// ... omitted for brevity
}.bind(this);
}
// This function is instance-level... it'll bind to the active record
instanceFunction() {
return true;
}
} Invoking the above inside an async function seems to work perfectly... import * as db from './dbLoader';
(async function () {
// Wait for the DB to be ready
await db.ping();
// Start a new instance
const s = new db.models.Session;
// Make sure we've got what we expect...
console.log('s.id ->', s.id);
console.log('s.idAgain ->', s.idAgain);
console.log('s.middleware -> ', s.middleware);
console.log('s.fields ->', s.fields);
console.log('s.whatever ->', s.whatever);
console.log('s.instanceFunction() ->', s.instanceFunction());
// Plus the static method means I can wire up things that belong at
// the class-level, like middleware
const app = new Koa;
app.use(db.models.Session.middleware());
})(); ... which returns the expected output:
|
A few reasons why I chose the syntax in the above post.
|
See my previous comment. Yes, decorators are still a proposal, but the complete Angular 2 framework is already built on it and that is now at a release candidate, people already use this in production and it works very well. Please note that class properties can not be used in Node 4/6. It is a Stage 1 proposal, while decorators are already Stage 2. So the API you are proposing will not work without transpilation either, just like a decorator API. I think aestetically, a static property and a decorator are on the same level, but decorators fit the use case better semantically (you decorate the class with metadata) and they have the benefit of normalization, while the class property would result in unexpected behaviour and would still require a seperate
I agree.
This is already implemented in 4.0.0-1. The
I don't get what you are trying to say. Imo a model file should be as self-contained as possible. I actually even use late imports instead of the |
No arguments there. I'm already using decorators with mobx too, and I love them! I just couldn't find a better fit for wiring up field definitions than a static property on the class. A decorator function seemed like overkill.
What I'm saying is that defining field types within the body of the class seems like a better approach. In your example, you do this... class Pub extends Sequelize.Model { }
Pub.init({
name: Sequelize.STRING,
address: Sequelize.STRING,
latitude: {
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: null,
validate: { min: -90, max: 90 }
},
longitude: {
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: null,
validate: { min: -180, max: 180 }
}
}, {
sequelize,
validate: {
bothCoordsOrNone() {
if ((this.latitude === null) !== (this.longitude === null)) {
throw new Error('Require either both latitude and longitude or neither')
}
}
}
}) This part irks me... class Pub extends Sequelize.Model { }
Pub.init({
// ...
}) You've created a class to extend Then in a separate call, you initialise it with IMO, it looks better to keep them defined within the class. Whether it's a decorator or a static property is less relevant... more so is that syntax outside of the class makes it hard to reason about exactly what fields the model contains. I have 500+ lines of field definitions and boilerplate in some places... I'd rather keep that inside the class than stick it where my Just my personal preference. |
Yes, |
Another option is static method... they've been in Node since 4+ and don't require any transpilation at all: class Session extends Sequelize.Model {
static fields() {
return {
id: {
type: Sequelize.UUID,
// ....
}
}
}
} |
@felixfbecker Is the model name taken from the class name? In that case we need a way to override it because a lot of people generally have lower case model names but would probably have lint rules telling them to have uppercase class names |
@leebenson Yes, but imo having a method that always returns the same configuration object just because the language doesn't have property declarations yet is syntax abuse and DSLish. @mickhansen yes, the model name is the |
@felixfbecker re: lower casing, I'm currently doing it explicitly like this (in my model loader) import utils from 'sequelize/lib/utils';
// Load each model file
export const models = Object.assign({}, ...fs.readdirSync(modelPath)
.filter(function (file) {
return (file.indexOf('.') !== 0) && (file.slice(-3) === '.js');
})
.map(function (file) {
const model = require(path.join(modelPath, file)).default;
return {
[model.name]: model.init(
(typeof model.fields === 'function' && model.fields()) || {},
Object.assign({
sequelize,
tableName: utils.pluralize(model.name.toLowerCase()), // <--- this bit
},
(typeof model.attributes === 'function' && model.attributes()) || {},
),
),
};
}) With my session definitions now looking closer to this: export default class Session extends Model {
static fields() {
return {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
},
// ....
};
}
//.....
} So a I agree with you that a method returning the same static data isn't ideal. It's an alternative to requiring a transpilation step, though, and it's only run once. Plus, it's an opportunity to bind the current Hard to get that with a static class attribute or a decorator that's outside of the definition scope. |
Please note that
I am highly against this. In a static method, I expect
It's super simple to share the sequelize instance is to simply have a module that exports it, and require it in the model files. |
Definitely, just need to make sure it's possible to reconfigure |
I know, but in the absence of either being set explicitly, the pluralised version of class.name is used, right? I think this is what @mickhansen was referring to. It threw me off too. Hence the pluralize hack above.
Binding as a parameter. Not to So you can do something like... class Session extends Sequelize.Model {
static fields(sequelize, DataTypes) {
return {
id: {
type: DataTypes.UUID,
// ....
}
}
}
} I do use this elsewhere on my class, btw, for things like GraphQL... class Session extends Sequelize.Model {
// GraphQL public mutations
static mutations(db, types) {
// models = access to `db`, which contains db.models.*, db.sequelize, etc
// types = GraphQL types, defined outside of this class
return {
logout: {
type: types.Session,
async resolve(root, args, ctx) {
// ctx = current Koa context for the web user
await ctx.session.logout();
return ctx.session;
},
},
};
}
} It keeps model logic neatly in one place, without polluting any model instance. |
Actually, I've just realised that's exactly what you're already doing here with As much as I love decorators, I can't help but feel they're overkill in this scenario because they require explicit access to An example like this could just as easily be... export class User extends Model {
// For field definitions
static fields(sequelize, DataTypes) {
username: {
type: DataTypes.STRING,
primaryKey: true,
},
lastName: DataTypes.STRING,
firstName: DataTypes.STRING,
}
// For options
static options(sequelize) {
return {
tableName: 'someTotallyDifferentName',
};
}
// For asociations
static associate(sequelize) {
this.belongsTo(sequelize.Session);
}
// Instance-level methods...
get fullName() {
return this.firstName + ' ' + this.lastName;
}
set fullName(fullName) {
const names = fullName.split(' ');
this.lastName = names.pop();
this.firstName = names.join(' ');
}
} ... with:
|
In your example, Please note that import {Sequelize} from 'sequelize'
export default sequelize = new Sequelize(process.env.DB) User.js import {DataTypes} from 'sequelize'
import sequelize from '../db'
export class User {
...
}
User.init({ ... }, { sequelize })
import UserGroup from './UserGroup'
User.belongsTo(UserGroup) UserGroup.js import {DataTypes} from 'sequelize'
import sequelize from '../db'
export class UserGroup {
...
}
UserGroup.init({ ... }, { sequelize })
import User from './User'
UserGroup.hasMany(UserGroup) We save the attributes under Regarding decorators being overkill: We need to normalize attributes and options. For example,
Decorators and Having these static methods return configuration is what put me and a lot of developers I talked to off from many of the PHP ORMs. In PHP you have no other option because patterns like a class factory are not possible, but in JavaScript I would prefer the |
I understand that your example above is the way it works now. I provided alternate syntax because it doesn't seem like you're settled a pattern yet, and IMO, my way was cleaner. FWIW, here's a more complete example of what my import Sequelize, { Model } from 'sequelize';
export default class User extends Model {
/* FIELDS */
static fields() {
return {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false,
},
email: {
type: Sequelize.STRING,
allowNull: false,
},
password: {
type: Sequelize.STRING,
allowNull: false,
},
firstName: {
type: Sequelize.STRING,
allowNull: false,
},
lastName: {
type: Sequelize.STRING,
allowNull: false,
},
};
}
/* RELATIONSHIPS */
static associate(models) {
this.hasMany(models.Session);
}
/* CLASS-LEVEL FUNCTIONS */
// Create a new user
static create(args) {
// logic to create a user
}
/* GRAPHQL MUTATIONS */
static mutations(g, t) {
return {
// Create a new user
createUser: {
type: t.User,
args: {
email: {
type: g.GraphQLString,
},
firstName: {
type: g.GraphQLString,
},
lastName: {
type: g.GraphQLString,
},
password: {
type: g.GraphQLString,
},
},
async resolve(root, args) {
return create(args);
},
},
};
}
} There are a few things here, IMO, that are improved over the syntax I've seen so far:
user.js import { Model } from 'sequelize';
export class User extends Model {
static fields() {
// return field def...
}
static options() {
// return options...
}
fullName() {
return `${this.firstName} ${this.lastName}`;
}
} db.js import Sequelize from 'sequelize';
import UserModel from './user'
export const connection = new Sequelize(/* connection vars */);
connection.addModel(UserModel); app.js import { connection } from './db';
connection.models.User.findOne(/* search args */).then(user => {
// user = found user
}); Instead, a user has to:
I'm just providing friendly feedback, as a user of Sequelize since the very early days. In v4 you have a chance to drastically reduce boilerplate. I think what you currently have just adds noise. Why is it any better than defining Again, I'm brand new to v4, so maybe you've figured those kinds of things out and I'm way off the mark. Maybe you already have a model manager that wires stuff up. I'm still figuring out my way around v4. |
@leebenson I really appreciate your feedback, please don't misunderstand my critic. 🙂 You bring in interesting ideas and it's nice to hear from someone who actually uses v4 with ES6 syntax. I hope I didn't come up rude while being open with my thoughts 😅 Long post incoming.
Yes, you have an inversion of control. But your model is still coupled (dependent) on the sequelize instance. You just inverse the control over it.
Yes, that would be my dream API too. It would be nice if models did not know about their connection at all, you only add them to a model manager and either pass the connection to use to Your API definitely is a viable workaround, but please not ethat there are some things which don't make it suitable for the API:
Whatever API we settle on, I would like it to be forward compatible. It should be built in mind with JS features that will arrive in the future. For example, with the decorator API, users who use decorators now can do it, users who don't can use
I don't know what you mean with "you're relying on both model files being fully instantiated before exporting". The idea of a late import is based on the fact that the module is not complete yet, NodeJS has excellent support for this, even a dedicated documentation section highlighting this feature. In short:
I look at it this way: So I don't even try to avoid the circular dependency between model A and B, I embrace it. It is one by definition, because it is a relation, and a relation is symmetrical. This pattern of exporting a closure that is called with
As it stands right now, I guess we will likely keep the examples in the docs with
You hit the nail on the head here unfortunately. Currently none of the syntaxes that work with native Node 4/6 are a great improvement over
Out of interest, what do you mean by this? Are you using Sequelize in some way for an isomorphic app? |
Thanks! Now here comes an equally long post, I'm afraid... 🙂
Yup, it's still tightly coupled (the model and the connection have to meet at some point, after all), but I think it'd be useful for that logic to happen behind the scenes for most users. If... const sequelize = new Sequelize(/* connection */); ... contains the connection, then feeding models into it with something like
Understood. I haven't poked around the internals, so I don't know how you're currently mutating underlying classes/models to reflect options, or what you consider 'reserved words'. No doubt, my example can't be used literally. But the fact that there's namespace clashes is sort of my point. IMO, it's a better design choice if:
I totally understand, but I would counter in two ways:
My point is not that decorators are bad (they're not), just that relying on them to work in a certain way is shaky ground. Plus, are they really needed? Everything you can do with decorators, you can do just as easily with static methods and a good mobx make great use of decorators, and I'd argue their use are worth taking a meantime punt on makeshift syntax. However, I'd also argue they serve a valid purpose - using an There's definitely ways this could be made more interesting, I think. I use decorators in my own projects to provide "higher order" functions in React, mobx, etc. My decorators usually have bound 'state', so when I do something like this in my React classes... @connect class Button extends React.Component {
render() {
// @connect has provided this function with `this.context.state`...
}
} ... then I apologise if I haven't understand your current decorator implementation correctly, but it seems if they require an explicit
Node has great support, but I've always thought of circular dependencies as an anti-pattern. Maybe this stems from my work with Go or other languages, but I'd generally agree with the points in the above link for any language, for the same reasons.
If models rely on each other, I'd think it's better design to import them from a third, 'impartial' class (i.e. the
I think that's smart, but I also think alternatives exist today that can achieve the effects of decorators, allow model classes to be 'bare' per my criteria list above, and define models inside a connection without passing around
Yes. In one project, I have a common code base that employs:
I use webpack to create three separate bundles:
Sequelize is my main ORM in most projects, and this one too. |
Response will come tomorrow, but one thing just came to my mind: class User extends Model {
static init(sequelize) {
super.init({
// attributes
}, {
sequelize,
// options
})
}
} You can call this from your model index file, you have the declarations in the class, you dont run into any name collisions, and it is supported by 4.0.0-1. |
@felixfbecker that's a decent approach. It keeps things in one place without name clashes. Nicely done. I'll probably go for this approach instead of having multiple methods, at least until I know more about the internals not to inadvertently overwrite reserved words. I still think there's some interesting things that could be done to the model manager that won't require connection strings or special boilerplate. I think that would open up your use of decorators, too. Something along these lines: _Session.js model_ import { model, options, relationships }, DataTypes from 'sequelize'
@model({ id: DataTypes.STRING })
@options({ tableName: 'somethingElse'})
@relationships({ belongsTo: ['User'], hasMany: ['SessionData']})
export default class Session {
// Anything on the class = static method()
// Anything on the instance = method()
} _db.js_ import Sequelize from 'sequelize';
// Create a connectiom and export it
export default connection = new Sequelize(/* connection */)
/*
//
// THAT'S IT!
//
// We now have `connection.models` containing our registered
// models, without _ever_ having to explicitly pass in 'connection'
// to them.
//
// This is achieved by storing a copy of the model class in its
// bare form inside a `WeakMap`, and then instantiating at the point
// of:
//
// a) Creating a connection
//
// and/or
//
// b) Using the @model decorator, which loops through the current
// `connections` Set and wires it up to each connections `.model` property,
// tying up relationships, etc. Notes:
That's basically what I do with MobX and it works really well. You get to use decorators (yay!), but without the unnecessary boilerplate. As a side effect, you can also do things like this: const staging = new Sequelize(/* connection */);
const production = new Sequelize(/* connection */); ... and both connections have their own 'injected' models that are bound to that connection, meaning you can define them in one place, but be assured that they'll be scoped to the right |
Please note that no matter how you put it, currently a model needs to know about the connection. Eg I am 100% for connection-independent models, and then we could override the connection to use on every query, while the model registration is handled by the
Yes, I know it is considered an anti pattern for the named reasons, and I agree with the points. But all of these don't apply in this case:
You are talking a lot about avoiding "state" in your models. But at least in master, a model never has any real state. It is completely static. You need to initialize it with the static constructor, and then the attributes and connection are defined and set in stone (except if you use One can definitely feel the differences in our technology stacks ;) As a React guy you desire statelessness, immutability, reducers, pass-by-argument. As a TypeScript guy I desire type safety and editor autocompletion (+I can use property decorators).
Not a fan of magic string constants. No explicit dependency expressed here, and you cannot get any autocompletion for the string. This is what put me off from PHP ORMs too, for example in doctrine you configure the relationships the same way, in a docblock: /**
* @ManyToOne(targetEntity="Address")
* @JoinColumn(name="address_id", referencedColumnName="id")
*/
private $address; The unique ability of JavaScript to work with classes as objects and pass them around makes it perfect for avoiding these kinds of magic strings, and I really like our current association syntax. Sequelize is really unique compared to other ORMs here because the language is so flexible. I don't see it as boilerplate if I get autocompletion and type safety in turn.
That kind of goes against the semantic of decorator - the decorator should decorate a class with metadata (mutate it), not pass it to some higher instance. For this, I would prefer a
I think the whole discussion around connection-independent models should be taken to a new issue, but I don't see it coming soon, because I personally don't have a use case for multiple connections and internally it is an epic refactor. But going from the architecture, it would be nice if we could pass a connection at query-time and optionally set the default connection with
Thats exactly how it works with
That is not true, the |
Thanks @felixfbecker - I'm reading through your post, but I just wanted to clear something up...
That's not actually the case 😄 A model doesn't have to know about it connection at the time of definition. You can store the class in a You don't have to do that ahead of time. I say that from experience. I'm using decorators throughout my application that relies on per-request state (generated when a user visits my app), where things like local scoping become paramount. None of my decorators require state explicitly. They wire it up implicitly at the time the connection is defined. I'd highly encourage you to explore WeakMaps - they can solve this design problem very easily. |
When I say 'state', I mean it analogous to 'connection' - i.e. the That is, in effect, state. It's a connection handler that defines an active connection pool, DB, etc. That's what I mean by 'state'. Models themselves don't need state. They're just static definitions. My point is that you can inject connections into the models (via a higher-order class, like Your current design has two major flaws:
tl;dr - if you want decorators, use |
It is, internally. A |
But @felixfbecker, you can still have all of that without explicitly passing around All you need is a global 'cache' of models inside Sequelize. When you connect them with a decorator like @model({ id: DataTypes.STRING })
export class SomeTable {} ... then the Then, whenever Simple. Nothing much needs to change under the hood. Just some caching and looping whenever a new model is defined and/or a |
The reason the above works, of course, is that all exports in Node are cached. So in your Sequelize library, you'd have a single reference like... const modelConfig = new Weakmap;
const models = new Set; ... and whenever you use Then whenever you do Your That's exactly how I do it, and it works beautifully. |
I apologise if it came across rude, not my intention. I'm just trying to express, as clearly as possible, that if you want to use decorators, I really think If I had a bit more time right now, I'd hack together a demo to show you it working in practice. |
@felixfbecker Thanks for your reply, i used babel es2015-node6 sequelize worked. |
I'd asked about this in Slack so here's a viewpoint developed prior to this looking at this thread: https://gist.github.com/richthegeek/ffe78543c530f5bf20b3b7ae5d9d3350 Having read through the thread, there are a few things that aren't clear:
More generally it seems like the discussion is being kept quite forcefully away from an "ideal API" (which mine definitely isn't, but i'd say the primary features would be "minimal" and "almost native") and honing in (mostly pointlessly?) on either what would work without any changes to the Sequelize core or what would be 'CS-theoretical correct'. If I have to use a transpiler to use this style of model definition then that'd be a massive failure. |
@richthegeek |
@mickhansen ah yes so it will :) in which case the |
The point of decorators is to annotate functions with new features, without changing the signature of the original function. When there's just one of them, it might seem like overkill; but you can imagine a scenario where you have They start to make more sense when you have a collection of well-designed decorators that can easily annotate a function without changing the inner function's implementation detail. The way they're currently defined in Sequelize is, IMO, a bit redundant. You have to explicitly pass in the sequelize object, which contradicts some of the lightweight ease of 'decorating' a plain class/function. In this case, you might as well just define things statically. I feel it'd make more sense if decorators followed a pattern like I demoed here. Since writing that, I generally now do 95% of my DB work in SQLAlchemy for Python. It's not fair to compare it with Sequelize directly, because Python offers a bunch of first-class language features (decorators being one of them) that Node doesn't support natively (or at all). It can perform ORM gymnastics that simply wouldn't be possible in Javascript, due to the lack of operator overloading. With that said, their general implementation is a joy to work with. You can define a global 'Base' and then further define models by simply extending Base. This is somewhat analogous to my suggested approach with decorators; you could export a global 'base' and then every call to FWIW, if you're flexible on the back-end language, I'd highly encourage giving SQLAlchemy a look. The work that has gone into Sequelize is fabulous, but if you're not constrained to Node, there's no need to limit yourself to Javascript. Python's language features make an ORM an easy fit. |
@richthegeek I haven't followed the discussion too closely, defining models is something i do once and as long as it works i don't really care too much, the query API is a bigger surface area. @leebenson SQLAlchemy is definitely a beaut to work with. |
@leebenson I'm not sure that I understand this: "annotate a function without changing the inner function's implementation detail" The definition of the model is the core of the implementation. What do you actually mean by this? |
@richthegeek - a decorator takes the original function, and generally returns a modified version of that same function. So the point of Basically -- the responsibility is then handed off to Sequelize, rather than it being something that happens in userland by adding statics to classes, etc. Like I said, it's a subtle difference when there's only one decorator in play, but it makes sense when there's a few of them that hide away the implementation-- you can avoid adding 'noise' to your classes by requiring that, say, |
It's worth noting the class+decorator syntax has been implemented fully with Typescript. And usage is very straightfoward without a complicated init. Is that what you're looking for? It's not quite as fully featured as the ORM in python you mentioned, but it's something! :D (although it's also worth mentioning this is just using a transpiler, which has been mentioned before.) |
@leebenson one option is to spin this off into a separate library (e.g. sequelize-model-builder) that could hide some of the messiness with "init" rather than working to get this into Sequelize while annotations still require transpilation. It's something I've considered working on. We have a large number of model definitions and the approach you posted above is along the lines of the direction we'll like to take. |
For typescript users I think typeorm completely nailed their orm api. https://github.com/typeorm/typeorm. Well worth reviewing for inspiration. |
What exactly is the "messiness" with Both typeorm and sequelize-typescript use terrible patterns for associations that will plain out break with native ES6 modules in Node: @HasMany(() => User) this is so incredibly hacky, a cyclic dependency that only works because of TS compiler implementation details when transpiling to CommonJS. Why does a "TypeScript library" for Sequelize have so many lines of code?! |
re: "terrible patterns" Ough! Ouch. 🗡 💀 re: messiness However, if I wasn't constrained by communicating to an older team, I would rather just use sequelize plainly ('idiomatically' if you will). If I wanted to cut down on lines of code, the decorator syntax OR static init would be leaner than what I have now. None of what @felixfbecker or @leebenson have suggested above is messy, it's an improvement over what existed. If you disagree, provide code samples to try to demonstrate some friction in the way you define your models and the way you wish you could define them, and then discussion can proceed, but I'm not seeing anything messy (even as someone who prefers the class approach). Implementation asideAlso, if the friction is just a repeated init function for all your models, and you think it's violating SRP... you can factor that out in its own function, or perhaps have all your models inherit from the same prototype, or perhaps do a more oopy 'base class' dealio. The problem you'd run into is I'm willing to bet not all your init functions are created equal. There's some good reasons the sequelize team has it still. As it stands, Sequelize lets you choose how to do it. We are using JavaScript after all You could even create the model builder yourself as a specialized model factory if you really wanted to. If you want it, just do it! Then share it here and the sequelize team will use what they think is valuable, and keep in userland what they believe to be best left in userland. |
Except operator overloads, first-class decorator support, list comprehensions, transactions that don't get lost inside of Promise chains... 😛 |
sequelize come on! ! ! i won't change to another orm(typeorm),i hope decorator become standard library in sequelize code and doc! |
@felixfbecker This is not true. ES6 modules are indeed able to handle cyclic dependencies. So this is not hacky at all and has nothing to do with 'implementation details' of the tsc. There are some browser(chrome canary, safari technology preview) out there, which implements ES6 modules natively. If you run the following code snippets (which includes a cyclic dependency) in one of these browsers, you will see that it works very well. // a.js
import {b} from './b.js';
export function a() {
return b();
}
export function a1() {
return 'a1';
} // b.js
import {a1} from './a.js';
export function b() {
return 'b' + a1();
} // index.js
import {a} from './a.js';
console.log(a()); <script type="module" src="index.js"></script> |
After all night of tweaking, I finally have a working example, based on @leebenson and @felixfbecker's posts on this thread. @snewell92 take a look at this. @felixfbecker one needs to explicitly To use, put all 4 files in an empty directory and do @mickhansen In my example, using Node 6.11.3 LTS, I did not need a transpiler for index.js// models/index.js
/**
index.js is an import utility that grabs all models in the same folder,
and instantiate a Sequelize object once for all models (instead of for each model).
This is done by passing the single Sequelize object to each
model as a reference, which each model then piggy-backs (sequelize.define())
for creating a single db class model.
*/
"use strict"; // typical JS thing to enforce strict syntax
const fs = require("fs"); // file system for grabbing files
const path = require("path"); // better than '\/..\/' for portability
const Sequelize = require("sequelize"); // Sequelize is a constructor
const env = process.env.NODE_ENV || "development"; // use process environment
const config = require(path.join(__dirname, '..', 'config.js'))[env] // Use the .config.json file in the parent folder
const sequelize = new Sequelize(config.database, config.username, config.password, {
dialect: config.dialect,
});
// Load each model file
const models = Object.assign({}, ...fs.readdirSync(__dirname)
.filter(file =>
(file.indexOf(".") !== 0) && (file !== "index.js")
)
.map(function (file) {
const model = require(path.join(__dirname, file));
// console.log(model.init(sequelize).tableName)
return {
[model.name]: model.init(sequelize),
};
})
);
// Load model associations
for (const model of Object.keys(models)) {
typeof models[model].associate === 'function' && models[model].associate(models);
}
module.exports = models; Post.js// models/Post.js
/**
Post.js
Class model for Post
*/
'use strict';
const Sequelize = require('sequelize');
module.exports =
class Post extends Sequelize.Model {
static init(sequelize) {
return super.init({
title: {
type: Sequelize.STRING,
allowNull: false,
},
body: {
type: Sequelize.TEXT,
allowNull: false,
},
assets: {
type: Sequelize.JSON,
allowNull: true
},
}, { sequelize })
};
static associate(models) {
// Using additional options like CASCADE etc for demonstration
// Can also simply do Task.belongsTo(models.Post);
this.hasMany(models.Comment, {
onDelete: "CASCADE",
foreignKey: {
allowNull: false
}
});
// Using additional options like CASCADE etc for demonstration
// Can also simply do Task.belongsTo(models.Post);
this.belongsTo(models.User, {
onDelete: "CASCADE",
foreignKey: {
allowNull: false
}
});
}
} User.js// models/User.js
/**
User.js
Class model for User
*/
'use strict';
const Sequelize = require('sequelize');
module.exports =
class User extends Sequelize.Model {
static init(sequelize) {
return super.init({
firstName: {
type: Sequelize.STRING,
allowNull: false,
},
lastName: {
type: Sequelize.STRING,
allowNull: false,
},
username: {
type: Sequelize.STRING,
allowNull: false,
},
password_hash: {
type: Sequelize.STRING,
allowNull: false,
},
salt: {
type: Sequelize.STRING,
allowNull: false,
},
email: {
type: Sequelize.STRING,
allowNull: false,
validate: {
isEmail: true
}
},
isActive: {
type: Sequelize.BOOLEAN
}
}, { sequelize })
};
static associate(models) {
// Using additional options like CASCADE etc for demonstration
// Can also simply do Task.belongsTo(models.User);
this.hasMany(models.Post, {
onDelete: "CASCADE",
foreignKey: {
allowNull: false
}
});
// Using additional options like CASCADE etc for demonstration
// Can also simply do Task.belongsTo(models.User);
this.hasMany(models.Comment, {
onDelete: "CASCADE",
foreignKey: {
allowNull: false
}
});
}
} Comment.js// models/Comment.js
/**
Comment.js
Class model for Comment
*/
'use strict';
const Sequelize = require('sequelize');
module.exports =
class Comment extends Sequelize.Model {
static init(sequelize) {
return super.init({
title: {
type: Sequelize.STRING,
allowNull: false,
},
body: {
type: Sequelize.TEXT,
allowNull: false,
}
}, { sequelize })
};
static associate(models) {
// Using additional options like CASCADE etc for demonstration
// Can also simply do Task.belongsTo(models.Comment);
this.belongsTo(models.User, {
onDelete: "CASCADE",
foreignKey: {
allowNull: false
}
});
// Using additional options like CASCADE etc for demonstration
// Can also simply do Task.belongsTo(models.Comment);
this.belongsTo(models.Post, {
onDelete: "CASCADE",
foreignKey: {
allowNull: false
}
});
}
} |
Dang, neat! Related tidbitThis is not strictly related, but I do something similar for initializing services in feathers by using One thing I commented on for a future item, is that if any of my services depend on one another, the way I'm initializing my services won't work. However, the way sequelize splits out associating models together after the models are all initialized could apply to that domain too. And thus, as long as my services aren't using each other in the |
Great post and thanks @zhanwenchen for this very detailed example which helped me a lot ! However I'm still having issues figuring out how to define hooks, getters and setters in this case. I tried static methods, instance methods, direct definition like Would you mind to give an example of the right way to do it please ? |
@Sylv3r SUPER late to the game but I think you can add it to the class User extends Model {
static init(sequelize){
return super.init({
//definition
}, {
sequelize,
hooks: {
beforeCreate: User.beforeCreate,
afterCreate: User.afterCreate,
beforeUpdate: User.beforeUpdate
}
})
}
} Haven't tested this, still researching if this is an ok pattern to follow. (The whole |
Wow this helped me a lot, thanks a ton! |
Is there a reason why this approach is not documented anywhere in the documentation? Should I be using this or the normal .define way? |
Just to confirm, my previous model does indeed work and is in production. |
Anyone had success with instance methods? Can't make it work for some reason. https://stackoverflow.com/questions/52813078/sequelize-es6-model-methods-no-existing |
Because the dicussion is spread across many issues and PRs, I wanted to open a new issue to discuss the future syntax for model definition.
Currently, besides the old
define()
, this syntax is possible in v4.0.0-1:If you prefer to not do the initialization (which also attaches the connection to the model) inside the model file, but rather from a model index file, this is also possible:
With the help of a decorator library and Babel transpilation, this is also possible:
And with TypeScript you can go even further:
BUT we definitely do not want to require the usage of any transpiler. The
Model.init()
API feels a bit weird, especially how the sequelize connection is passed. Some thoughts:sequelize
optionsequelize.add(Model)
instead of passing the connection. That is not implemented atm though, currently the model needs to know about the connection.Please chime in and share your thoughts and ideas!
Issues/PRs for reference: #5898 #6439 #5877 #5924 #5205 #4728
The text was updated successfully, but these errors were encountered: