[IMP] mail: slightly improve performance of model manager
closes odoo/odoo#98672 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -216,7 +216,7 @@ export class ModelField {
|
||||
computeRelated(record) {
|
||||
const [relationName, relatedFieldName] = this.related.split('.');
|
||||
const model = record.constructor;
|
||||
const relationField = model.__fieldMap[relationName];
|
||||
const relationField = model.__fieldMap.get(relationName);
|
||||
if (relationField.relationType === 'many') {
|
||||
const newVal = [];
|
||||
for (const otherRecord of record[relationName]) {
|
||||
@@ -428,7 +428,7 @@ export class ModelField {
|
||||
* @returns {any}
|
||||
*/
|
||||
read(record) {
|
||||
return record.__values[this.fieldName];
|
||||
return record.__values.get(this.fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -492,17 +492,15 @@ export class ModelField {
|
||||
*/
|
||||
_insertOtherRecord(record, data, options) {
|
||||
const otherModel = record.models[this.to];
|
||||
const otherField = otherModel.__fieldMap[this.inverse];
|
||||
const otherField = otherModel.__fieldMap.get(this.inverse);
|
||||
const isMulti = typeof data[Symbol.iterator] === 'function';
|
||||
const dataList = [];
|
||||
for (const recordData of (isMulti ? data : [data])) {
|
||||
const recordData2 = { ...recordData };
|
||||
const dataList = isMulti ? data : [data];
|
||||
for (const recordData of dataList) {
|
||||
if (otherField.relationType === 'one') {
|
||||
recordData2[this.inverse] = record;
|
||||
recordData[this.inverse] = record;
|
||||
} else {
|
||||
recordData2[this.inverse] = link(record);
|
||||
recordData[this.inverse] = link(record);
|
||||
}
|
||||
dataList.push(recordData2);
|
||||
}
|
||||
const records = record.modelManager._insert(otherModel, dataList, options);
|
||||
return isMulti ? records : records[0];
|
||||
@@ -521,7 +519,7 @@ export class ModelField {
|
||||
if (currentValue === newVal) {
|
||||
return false;
|
||||
}
|
||||
record.__values[this.fieldName] = newVal;
|
||||
record.__values.set(this.fieldName, newVal);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -655,7 +653,7 @@ export class ModelField {
|
||||
// unlink to properly update previous inverse before linking new value
|
||||
this._setRelationUnlinkX2One(record, { hasToUpdateInverse: true });
|
||||
// link other record to current record
|
||||
record.__values[this.fieldName] = recordToLink;
|
||||
record.__values.set(this.fieldName, recordToLink);
|
||||
// link current record to other record
|
||||
if (hasToUpdateInverse) {
|
||||
record.modelManager._update(
|
||||
@@ -821,7 +819,7 @@ export class ModelField {
|
||||
return false;
|
||||
}
|
||||
// unlink other record from current record
|
||||
record.__values[this.fieldName] = undefined;
|
||||
record.__values.set(this.fieldName, undefined);
|
||||
// unlink current record from other record
|
||||
if (hasToUpdateInverse) {
|
||||
if (!otherRecord.exists()) {
|
||||
|
||||
@@ -160,7 +160,7 @@ export class ModelManager {
|
||||
const entry = this._listenersObservingAllByModel.get(model);
|
||||
const info = {
|
||||
listener,
|
||||
reason: `all() - ${model}`,
|
||||
reason: this.isDebug && `all() - ${model}`,
|
||||
};
|
||||
if (entry.has(listener)) {
|
||||
entry.get(listener).push(info);
|
||||
@@ -224,14 +224,14 @@ export class ModelManager {
|
||||
* @returns {Record|undefined}
|
||||
*/
|
||||
findFromIdentifyingData(model, data = {}) {
|
||||
const data2 = { ...data };
|
||||
for (const fieldName of model.__identifyingFieldNames) {
|
||||
const field = model.__fieldMap[fieldName];
|
||||
if (data2[field.fieldName] === undefined && field.default !== undefined) {
|
||||
data2[field.fieldName] = field.default;
|
||||
const field = model.__fieldMap.get(fieldName);
|
||||
if (data[field.fieldName] === undefined && field.default !== undefined) {
|
||||
data[field.fieldName] = field.default;
|
||||
}
|
||||
}
|
||||
const record = model.__recordsIndex.findRecord(this._preInsertIdentifyingFieldsFromData(model, data2));
|
||||
this._preInsertIdentifyingFieldsFromData(model, data);
|
||||
const record = model.__recordsIndex.findRecord(data);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export class ModelManager {
|
||||
const entry = this._listenersObservingRecord.get(record);
|
||||
const info = {
|
||||
listener,
|
||||
reason: `findFromIdentifyingData record - ${record}`,
|
||||
reason: this.isDebug && `findFromIdentifyingData record - ${record}`,
|
||||
};
|
||||
if (entry.has(listener)) {
|
||||
entry.get(listener).push(info);
|
||||
@@ -592,7 +592,7 @@ export class ModelManager {
|
||||
if (!relatedModel) {
|
||||
throw new Error(`${field} on ${model} defines a relation to model(${field.to}), but there is no model registered with this name.`);
|
||||
}
|
||||
const inverseField = relatedModel.__fieldMap[field.inverse];
|
||||
const inverseField = relatedModel.__fieldMap.get(field.inverse);
|
||||
if (!inverseField) {
|
||||
throw new Error(`${field} on ${model} defines its inverse as field(${field.inverse}) on ${relatedModel}, but it does not exist.`);
|
||||
}
|
||||
@@ -604,7 +604,7 @@ export class ModelManager {
|
||||
}
|
||||
}
|
||||
for (const identifyingField of model.__identifyingFieldNames) {
|
||||
const field = model.__fieldMap[identifyingField];
|
||||
const field = model.__fieldMap.get(identifyingField);
|
||||
if (!field) {
|
||||
throw new Error(`Identifying field "${identifyingField}" is not a field on ${model}.`);
|
||||
}
|
||||
@@ -613,7 +613,7 @@ export class ModelManager {
|
||||
throw new Error(`Identifying field "${identifyingField}" on ${model} has a relation of type "${field.relationType}" but identifying field is only supported for "one".`);
|
||||
}
|
||||
const relatedModel = this.models[field.to];
|
||||
const inverseField = relatedModel.__fieldMap[field.inverse];
|
||||
const inverseField = relatedModel.__fieldMap.get(field.inverse);
|
||||
if (!inverseField.isCausal) {
|
||||
throw new Error(`Identifying field "${identifyingField}" on ${model} has an inverse "${field.inverse}" not declared as "isCausal" on ${relatedModel}.`);
|
||||
}
|
||||
@@ -641,13 +641,13 @@ export class ModelManager {
|
||||
// change in dependencies of compute, related and "on change".
|
||||
__listeners: [],
|
||||
// Field values of record.
|
||||
__values: {},
|
||||
__values: new Map(),
|
||||
[IS_RECORD]: true,
|
||||
});
|
||||
const record = owl.markRaw(!this.isDebug ? nonProxyRecord : new Proxy(nonProxyRecord, {
|
||||
get: function getFromProxy(record, prop) {
|
||||
if (
|
||||
!model.__fieldMap[prop] &&
|
||||
!model.__fieldMap.has(prop) &&
|
||||
!['_super', 'then', 'localId'].includes(prop) &&
|
||||
typeof prop !== 'symbol' &&
|
||||
!(prop in record)
|
||||
@@ -659,10 +659,9 @@ export class ModelManager {
|
||||
}));
|
||||
// Ensure X2many relations are Set initially (other fields can stay undefined).
|
||||
for (const field of model.__fieldList) {
|
||||
record.__values[field.fieldName] = undefined;
|
||||
if (field.fieldType === 'relation') {
|
||||
if (field.relationType === 'many') {
|
||||
record.__values[field.fieldName] = new RelationSet(record, field);
|
||||
record.__values.set(field.fieldName, new RelationSet(record, field));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,9 +669,6 @@ export class ModelManager {
|
||||
this._listenersObservingRecord.set(record, new Map());
|
||||
}
|
||||
this._listenersObservingFieldOfRecord.set(record, new Map());
|
||||
for (const field of model.__fieldList) {
|
||||
this._listenersObservingFieldOfRecord.get(record).set(field, new Map());
|
||||
}
|
||||
/**
|
||||
* Register record.
|
||||
*/
|
||||
@@ -694,14 +690,14 @@ export class ModelManager {
|
||||
for (const [listener, infoList] of this._listenersObservingAllByModel.get(model)) {
|
||||
this._markListenerToNotify(listener, {
|
||||
listener,
|
||||
reason: `_create: allByModel - ${record}`,
|
||||
reason: this.isDebug && `_create: allByModel - ${record}`,
|
||||
infoList,
|
||||
});
|
||||
}
|
||||
for (const [listener, infoList] of this._listenersObservingRecord.get(record)) {
|
||||
this._markListenerToNotify(listener, {
|
||||
listener,
|
||||
reason: `_create: record - ${record}`,
|
||||
reason: this.isDebug && `_create: record - ${record}`,
|
||||
infoList,
|
||||
});
|
||||
}
|
||||
@@ -713,7 +709,9 @@ export class ModelManager {
|
||||
* @param {Record} record
|
||||
*/
|
||||
_delete(record) {
|
||||
this._ensureNoLockingListener();
|
||||
if (this.isDebug) {
|
||||
this._ensureNoLockingListener();
|
||||
}
|
||||
const model = record.constructor;
|
||||
if (!record.exists()) {
|
||||
throw Error(`Cannot delete already deleted record ${record}.`);
|
||||
@@ -742,14 +740,14 @@ export class ModelManager {
|
||||
for (const [listener, infoList] of this._listenersObservingRecord.get(record)) {
|
||||
this._markListenerToNotify(listener, {
|
||||
listener,
|
||||
reason: `_delete: record - ${record}`,
|
||||
reason: this.isDebug && `_delete: record - ${record}`,
|
||||
infoList,
|
||||
});
|
||||
}
|
||||
for (const [listener, infoList] of this._listenersObservingAllByModel.get(model)) {
|
||||
this._markListenerToNotify(listener, {
|
||||
listener,
|
||||
reason: `_delete: allByModel - ${record}`,
|
||||
reason: this.isDebug && `_delete: allByModel - ${record}`,
|
||||
infoList,
|
||||
});
|
||||
}
|
||||
@@ -818,7 +816,7 @@ export class ModelManager {
|
||||
for (const listener of listeners) {
|
||||
listener.onChange({
|
||||
listener,
|
||||
reason: `first call on ${record}`,
|
||||
reason: this.isDebug && `first call on ${record}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -871,7 +869,7 @@ export class ModelManager {
|
||||
record.__listeners.push(listener);
|
||||
listener.onChange({
|
||||
listener,
|
||||
reason: `first call on ${record}`,
|
||||
reason: this.isDebug && `first call on ${record}`,
|
||||
});
|
||||
if (!record.exists()) {
|
||||
break; // onChange might have deleted the record, other onChange shouldn't be executed
|
||||
@@ -951,12 +949,15 @@ export class ModelManager {
|
||||
* @returns {Record[]}
|
||||
*/
|
||||
_insert(model, dataList, options = {}) {
|
||||
this._ensureNoLockingListener();
|
||||
if (this.isDebug) {
|
||||
this._ensureNoLockingListener();
|
||||
}
|
||||
const records = [];
|
||||
for (const data of dataList) {
|
||||
let record = this.findFromIdentifyingData(model, data);
|
||||
if (!record) {
|
||||
const data2 = this._preInsertIdentifyingFieldsFromData(model, this._addDefaultData(model, data));
|
||||
const data2 = this._addDefaultData(model, data);
|
||||
this._preInsertIdentifyingFieldsFromData(model, data2);
|
||||
record = this._create(model);
|
||||
model.__recordsIndex.addRecord(record, data2);
|
||||
this._update(record, data2, { ...options, allowWriteReadonly: true });
|
||||
@@ -1027,10 +1028,10 @@ export class ModelManager {
|
||||
* @param {ModelField} field
|
||||
*/
|
||||
_markRecordFieldAsChanged(record, field) {
|
||||
for (const [listener, infoList] of this._listenersObservingFieldOfRecord.get(record).get(field)) {
|
||||
for (const [listener, infoList] of this._listenersObservingFieldOfRecord.get(record).get(field) || []) {
|
||||
this._markListenerToNotify(listener, {
|
||||
listener,
|
||||
reason: `_update: ${field} of ${record}`,
|
||||
reason: this.isDebug && `_update: ${field} of ${record}`,
|
||||
infoList,
|
||||
});
|
||||
}
|
||||
@@ -1082,23 +1083,18 @@ export class ModelManager {
|
||||
* and to replace them by corresponding "replace" commands.
|
||||
*
|
||||
* @param {Object} model
|
||||
* @param {Object} [data={}]
|
||||
* @returns {Object}
|
||||
* @param {Object} data
|
||||
*/
|
||||
_preInsertIdentifyingFieldsFromData(model, data) {
|
||||
const data2 = { ...data };
|
||||
for (const [fieldName, value] of Object.entries(data2)) {
|
||||
const field = model.__fieldMap[fieldName];
|
||||
if (!field) {
|
||||
throw new Error(`"${fieldName}" is not a field on "${model}".`);
|
||||
for (const fieldName of model.__identifyingFieldNames) {
|
||||
if (data[fieldName] === undefined) {
|
||||
return;
|
||||
}
|
||||
const field = model.__fieldMap.get(fieldName);
|
||||
if (!field.to) {
|
||||
continue;
|
||||
}
|
||||
if (!field.identifying) {
|
||||
continue;
|
||||
}
|
||||
const commands = field.convertToFieldCommandList(value);
|
||||
const commands = field.convertToFieldCommandList(data[fieldName]);
|
||||
if (commands.length !== 1) {
|
||||
throw new Error(`Identifying field "${model}/${fieldName}" should receive a single command.`);
|
||||
}
|
||||
@@ -1119,9 +1115,8 @@ export class ModelManager {
|
||||
throw new Error(`Identifying field "${model}/${fieldName}" should receive a single data object.`);
|
||||
}
|
||||
const [record] = this._insert(this.models[field.to], [command._value]);
|
||||
data2[fieldName] = record;
|
||||
data[fieldName] = record;
|
||||
}
|
||||
return data2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1205,28 +1200,32 @@ export class ModelManager {
|
||||
*/
|
||||
for (const model of Object.values(this.models)) {
|
||||
// Object with fieldName/field as key/value pair, for quick access.
|
||||
model.__fieldMap = model.__combinedFields;
|
||||
model.__fieldMap = new Map(Object.entries(model.__combinedFields));
|
||||
// List of all fields, for iterating.
|
||||
model.__fieldList = Object.values(model.__fieldMap);
|
||||
model.__fieldList = [...model.__fieldMap.values()];
|
||||
model.__requiredFieldsList = model.__fieldList.filter(
|
||||
field => field.required
|
||||
);
|
||||
model.__identifyingFieldNames = new Set();
|
||||
for (const [fieldName, field] of Object.entries(model.__fieldMap)) {
|
||||
for (const [fieldName, field] of model.__fieldMap) {
|
||||
if (field.identifying) {
|
||||
model.__identifyingFieldNames.add(fieldName);
|
||||
}
|
||||
// Add field accessors.
|
||||
Object.defineProperty(model.prototype, fieldName, {
|
||||
get: function getFieldValue() { // this is bound to record
|
||||
const field = model.__fieldMap[fieldName];
|
||||
if (this.modelManager._listeners.size) {
|
||||
if (!this.modelManager._listenersObservingRecord.has(this)) {
|
||||
this.modelManager._listenersObservingRecord.set(this, new Map());
|
||||
let entryRecord = this.modelManager._listenersObservingRecord.get(this);
|
||||
if (!entryRecord) {
|
||||
entryRecord = new Map();
|
||||
this.modelManager._listenersObservingRecord.set(this, entryRecord);
|
||||
}
|
||||
const reason = this.modelManager.isDebug && `getField - ${field} of ${this}`;
|
||||
let entryField = this.modelManager._listenersObservingFieldOfRecord.get(this).get(field);
|
||||
if (!entryField) {
|
||||
entryField = new Map();
|
||||
this.modelManager._listenersObservingFieldOfRecord.get(this).set(field, entryField);
|
||||
}
|
||||
const entryRecord = this.modelManager._listenersObservingRecord.get(this);
|
||||
const reason = `getField - ${field} of ${this}`;
|
||||
const entryField = this.modelManager._listenersObservingFieldOfRecord.get(this).get(field);
|
||||
for (const listener of this.modelManager._listeners) {
|
||||
listener.lastObservedRecords.add(this);
|
||||
const info = { listener, reason };
|
||||
@@ -1263,32 +1262,21 @@ export class ModelManager {
|
||||
* @returns {boolean} whether any value changed for the current record
|
||||
*/
|
||||
_update(record, data, options = {}) {
|
||||
this._ensureNoLockingListener();
|
||||
if (!record.exists()) {
|
||||
if (this.isDebug) {
|
||||
this._ensureNoLockingListener();
|
||||
}
|
||||
if (this.isDebug && !record.exists()) {
|
||||
throw Error(`Cannot update already deleted record ${record}.`);
|
||||
}
|
||||
const { allowWriteReadonly = false } = options;
|
||||
const model = record.constructor;
|
||||
let hasChanged = false;
|
||||
const sortedFieldNames = Object.keys(data);
|
||||
sortedFieldNames.sort((a, b) => {
|
||||
// Always update identifying fields first because updating other relational field will
|
||||
// trigger update of inverse field, which will require this identifying fields to be set
|
||||
// beforehand to properly detect whether this is already linked or not on the inverse.
|
||||
if (model.__identifyingFieldNames.has(a) && !model.__identifyingFieldNames.has(b)) {
|
||||
return -1;
|
||||
}
|
||||
if (!model.__identifyingFieldNames.has(a) && model.__identifyingFieldNames.has(b)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
for (const fieldName of sortedFieldNames) {
|
||||
for (const fieldName of Object.keys(data)) {
|
||||
if (data[fieldName] === undefined) {
|
||||
// `undefined` should have the same effect as not passing the field
|
||||
continue;
|
||||
}
|
||||
const field = model.__fieldMap[fieldName];
|
||||
const field = model.__fieldMap.get(fieldName);
|
||||
if (!field) {
|
||||
throw new Error(`Cannot create/update record with data unrelated to a field. (record: "${record}", non-field attempted update: "${fieldName}")`);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
export function followRelations(record, relatedPath) {
|
||||
let target = record;
|
||||
for (const field of relatedPath.split('.')) {
|
||||
if (!target.constructor.__fieldMap[field]) {
|
||||
if (!target.constructor.__fieldMap.has(field)) {
|
||||
throw Error(`field(${field}) does not exist on ${target}`);
|
||||
}
|
||||
target = target[field];
|
||||
|
||||
Reference in New Issue
Block a user