database-jones/Adapter/api/SessionFactory.js (424 lines of code) (raw):
/*
Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights
reserved.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; version 2 of
the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
02110-1301 USA
*/
"use strict";
var stats = {
"TableHandler" : {
"success" : 0,
"idempotent" : 0,
"cache_hit" : 0
},
"tables_created" : 0
};
var session = require("./Session.js"),
util = require("util"),
jones = require("database-jones"),
unified_debug = require("unified_debug"),
udebug = unified_debug.getLogger("SessionFactory.js"),
UserContext = require("./UserContext.js"),
meta = require("./Meta.js"),
TableMapping = require("./TableMapping.js"),
DBTableHandler = require(jones.common.DBTableHandler).DBTableHandler,
stats_module = require(jones.api.stats),
Db = require("./Db.js");
stats_module.register(stats, "api", "SessionFactory");
var SessionFactory = function(key, dbConnectionPool, properties, mappings, delete_callback) {
if (!dbConnectionPool) {
throw new Error("Fatal internal error; dbConnectionPool must not be null or undefined");
}
this.key = key;
this.dbConnectionPool = dbConnectionPool;
this.properties = properties;
this.mappings = mappings;
this.delete_callback = delete_callback;
this.sessions = [];
this.tableHandlers = {};
this.tableMetadatas = {};
this.tableMappings = {}; // mappings for tables
this.capabilities = dbConnectionPool.getCapabilities();
};
SessionFactory.prototype[util.inspect.custom] = function() {
var numberOfMappings = this.mappings? this.mappings.length: 0;
var numberOfSessions = this.sessions? this.sessions.length: 0;
return "[[API SessionFactory with key:" + this.key + ", " +
numberOfMappings + " mappings, " + numberOfSessions + " sessions.]]\n";
};
/** openSession(Object mappings, Function(Object error, Session session, ...) callback, ...);
* Open new session or get one from a pool.
* @param mappings a table name, mapped constructor, or array of them
* @return promise
*/
SessionFactory.prototype.openSession = function(mappings, callback) {
// if only one argument, it might be a mappings or a callback
var args = arguments;
if (arguments.length === 1 && !jones.isMappings(mappings)) {
args[1] = mappings;
args[0] = null;
}
var context = new UserContext.UserContext(args, 2, 2, null, this);
context.cacheTableHandlerInSession = true;
context.user_mappings = args[0];
// delegate to context for execution
if(udebug.is_detail()) {udebug.log_detail('SessionFactory.openSession with mappings ', context.user_mappings);}
return context.openSession();
};
/** Allocate a slot in the sessions array for a new session.
* If there are no empty slots, extend the
* sessions array. Assign a placeholder
* and return the index into the array.
*/
SessionFactory.prototype.allocateSessionSlot = function() {
// allocate a new session slot in sessions
var i;
for (i = 0; i < this.sessions.length; ++i) {
if (this.sessions[i] === null) {
break;
}
}
this.sessions[i] = {
'placeholder': true,
'index': i,
// dummy callback in case the session is closed prematurely
close: function(callback) {
callback();
}
};
return i;
};
/** Create table ("IF NOT EXISTS") for a table mapping.
* @param tableMapping
* @param callback
* @return promise
*/
SessionFactory.prototype.createTable = function(tableMapping, callback) {
var context = new UserContext.UserContext(arguments, 2, 1, null, this);
return context.createTable();
};
/** Drop table ("IF EXISTS")
* @param tableNameOrMapping
* @param callback
* @return promise
*/
SessionFactory.prototype.dropTable = function(tableNameOrMapping, callback) {
var context = new UserContext.UserContext(arguments, 2, 1, null, this);
return context.dropTable();
};
/** Drop and create table for a table mapping.
* @param tableMapping
* @param callback
* @return promise
*/
SessionFactory.prototype.dropAndCreateTable = function(tableMapping, callback) {
var context = new UserContext.UserContext(arguments, 2, 1, null, this);
return context.dropAndCreateTable();
};
// FIXME: close() should return a promise.
SessionFactory.prototype.close = function(user_callback) {
var self = this;
udebug.log('close for key', self.key, 'database', self.properties.database);
var i;
var tableKey;
var numberOfSessionsToClose = 0;
var closedSessions = 0;
function closeOnConnectionClose() {
if(typeof user_callback === 'function') {
udebug.log_detail('closeOnConnectionClose calling user_callback');
user_callback();
}
}
function closeConnection() {
if(udebug.is_detail()) {udebug.log_detail(
'closeConnection calling jones.delete_callback for key', self.key, 'database', self.properties.database);
}
self.delete_callback(self.key, self.properties.database, closeOnConnectionClose);
}
var onSessionClose = function(err) {
if (++closedSessions === numberOfSessionsToClose) {
closeConnection();
}
};
// SessionFactory.close starts here
// invalidate all table metadata objects
for (tableKey in this.tableMetadatas) {
if (this.tableMetadatas.hasOwnProperty(tableKey)) {
this.tableMetadatas[tableKey].invalidate();
}
}
// count the number of sessions to close
for (i = 0; i < self.sessions.length; ++i) {
if (self.sessions[i]) {
++numberOfSessionsToClose;
}
}
udebug.log('session factory', self.key, 'found', numberOfSessionsToClose, 'sessions to close.');
// if no sessions to close, go directly to close dbConnectionPool
if (numberOfSessionsToClose === 0) {
closeConnection();
}
// close the sessions
for (i = 0; i < self.sessions.length; ++i) {
if (self.sessions[i]) {
self.sessions[i].close(onSessionClose);
self.sessions[i] = null;
}
}
};
SessionFactory.prototype.closeSession = function(index, session) {
this.sessions[index] = null;
};
SessionFactory.prototype.getOpenSessions = function() {
var result = [];
var i;
for (i = 0; i < this.sessions.length; ++i) {
if (this.sessions[i]) {
result.push(this.sessions[i]);
}
}
return result;
};
SessionFactory.prototype.registerTypeConverter = function(type, converter) {
return this.dbConnectionPool.registerTypeConverter(type, converter);
};
/** Get a proxy for a db object similar to "easy to use" api.
*
* @param db_name optional database name to use
* @return db
*/
SessionFactory.prototype.db = function(db_name) {
return new Db(this, db_name);
};
/** Associate a table mapping with a table name. This is used for cases where users
* prefer to use their own table mapping and possibly specify forward mapping meta.
* This function is immediate.
*/
SessionFactory.prototype.mapTable = function(tableMapping) {
var database = tableMapping.database || this.properties.database;
var qualifiedTableName = database + '.' + tableMapping.table;
this.tableMappings[qualifiedTableName] = tableMapping;
udebug.log('mapTable', tableMapping, this.properties, qualifiedTableName);
};
/** Create a table mapping for the default case (id, sparse_fields)
*/
function createDefaultTableMapping(qualified_table_name) {
var tableMapping;
udebug.log('createDefaultTableMapping for', qualified_table_name);
tableMapping = new TableMapping.TableMapping(qualified_table_name);
tableMapping.mapField('id', meta.int(32).primaryKey().autoincrement());
tableMapping.mapSparseFields('SPARSE_FIELDS', meta.varchar(11111).sparseContainer());
return tableMapping;
}
/** Create a struct containing database name, unqualified, and qualified table name
*/
function getTableSpecification(defaultDatabaseName, tableName) {
var split = tableName.split(".");
var result = {};
if (split.length == 2) {
result.dbName = split[0];
result.unqualifiedTableName = split[1];
result.qualifiedTableName = tableName;
} else {
// if split.length is not 1 then this error will be caught later
result.dbName = defaultDatabaseName;
result.unqualifiedTableName = tableName;
result.qualifiedTableName = defaultDatabaseName + '.' + tableName;
}
udebug.log_detail('getTableSpecification for', defaultDatabaseName, ',', tableName, 'returned', result);
return result;
}
/** Create a table based on the table mapping, which might be user-specified or default mapping
* This function must not be used by applications.
*/
function createTableInternal(tableMapping, sessionFactory, session, callback) {
var connectionPool = sessionFactory.dbConnectionPool;
function createTableOnTableCreated(err) {
if (err) {
callback(err);
} else {
stats.tables_created++;
callback();
}
}
// start of createTableInternal
udebug.log('createTableInternal with tableMapping:', tableMapping);
connectionPool.createTable(tableMapping, session, createTableOnTableCreated);
}
/** Get the table handler for a table name, constructor, or domain object.
* This function is used internally by most user-visible functions on Session.
* Table handler merges table mapping with table metadata from database.
* The passed userContext is used to store tableSpecification and constructor
* between execution of asynchronous functions.
* The algorithm depends on the type of the first user argument:
* - Table Name: check session factory for cached table handler. if cached, return it.
* if table handler is not cached, get metadata for table;
* if exists, create table handler
* if table does not exist, check session factory for cached table metadata.
* if cached table metadata, create the table.
* if no cached table metadata, and session.allowCreateUnmappedTable, create the table.
* otherwise, error.
* - Constructor: check constructor for table handler in constructor.prototype.jones.dbTableHandler
* if table handler is cached, return it.
* if no table handler, check for table mapping
* if no user-specified table mapping, use default table mapping and continue
* if user-specified table mapping, create DBTableHandler and cache it in the constructor
* otherwise, error.
* - Domain Object:
* get constructor from domain object prototype. goto constructor algorithm.
* - TableMapping:
* get table name from mapping. get metadata for table. get DBTableHandler for mapping.
*/
SessionFactory.prototype.getTableHandler = function(userContext, domainObjectTableNameOrConstructor, session, onTableHandler) {
var sessionFactory = this;
var dbTableHandler;
var tableSpecification;
var tableMetadata;
var tableMapping;
var err;
var constructor;
var tableIndicatorType;
var databaseDotTable;
var constructorJones;
var tableKey;
function onExistingTableMetadata(err, tableMetadata) {
tableSpecification = userContext.tableSpecification;
constructor = userContext.handlerCtor;
var invalidateCallback;
tableKey = tableSpecification.qualifiedTableName;
if(udebug.is_detail()) {
udebug.log_detail('onExistingTableMetadata for ', tableSpecification.qualifiedTableName + ' with err: ' + err);
}
if (err) {
onTableHandler(err, null);
} else {
// check to see if the metadata has already been cached
if (sessionFactory.tableMetadatas[tableKey] === undefined) {
// put the table metadata into the table metadata map
sessionFactory.tableMetadatas[tableKey] = tableMetadata;
invalidateCallback = function() {
// use " = undefined" here to keep tableKey in the tableMetadatas object
udebug.log('invalidateCallback called for session factory table metadata for', tableKey);
sessionFactory.tableMetadatas[tableKey] = undefined;
};
tableMetadata.registerInvalidateCallback(invalidateCallback);
}
// we have the table metadata; now create the default table handler if not cached
// do not use the existing cached table handler if processing a new table mapping
if (userContext.tableIndicatorType === 'tablemapping' ||
(session.tableHandlers[tableKey] === undefined &&
sessionFactory.tableHandlers[tableKey] === undefined)) {
if(udebug.is_detail()) { udebug.log_detail('creating the default table handler for ', tableKey); }
dbTableHandler = new DBTableHandler(tableMetadata, tableMapping);
if (dbTableHandler.isValid) {
// cache the table handler for the table name and table mapping cases
if (userContext.cacheTableHandlerInSessionFactory) {
udebug.log('caching the default table handler in the session factory for', tableKey);
sessionFactory.tableHandlers[tableKey] = dbTableHandler;
invalidateCallback = function() {
// use " = undefined" here to keep tableKey in the tableHandlers object
udebug.log('invalidateCallback called for session factory default table handlers for', tableKey);
sessionFactory.tableHandlers[tableKey] = undefined;
};
tableMetadata.registerInvalidateCallback(invalidateCallback);
}
if (userContext.cacheTableHandlerInSession) {
udebug.log('caching the default table handler in the session for', tableKey);
session.tableHandlers[tableKey] = dbTableHandler;
invalidateCallback = function() {
// use " = undefined" here to keep tableKey in the tableHandlers object
udebug.log('invalidateCallback called for session default table handlers for', tableKey);
session.tableHandlers[tableKey] = undefined;
};
tableMetadata.registerInvalidateCallback(invalidateCallback);
}
} else {
err = new Error(dbTableHandler.errorMessages);
udebug.log('onExistingTableMetadata got invalid dbTableHandler', dbTableHandler.errorMessages);
}
} else {
dbTableHandler = session.tableHandlers[tableKey] || sessionFactory.tableHandlers[tableKey];
udebug.log('onExistingTableMetadata got default dbTableHandler but' +
' someone else put it in the cache first for ', tableKey);
}
if (constructor) {
constructorJones = constructor.prototype.jones;
if (constructorJones === undefined) {
onTableHandler(new Error('Internal error: constructor.prototype.jones is undefined.'));
return;
}
dbTableHandler = constructorJones.dbTableHandler;
if (dbTableHandler === undefined) {
// if a domain object mapping, cache the table handler in the prototype
tableMapping = constructorJones.mapping;
dbTableHandler = new DBTableHandler(tableMetadata, tableMapping, constructor);
if (dbTableHandler.isValid) {
stats.TableHandler.success++;
constructorJones.dbTableHandler = dbTableHandler;
invalidateCallback = function() {
if(udebug.is_detail()) {
udebug.log_detail('invalidateCallback called for constructor', constructor.name,
'for table', tableMetadata.database+'.'+tableMetadata.name);
constructorJones.dbTableHandler = null;
}
};
tableMetadata.registerInvalidateCallback(invalidateCallback);
if(udebug.is_detail()) {
udebug.log('caching the table handler in the prototype for constructor.');
}
} else {
err = new Error(dbTableHandler.errorMessages);
if(udebug.is_detail()) { udebug.log_detail('got invalid dbTableHandler', dbTableHandler.errorMessages); }
}
} else {
stats.TableHandler.idempotent++;
if(udebug.is_detail()) {
if(udebug.is_detail()) { udebug.log_detail('got dbTableHandler but someone else put it in the prototype first.'); }
}
}
}
userContext.handlerCtor = undefined;
onTableHandler(err, dbTableHandler);
}
}
function onCreateTable(err) {
if (err) {
onExistingTableMetadata(err, null);
} else {
sessionFactory.dbConnectionPool.getTableMetadata(tableSpecification.dbName,
tableSpecification.unqualifiedTableName, session.dbSession, onExistingTableMetadata);
}
}
function onTableMetadata(err, tableMetadata) {
if (err) {
// get default tableMapping if not already specified
tableMapping = tableMapping || sessionFactory.tableMappings[tableSpecification.qualifiedTableName];
// create the schema if it does not already exist and user flag allows it
if (!tableMapping && session.allowCreateUnmappedTable) {
udebug.log('getTableHandler.onTableMetadata creating table for',tableSpecification.qualifiedTableName);
// create the table from the default table mapping
tableMapping = createDefaultTableMapping(tableSpecification.qualifiedTableName);
// cache the default tableMapping
sessionFactory.tableMappings[tableSpecification.qualifiedTableName] = tableMapping;
}
if (tableMapping) {
createTableInternal(tableMapping, sessionFactory, session, onCreateTable);
return;
}
}
onExistingTableMetadata(err, tableMetadata);
}
function createTableHandler(tableSpecification, dbSession, onTableHandler) {
userContext.tableSpecification = tableSpecification;
// first get the table metadata from the cache of table metadatas in session factory
tableMetadata = sessionFactory.tableMetadatas[tableSpecification.qualifiedTableName];
if (tableMetadata) {
// we already have cached the table metadata
onExistingTableMetadata(null, tableMetadata);
} else {
// get the table metadata from the db connection pool
// getTableMetadata(dbSession, databaseName, tableName, callback(error, DBTable));
if(udebug.is_detail()) {
udebug.log_detail('getTableHandler.createTableHandler did not find cached tableMetadata for',
tableSpecification.qualifiedTableName);
}
sessionFactory.dbConnectionPool.getTableMetadata(
tableSpecification.dbName, tableSpecification.unqualifiedTableName, dbSession, onTableMetadata);
}
}
// handle the case where the parameter is the (possibly unqualified) table name
function tableIndicatorTypeString() {
if(udebug.is_detail()) { udebug.log_detail('tableIndicatorTypeString for table', domainObjectTableNameOrConstructor); }
tableSpecification = getTableSpecification(sessionFactory.properties.database, domainObjectTableNameOrConstructor);
tableKey = tableSpecification.qualifiedTableName;
// look up in table name to default table handler hash in session and session factory
dbTableHandler = session.tableHandlers[tableKey] || sessionFactory.tableHandlers[tableKey];
if (dbTableHandler === undefined) {
if(udebug.is_detail()) {
udebug.log_detail('tableIndicatorTypeString for table name did not find cached dbTableHandler for table', tableKey);
}
// create a new table handler for the table
createTableHandler(tableSpecification, session.dbSession, onTableHandler);
} else {
stats.TableHandler.cache_hit++;
if(udebug.is_detail()) {udebug.log_detail(
'getTableHandler for table name found cached dbTableHandler for table', tableKey);
}
// send back the dbTableHandler to the caller
onTableHandler(null, dbTableHandler);
}
}
// handle the case where the parameter is the user-defined TableMapping
function tableIndicatorTypeTableMapping() {
if(udebug.is_detail()) { udebug.log_detail('tableIndicatorTypeTableMapping for', domainObjectTableNameOrConstructor); }
tableMapping = domainObjectTableNameOrConstructor;
if (tableMapping.database) {
databaseDotTable = tableMapping.database + '.' + tableMapping.table;
} else {
databaseDotTable = tableMapping.table;
}
tableSpecification = getTableSpecification(sessionFactory.properties.database, databaseDotTable);
tableKey = tableSpecification.qualifiedTableName;
// create a new table handler for the table with the user-defined TableHandler
createTableHandler(tableSpecification, session.dbSession, onTableHandler);
}
function tableIndicatorTypeFunction() {
constructorJones = userContext.handlerCtor.prototype.jones;
// parameter is a constructor; it must have been annotated already
if (constructorJones === undefined) {
err = new Error('User exception: constructor for ' + userContext.handlerCtor.name +
' must have been annotated (call TableMapping.applyToClass).');
onTableHandler(err, null);
} else {
dbTableHandler = constructorJones.dbTableHandler;
if (dbTableHandler === undefined) {
// create the dbTableHandler from the mapping in the constructor
if (!constructorJones.mapping.isValid()) {
udebug.log('tableIndicatorTypeFunction found invalid table mapping:', constructorJones.mapping.error);
err = new Error(constructorJones.mapping.error);
onTableHandler(err);
return;
}
databaseDotTable = constructorJones.mapping.database ?
constructorJones.mapping.database + '.' + constructorJones.mapping.table :
constructorJones.mapping.table;
tableSpecification = getTableSpecification(sessionFactory.properties.database, databaseDotTable);
createTableHandler(tableSpecification, session.dbSession, onTableHandler);
} else {
stats.TableHandler.cache_hit++;
if(udebug.is_detail()) {
udebug.log('tableIndicatorTypeFunction found cached dbTableHandler in constructor:', dbTableHandler); }
userContext.handlerCtor = undefined;
onTableHandler(null, dbTableHandler);
}
}
}
// start of getTableHandler
tableIndicatorType = typeof domainObjectTableNameOrConstructor;
if (tableIndicatorType === 'object' && domainObjectTableNameOrConstructor.constructor.name === 'TableMapping') {
tableIndicatorType = 'tablemapping';
}
userContext.tableIndicatorType = tableIndicatorType;
if (tableIndicatorType === 'string') {
userContext.handlerCtor = undefined;
tableIndicatorTypeString();
} else if (tableIndicatorType === 'function') {
userContext.handlerCtor = domainObjectTableNameOrConstructor;
tableIndicatorTypeFunction();
} else if (tableIndicatorType === 'object') {
userContext.handlerCtor = domainObjectTableNameOrConstructor.constructor;
tableIndicatorTypeFunction();
} else if (tableIndicatorType === 'tablemapping') {
userContext.handlerCtor = undefined;
tableIndicatorTypeTableMapping();
} else {
err = new Error('User error: parameter must be a domain object, string, TableMapping or constructor function.');
onTableHandler(err, null);
}
};
SessionFactory.prototype.userSessionFactory = function() {
var sf = this;
return {
tableMetadatas: sf.tableMetadatas,
database: sf.database,
key: sf.key,
db: function() {return sf.db.apply(sf,arguments);},
close: function() {return sf.close.apply(sf, arguments);},
mapTable: function() {return sf.mapTable.apply(sf, arguments);},
getTableMetadata: function() {return sf.getTableMetadata.apply(sf, arguments);},
openSession: function() {return sf.openSession.apply(sf, arguments);},
registerTypeConverter: function() {return sf.registerTypeConverter.apply(sf, arguments);}
};
};
exports.SessionFactory = SessionFactory;