-
Notifications
You must be signed in to change notification settings - Fork 0
ConnectionBinding
Connection binding is the process of binding a connection to a model. By default when you create a Mythix ORM connection, it will bind the connection to the models provided, unless you pass the option { bindModels: false } to the connection when you create it.
Connection binding works by setting a static _mythixBoundConnection = connection property onto the model class itself. This however comes with its own side-effects, not the least of which is modifying your class directly. While this generally isn't an issue, it may become an issue if you wish to use more than one connection in your application, and it can be a very big issue during unit testing.
Mythix ORM works this way because while architecting Mythix ORM, it was decided that there would be no use (or minimal use) of globals, and that any application can and should be able to use multiple connections simultaneously.
Because of this design decision, it becomes difficult to provide a needed connection to each model. The solution to this problem is to bind the connection used for your application to each model class directly via a static property on the model class itself, or to provide a connection via other means (discussed below).
Mythix ORM will only allow you to bind a connection to a model class once. Attempting to bind a connection more than once to a specific model will throw an exception (unless you specify { forceConnectionBinding: true } as an option to the connection when you create your connection... which you should only ever do if you know exactly what you are doing).
This can cause very big issues for you when you go to write unit tests for your application. Generally unit tests will run randomly, or in parallel. Because of this, it is often desired to have a connection instance per-test-suite. However, as you may have guessed, since the default connection binding scheme works by modifying your model classes directly, and attempting to bind more than once will throw an exception, this will cause big problems when your model spans unit tests, but connections do not span unit tests, and still need to be bound to the model somehow.
Because of the issues connecting binding can cause, Mythix ORM allows the user to opt-out of connection binding if they choose to do so. All you need to do is pass a { bindModels: false } option to your connection when you create it. When you do this, Mythix ORM will no longer modify your model class, and hence won't bind any connection to your models.
...but, now where does Mythix ORM look to find a connection?
By default, Mythix ORM will generally always call the model method Model.getConnection. This method will search for a connection in the following order, and will return the first valid connection found:
- The provided
connectionvia the arguments to the call (if provided). - The connection provided to the model instance itself (if any was provided) via
model.connection. A connection can be provided to a model as the "options" when you create a model, i.e.new MyModel(attributes, { connection }). - Finally, if both of those fail, Model.getConnection will call Model._getConnection to attempt to find a connection that way.
Model._getConnection is a static per-model "global helper" method for finding the connection for a model. By default, it will search for a connection in the following order, and will return the first valid connection found:
- The provided
connectionvia the arguments to the call (if provided). - The connection on the AsyncLocalStorage "model context", if any exists.
- The
connectionproperty directly on the AsyncLocalStorage, if any exists (which is set by transactions). - Finally, it will see if the
static _mythixBoundConnectionproperty on the class is a valid connection, and if so, return that.
The best solution to connection binding issues, especially in unit tests, is to simply wrap your tests in a Connection.createContext call. Connection.createContext will create a connection context using AsyncLocalStorage, that will be available for every method inside your callback to Connection.createContext.
Using this method, your unit test connection can opt-out of connection binding to the models by passing a { bindModels: false } to your test connection. Then, when you wrap all your tests with a Connection.createContext call, the connection will be provided to all models and all operations in your application that uses your models via the AsyncLocalStorage context.
This can be fairly easily done in most test runners by simply hijacking the it or test methods. For example:
// The following methods should probably
// be included from some test-helper file
/* global it, fit */
const _it = it;
const _fit = fit;
// Hijack the "it" method, wrapping the test
// in a `connection.createContext` call.
function createIT(func, getConnection) {
return function it(desc, runner) {
return func.call(this, desc, async () => {
await getConnection().createContext(runner);
});
};
}
function createFIT(func, getConnection) {
return function fit(desc, runner) {
return func.call(this, desc, async () => {
await getConnection().createContext(runner);
});
};
}
function createRunners(getConnection) {
return {
it: createIT(_it, getConnection),
fit: createFIT(_fit, getConnection),
};
}
const models = require('./my-models');
describe('MyModelTest', () => {
let connection;
// Hijack "it" and "fit" so that each test is
// wrapped in a `connection.createContext` call
const { it, fit } = createRunners(() => connection);
beforeAll(async () => {
connection = createTestConnection({
bindModels: false,
models: models,
});
await connection.createTables(models);
});
afterEach(async () => {
await connection.dropTables(models);
});
it('can create a model', async () => {
let myModel = await models.MyModel.create({ someAttribute: 'test' });
expect(myModel.someAttribute).toEqual('test');
});
});As you can see, this way of solving the connection binding problem can easily solve the issues by providing a connection to all your model and application code via an AsyncLocalStorage context.
Another method that will give okay results (as long as you are careful how you write your code) is to subclass all your models, and bind the connection on a subclass.
This can solve the problem entirely if you write your code correctly, but caution needs to be taken on how you use models in your application. You must always call Connection.getModel or Connection.getModels everywhere throughout your code for every single model class you interact with. The benefit to this painstaking endeavour is that at least model auto-reloading will work flawlessly for you 😊.
Let's see an example of how this might work:
let { MyModel: _MyModel } = require('./my-models);
describe('MyModelTest', () => {
let connection;
let models;
beforeAll(async () => {
// Bind the connection by subclassing your model
class MyModel extends _MyModel {
static getConnection() {
return connection;
}
}
connection = createTestConnection({
bindModels: false,
models: [
MyModel,
],
});
// Now we need to ensure we always use our
// subclass instead of the imported model class.
models = connection.getModels();
await connection.createTables(models);
});
afterEach(async () => {
await connection.dropTables(models);
});
it('can create a model', async () => {
// now all this code will work...
// AS LONG AS _all_ your application code
// also uses `connection.getModels` so that
// your application always gets the
// subclassed model
let myModel = await models.MyModel.create({ someAttribute: 'test' });
expect(myModel.someAttribute).toEqual('test');
});
});This example is incomplete, because in the real-world you would also need to provide this same test connection to your application code... but I will leave that challenge up to you to figure out 🙂.
Another solution is just to provide a truly global connection to your models. This might be a wee bit difficult to figure out, especially for unit testing, but can still be accomplished. To make this work, simply overwrite the static _getConnection method on all your models, and return the global connection. This is generally best done by providing a base model class that all your other models inherit from.
For example:
const globalApplicationConnection = require('./global-connection');
class ModelBase {
static _getConnection() {
return globalApplicationConnection;
}
}
class MyModel extends ModelBase {
...
}Keep in mind the possible issues this might cause, especially when unit testing. By sharing a global connection across tests, you might end up with data in your database that you don't expect from other tests that are running in parallel. If you go the route of a global connection, it is highly recommended that you request your test runner to run tests serially, instead of randomly, or in parallel.
The final and most tedious solution to the connection binding problem is to simply always supply every model you create with a connection.
For example:
let myModel = new MyModel(attributes, { connection });
However, you also need to supply a connection to the query engine for every query you create:
let query = MyModel.where(connection).something.EQ('test');
And you may be required to provided a connection in other circumstances as well.
- Generally, Mythix ORM will call Model.getConnection first (if possible), before calling the lower-level Model._getConnection. This will allow the connection to be provided by the model instance itself.
- When Model.getConnection is not available (for example, in a context where there is no instance of a model), then Model._getConnection will be called directly to fetch a connection.
- The static property
_mythixBoundConnectionon a model class is where Mythix ORM will finally attempt to locate a connection. - An AsyncLocalStorage context can always be used, anywhere in your code, to provide all code beneath a connection. This can be accomplished by calling Connection.createContext and providing it an async callback.
- Lastly, you can manually supply connections to your models when you create your models, and manually supply your connection elsewhere that it is needed, such as to queries.
- Relational fields, such as ForeignKeyType, ModelType, and ModelsType require a connection, or things will explode. Most other model operations will attempt to do their best without a connection, but these field types will throw exceptions if no connection is available.
- Associations
- Certifications
- Connection Binding
- Home
- Models
- Queries
- TypeScript
- Types Reference
-
namespace AsyncStore
- function getContextStore
- function getContextValue
- function runInContext
- function setContextValue
-
namespace Helpers
- function checkDefaultValueFlags
- function defaultValueFlags
- function getDefaultValueFlags
- property FLAG_LITERAL
- property FLAG_ON_INITIALIZE
- property FLAG_ON_INSERT
- property FLAG_ON_STORE
- property FLAG_ON_UPDATE
- property FLAG_REMOTE
-
namespace MiscUtils
- function collect
- function valueToDateTime
-
namespace ModelUtils
- function parseQualifiedName
-
namespace QueryUtils
- function generateQueryFromFilter
- function mergeFields
- function parseFilterFieldAndOperator
-
class AverageLiteral
- method static isAggregate
- method toString
-
class BigIntType
- property Default
- method castToType
- method constructor
- method isValidValue
- method static getDisplayName
- method toString
-
class BlobType
- method castToType
- method constructor
- method isValidValue
- method static getDisplayName
- method toString
-
class BooleanType
- method castToType
- method isValidValue
- method static getDisplayName
- method toString
-
class CacheKey
- method constructor
- method valueOf
-
class CharType
- method castToType
- method isValidValue
- method static getDisplayName
- method toString
-
class ConnectionBase
- property _isMythixConnection
- property DefaultQueryGenerator
- property dialect
- property Literals
- method _averageLiteralToString
- method _bigintTypeToString
- method _blobTypeToString
- method _booleanTypeToString
- method _charTypeToString
- method _countLiteralToString
- method _datetimeTypeToString
- method _dateTypeToString
- method _distinctLiteralToString
- method _escape
- method _escapeID
- method _fieldLiteralToString
- method _getFromModelCache
- method _integerTypeToString
- method _maxLiteralToString
- method _minLiteralToString
- method _numericTypeToString
- method _realTypeToString
- method _setToModelCache
- method _stringTypeToString
- method _sumLiteralToString
- method _textTypeToString
- method _uuidV1TypeToString
- method _uuidV3TypeToString
- method _uuidV4TypeToString
- method _uuidV5TypeToString
- method _xidTypeToString
- method addColumn
- method addIndex
- method aggregate
- method alterColumn
- method alterTable
- method average
- method buildConnectionContext
- method bulkModelOperation
- method constructor
- method convertDateToDBTime
- method count
- method createContext
- method createQueryGenerator
- method createTable
- method createTables
- method destroy
- method destroyModels
- method dirtyFieldHelper
- method dropColumn
- method dropIndex
- method dropTable
- method dropTables
- method ensureAllModelsAreInstances
- method escape
- method escapeID
- method exists
- method finalizeQuery
- method findModelField
- method getContextValue
- method getDefaultFieldValue
- method getDefaultOrder
- method getField
- method getLockMode
- method getModel
- method getModels
- method getOptions
- method getQueryEngineClass
- method getQueryGenerator
- method insert
- method isStarted
- method literalToString
- method max
- method min
- method parseQualifiedName
- method pluck
- method prepareAllModelsAndSubModelsForOperation
- method prepareAllModelsForOperation
- method query
- method registerModel
- method registerModels
- method runSaveHooks
- method select
- method setContextValue
- method setPersisted
- method setQueryGenerator
- method splitModelAndSubModels
- method stackAssign
- method start
- method static getLiteralClassByName
- method static isConnection
- method static isConnectionClass
- method static Literal
- method stop
- method sum
- method toQueryEngine
- method transaction
- method truncate
- method typeToString
- method update
- method updateAll
- method upsert
-
class CountLiteral
- method static isAggregate
- method static isFieldRequired
- method toString
-
class DateTimeType
- property Default
- method castToType
- method constructor
- method deserialize
- method isValidValue
- method serialize
- method static getDisplayName
- method toString
-
class DateType
- property Default
- method castToType
- method constructor
- method deserialize
- method isValidValue
- method serialize
- method static getDisplayName
- method toString
-
class DistinctLiteral
- method toString
-
class Field
- property _isMythixField
- property allowNull
- property defaultValue
- property fieldName
- property get
- property index
- property primaryKey
- property set
- property type
- property unique
- property validate
- method clone
- method constructor
- method setModel
- method static isField
- method static isFieldClass
-
class FieldLiteral
- method toString
- class FieldScope
-
class ForeignKeyType
- method castToType
- method constructor
- method getOptions
- method getTargetField
- method getTargetFieldName
- method getTargetModel
- method getTargetModelName
- method initialize
- method isValidValue
- method parseOptionsAndCheckForErrors
- method static getDisplayName
- method static isForeignKey
- method toString
-
class IntegerType
- property Default
- method castToType
- method constructor
- method isValidValue
- method static getDisplayName
- method toString
-
class Literal
- method constructor
-
class LiteralBase
- property _isMythixLiteral
- method constructor
- method definitionToField
- method fullyQualifiedNameToDefinition
- method static isAggregate
- method static isLiteral
- method static isLiteralClass
- method static isLiteralType
- method toString
- method valueOf
-
class LiteralFieldBase
- method constructor
- method getField
- method getFullyQualifiedFieldName
- method static isFieldRequired
- method valueOf
-
class MaxLiteral
- method static isAggregate
- method toString
-
class MinLiteral
- method static isAggregate
- method toString
-
class Model
- property _isMythixModel
- method _castFieldValue
- method _constructField
- method _constructFields
- method _constructor
- method _getConnection
- method _getDirtyFields
- method _getFieldValue
- method _initializeFieldData
- method _initializeModelData
- method _setFieldValue
- method clearDirty
- method constructor
- method destroy
- method getAttributes
- method getConnection
- method getDataValue
- method getDirtyFields
- method getOptions
- method hasValidPrimaryKey
- method isDirty
- method isPersisted
- method onAfterCreate
- method onAfterSave
- method onAfterUpdate
- method onBeforeCreate
- method onBeforeSave
- method onBeforeUpdate
- method onValidate
- method reload
- method save
- method setAttributes
- method setDataValue
- method static _getConnection
- method static all
- method static bindConnection
- method static count
- method static create
- method static cursor
- method static defaultScope
- method static finalizeQuery
- method static first
- method static getConcreteFieldCount
- method static getContextValue
- method static getField
- method static getFields
- method static getForeignKeyFieldsMap
- method static getForeignKeysTargetField
- method static getForeignKeysTargetFieldNames
- method static getForeignKeysTargetModelNames
- method static getForeignKeysTargetModels
- method static getModel
- method static getModelContext
- method static getModelName
- method static getPluralModelName
- method static getPrimaryKeyField
- method static getPrimaryKeyFieldName
- method static getQueryEngine
- method static getQueryEngineClass
- method static getSingularName
- method static getSortedFields
- method static getTableName
- method static getUnscopedQueryEngine
- method static getWhereWithConnection
- method static hasField
- method static hasRemoteFieldValues
- method static initializeFields
- method static isForeignKeyTargetModel
- method static isModel
- method static isModelClass
- method static iterateFields
- method static last
- method static mergeFields
- method static pluck
- method static primaryKeyHasRemoteValue
- method static setContextValue
- method static toString
- method static updateModelContext
- method toJSON
- method toString
- method updateDirtyID
-
class ModelScope
- method _getField
- method AND
- method CROSS_JOIN
- method DISTINCT
- method EXISTS
- method Field
- method FULL_JOIN
- method GROUP_BY
- method HAVING
- method INNER_JOIN
- method JOIN
- method LEFT_JOIN
- method LIMIT
- method mergeFields
- method NOT
- method OFFSET
- method OR
- method ORDER
- method PROJECT
- method RIGHT_JOIN
-
class ModelType
- method fieldNameToOperationName
- method initialize
-
class ModelsType
- method fieldNameToOperationName
- method initialize
-
class NumericType
- method castToType
- method constructor
- method isValidValue
- method static getDisplayName
- method toString
-
class ProxyClass
- property APPLY
- property AUTO_CALL
- property AUTO_CALL_CALLED
- property AUTO_CALL_CALLER
- property CALLABLE
- property CONSTRUCT
- property DEFINE_PROPERTY
- property DELETE_PROPERTY
- property GET
- property GET_OWN_PROPERTY_DESCRIPTOR
- property GET_PROTOTYPEOF
- property HAS
- property IS_EXTENSIBLE
- property MISSING
- property OWN_KEYS
- property PREVENT_EXTENSIONS
- property PROXY
- property SELF
- property SET
- property SET_PROTOTYPEOF
- property shouldSkipProxy
- property TARGET
- method ___autoCall
- method ___call
- method constructor
-
class QueryEngine
- method all
- method average
- method constructor
- method count
- method cursor
- method destroy
- method exists
- method finalizeQuery
- method first
- method getFieldScopeClass
- method getModelScopeClass
- method last
- method max
- method MERGE
- method min
- method Model
- method pluck
- method sum
- method toString
- method unscoped
- method updateAll
-
class QueryEngineBase
- method _fetchScope
- method _inheritContext
- method _newFieldScope
- method _newModelScope
- method _newQueryEngineScope
- method _pushOperationOntoStack
- method clone
- method constructor
- method filter
- method getAllModelsUsedInQuery
- method getConnection
- method getFieldScopeClass
- method getModel
- method getModelScopeClass
- method getOperationContext
- method getOperationStack
- method getQueryEngineClass
- method getQueryEngineScope
- method getQueryEngineScopeClass
- method getQueryID
- method isLastOperationCondition
- method isLastOperationControl
- method isModelUsedInQuery
- method logQueryOperations
- method map
- method queryHasConditions
- method queryHasJoins
- method static generateID
- method static getQueryOperationInfo
- method static isQuery
- method static isQueryOperationContext
- method walk
-
class QueryGeneratorBase
- method _averageLiteralToString
- method _countLiteralToString
- method _distinctLiteralToString
- method _fieldLiteralToString
- method _maxLiteralToString
- method _minLiteralToString
- method _sumLiteralToString
- method constructor
- method escape
- method escapeID
- method getConnection
- method getFieldDefaultValue
- method getIndexFieldsFromFieldIndex
- method setConnection
- method stackAssign
- method toConnectionString
-
class RealType
- method castToType
- method constructor
- method isValidValue
- method static getDisplayName
- method toString
-
class SerializedType
- method castToType
- method constructor
- method deserialize
- method getOptions
- method initialize
- method isDirty
- method isValidValue
- method onSetFieldValue
- method serialize
- method static getDisplayName
- method toString
-
class StringType
- method castToType
- method constructor
- method isValidValue
- method static getDisplayName
- method toString
-
class SumLiteral
- method static isAggregate
- method toString
-
class TextType
- method castToType
- method constructor
- method isValidValue
- method static getDisplayName
- method toString
-
class Type
- property _isMythixFieldType
- property clone
- method castToType
- method clone
- method constructor
- method deserialize
- method exposeToModel
- method getDisplayName
- method getField
- method getModel
- method initialize
- method isDirty
- method isForeignKey
- method isRelational
- method isRemote
- method isValidValue
- method isVirtual
- method onSetFieldValue
- method serialize
- method setField
- method setModel
- method static instantiateType
- method static isSameType
- method static isType
- method static isTypeClass
- method static wrapConstructor
- method toConnectionType
-
class UUIDV1Type
- property Default
- method castToType
- method getArgsForUUID
- method isValidValue
- method static getDisplayName
- method validateOptions
-
class UUIDV3Type
- property Default
- method castToType
- method getArgsForUUID
- method isValidValue
- method static getDisplayName
- method validateOptions
-
class UUIDV4Type
- property Default
- method castToType
- method getArgsForUUID
- method isValidValue
- method static getDisplayName
- method validateOptions
-
class UUIDV5Type
- property Default
- method castToType
- method getArgsForUUID
- method isValidValue
- method static getDisplayName
- method validateOptions
-
class XIDType
- property Default
- method castToType
- method isValidValue
- method static getDisplayName