Support upsert

This commit is contained in:
Anatoliy Chakkaev 2012-03-22 23:46:16 +04:00
parent ff65e3a7ad
commit c06f28f433
8 changed files with 222 additions and 11 deletions

View File

@ -167,6 +167,30 @@ AbstractClass.create = function (data, callback) {
} }
}; };
AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
var Model = this;
if (!data.id) return this.create(data, callback);
if (this.schema.adapter.updateOrCreate) {
this.schema.adapter.updateOrCreate(Model.modelName, data, function (err, data) {
var obj = data ? new Model(data) : null;
if (obj) {
addToCache(Model, obj);
}
callback(err, obj);
});
} else {
this.find(data.id, function (err, inst) {
if (err) return callback(err);
if (inst) {
inst.updateAttributes(data, callback);
} else {
var obj = new Model(data);
obj.save(data, callback);
}
});
}
};
AbstractClass.exists = function exists(id, cb) { AbstractClass.exists = function exists(id, cb) {
if (id) { if (id) {
this.schema.adapter.exists(this.modelName, id, cb); this.schema.adapter.exists(this.modelName, id, cb);
@ -250,9 +274,7 @@ function substractDirtyAttributes(object, data) {
AbstractClass.destroyAll = function destroyAll(cb) { AbstractClass.destroyAll = function destroyAll(cb) {
this.schema.adapter.destroyAll(this.modelName, function (err) { this.schema.adapter.destroyAll(this.modelName, function (err) {
if (!err) {
clearCache(this); clearCache(this);
}
cb(err); cb(err);
}.bind(this)); }.bind(this));
}; };
@ -424,7 +446,7 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
} }
done.call(inst, function () { done.call(inst, function () {
saveDone.call(inst, function () { saveDone.call(inst, function () {
cb(err); cb(err, inst);
}); });
}); });
}); });

View File

@ -17,15 +17,29 @@ Memory.prototype.define = function defineModel(descr) {
}; };
Memory.prototype.create = function create(model, data, callback) { Memory.prototype.create = function create(model, data, callback) {
var id = this.ids[model]++; var id = data.id || this.ids[model]++;
data.id = id; data.id = id;
this.cache[model][id] = data; this.cache[model][id] = data;
callback(null, id); callback(null, id);
}; };
Memory.prototype.updateOrCreate = function (model, data, callback) {
var mem = this;
this.exists(model, data.id, function (err, exists) {
if (exists) {
mem.save(model, data, callback);
} else {
mem.create(model, data, function (err, id) {
data.id = id;
callback(err, data);
});
}
});
};
Memory.prototype.save = function save(model, data, callback) { Memory.prototype.save = function save(model, data, callback) {
this.cache[model][data.id] = data; this.cache[model][data.id] = data;
callback(); callback(null, data);
}; };
Memory.prototype.exists = function exists(model, id, callback) { Memory.prototype.exists = function exists(model, id, callback) {
@ -151,6 +165,7 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c
}; };
function merge(base, update) { function merge(base, update) {
if (!base) return update;
Object.keys(update).forEach(function (key) { Object.keys(update).forEach(function (key) {
base[key] = update[key]; base[key] = update[key];
}); });

View File

@ -81,6 +81,29 @@ MongoDB.prototype.find = function find(model, id, callback) {
}); });
}; };
MongoDB.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
var adapter = this;
if (!data.id) return this.create(data, callback);
this.find(model, data.id, function (err, inst) {
if (err) return callback(err);
if (inst) {
adapter.updateAttributes(model, data.id, data, callback);
} else {
delete data.id;
adapter.create(model, data, function (err, id) {
if (err) return callback(err);
if (id) {
data.id = id;
delete data._id;
callback(null, data);
} else{
callback(null, null); // wtf?
}
});
}
});
};
MongoDB.prototype.destroy = function destroy(model, id, callback) { MongoDB.prototype.destroy = function destroy(model, id, callback) {
this.collection(model).remove({_id: new ObjectID(id)}, callback); this.collection(model).remove({_id: new ObjectID(id)}, callback);
}; };

View File

@ -15,13 +15,24 @@ exports.initialize = function initializeSchema(schema, callback) {
port: s.port || 3306, port: s.port || 3306,
user: s.username, user: s.username,
password: s.password, password: s.password,
database: s.database,
debug: s.debug debug: s.debug
}); });
schema.adapter = new MySQL(schema.client); schema.adapter = new MySQL(schema.client);
schema.adapter.schema = schema;
// schema.client.query('SET TIME_ZONE = "+04:00"', callback); // schema.client.query('SET TIME_ZONE = "+04:00"', callback);
process.nextTick(callback); schema.client.query('USE ' + s.database, function (err) {
if (err && err.message.match(/^unknown database/i)) {
var dbName = s.database;
schema.client.query('CREATE DATABASE ' + dbName, function (error) {
if (!error) {
callback();
} else {
throw error;
}
});
} else callback();
});
}; };
function MySQL(client) { function MySQL(client) {
@ -32,10 +43,27 @@ function MySQL(client) {
require('util').inherits(MySQL, BaseSQL); require('util').inherits(MySQL, BaseSQL);
MySQL.prototype.query = function (sql, callback) { MySQL.prototype.query = function (sql, callback) {
if (!this.schema.connected) {
return this.schema.on('connected', function () {
this.query(sql, callback);
}.bind(this));
}
var client = this.client;
var time = Date.now(); var time = Date.now();
var log = this.log; var log = this.log;
if (typeof callback !== 'function') throw new Error('callback should be a function'); if (typeof callback !== 'function') throw new Error('callback should be a function');
this.client.query(sql, function (err, data) { this.client.query(sql, function (err, data) {
if (err && err.message.match(/^unknown database/i)) {
var dbName = err.message.match(/^unknown database '(.*?)'/i)[1];
client.query('CREATE DATABASE ' + dbName, function (error) {
if (!error) {
client.query(sql, callback);
} else {
callback(err);
}
});
return;
}
if (log) log(sql, time); if (log) log(sql, time);
callback(err, data); callback(err, data);
}); });
@ -57,6 +85,40 @@ MySQL.prototype.create = function (model, data, callback) {
}); });
}; };
MySQL.prototype.updateOrCreate = function (model, data, callback) {
var mysql = this;
var fieldsNames = [];
var fieldValues = [];
var combined = [];
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
if (props[key] || key === 'id') {
var k = '`' + key + '`';
var v;
if (key !== 'id') {
v = mysql.toDatabase(props[key], data[key]);
} else {
v = data[key];
}
fieldsNames.push(k);
fieldValues.push(v);
if (key !== 'id') combined.push(k + ' = ' + v);
}
});
var sql = 'INSERT INTO ' + this.tableEscaped(model);
sql += ' (' + fieldsNames.join(', ') + ')';
sql += ' VALUES (' + fieldValues.join(', ') + ')';
sql += ' ON DUPLICATE KEY UPDATE ' + combined.join(', ');
this.query(sql, function (err, info) {
if (!err && info && info.insertId) {
data.id = info.insertId;
}
callback(err, data);
});
};
MySQL.prototype.toFields = function (model, data) { MySQL.prototype.toFields = function (model, data) {
var fields = []; var fields = [];
var props = this._models[model].properties; var props = this._models[model].properties;

View File

@ -207,7 +207,7 @@ Neo4j.prototype.save = function save(model, data, callback) {
if (err) return callback(err); if (err) return callback(err);
self.updateIndexes(model, node, function (err) { self.updateIndexes(model, node, function (err) {
if (err) return console.log(err); if (err) return console.log(err);
callback(null); callback(null, node.data);
}); });
}); });
}); });

View File

@ -55,7 +55,7 @@ PG.prototype.query = function (sql, callback) {
* Must invoke callback(err, id) * Must invoke callback(err, id)
*/ */
PG.prototype.create = function (model, data, callback) { PG.prototype.create = function (model, data, callback) {
var fields = this.toFields(model, data,true); var fields = this.toFields(model, data, true);
var sql = 'INSERT INTO ' + this.tableEscaped(model) + ''; var sql = 'INSERT INTO ' + this.tableEscaped(model) + '';
if (fields) { if (fields) {
sql += ' ' + fields; sql += ' ' + fields;
@ -69,6 +69,43 @@ PG.prototype.create = function (model, data, callback) {
}); });
}; };
PG.prototype.updateOrCreate = function (model, data, callback) {
var pg = this;
var fieldsNames = [];
var fieldValues = [];
var combined = [];
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
if (props[key] || key === 'id') {
var k = '"' + key + '"';
var v;
if (key !== 'id') {
v = pg.toDatabase(props[key], data[key]);
} else {
v = data[key];
}
fieldsNames.push(k);
fieldValues.push(v);
if (key !== 'id') combined.push(k + ' = ' + v);
}
});
var sql = 'UPDATE ' + this.tableEscaped(model);
sql += ' SET ' + combined + ' WHERE id = ' + data.id + ';';
sql += ' INSERT INTO ' + this.tableEscaped(model);
sql += ' (' + fieldsNames.join(', ') + ')';
sql += ' SELECT ' + fieldValues.join(', ')
sql += ' WHERE NOT EXISTS (SELECT 1 FROM ' + this.tableEscaped(model);
sql += ' WHERE id = ' + data.id + ') RETURNING id';
this.query(sql, function (err, info) {
if (!err && info && info[0] && info[0].id) {
data.id = info[0].id;
}
callback(err, data);
});
};
PG.prototype.toFields = function (model, data, forCreate) { PG.prototype.toFields = function (model, data, forCreate) {
var fields = []; var fields = [];
var props = this._models[model].properties; var props = this._models[model].properties;

View File

@ -90,6 +90,24 @@ SQLite3.prototype.create = function (model, data, callback) {
}); });
}; };
SQLite3.prototype.updateOrCreate = function (model, data, callback) {
data = data || {};
var questions = [];
var values = Object.keys(data).map(function (key) {
questions.push('?');
return data[key];
});
var sql = 'INSERT OR REPLACE INTO ' + this.tableEscaped(model) + ' (' + Object.keys(data).join(',') + ') VALUES ('
sql += questions.join(',');
sql += ')';
this.command(sql, values, function (err) {
if (!err && this) {
data.id = this.lastID;
}
callback(err, data);
});
};
SQLite3.prototype.toFields = function (model, data) { SQLite3.prototype.toFields = function (model, data) {
var fields = []; var fields = [];
var props = this._models[model].properties; var props = this._models[model].properties;

View File

@ -716,6 +716,40 @@ function testOrm(schema) {
}); });
}); });
if (schema.name !== 'mongoose' && schema.name !== 'neo4j')
it('should update or create record', function (test) {
var newData = {
id: 1,
title: 'New title (really new)',
content: 'Some example content (updated)'
};
Post.updateOrCreate(newData, function (err, updatedPost) {
if (err) throw err;
test.ok(updatedPost);
if (!updatedPost) throw Error('No post!');
test.equal(newData.id, updatedPost.toObject().id);
test.equal(newData.title, updatedPost.toObject().title);
test.equal(newData.content, updatedPost.toObject().content);
Post.find(updatedPost.id, function (err, post) {
if (err) throw err;
if (!post) throw Error('No post!');
test.equal(newData.id, post.toObject().id);
test.equal(newData.title, post.toObject().title);
test.equal(newData.content, post.toObject().content);
Post.updateOrCreate({id: 100001, title: 'hey'}, function (err, post) {
if (schema.name !== 'mongodb') test.equal(post.id, 100001);
test.equal(post.title, 'hey');
Post.find(post.id, function (err, post) {
if (!post) throw Error('No post!');
test.done();
});
});
});
});
});
it('all tests done', function (test) { it('all tests done', function (test) {
test.done(); test.done();
process.nextTick(allTestsDone); process.nextTick(allTestsDone);