Обзор структуры моделей в сгенерированных приложениях.

Особенности объявления сгенерированных моделей в коде

Модели в технологии Flexberry Ember определяются “стандартным” для Ember способом. Создаваемая модель наследуется от базового технологического класса и предоставляет возможность применять правила валидации модели.

Генерируемые из Flexberry Designer модели чаще всего имеют следующую структуру

// Импорт для валидации.
import { buildValidations } from 'ember-cp-validations';

// Импорт базового класса для моделей во Flexberry Ember.
import EmberFlexberryDataModel from 'ember-flexberry-data/models/model';

// Импорт модели для работы в офлайн-режиме.
import OfflineModelMixin from 'ember-flexberry-data/mixins/offline-model';

// Проекции, правила валидации и сама модель определяются из соответсвующего миксина.
import {
  defineProjections,
  ValidationRules,
  Model as AgregatorClassMixin
} from '../mixins/regenerated/models/i-i-s-gen-test-agregator-class';

// Подготовка для задания на модель правил валидации данных.
const Validations = buildValidations(ValidationRules, {
  dependentKeys: ['model.i18n.locale'],
});

// Непосредственно определение модели.
let Model = EmberFlexberryDataModel.extend(OfflineModelMixin, AgregatorClassMixin, Validations, {
});

// Определение проекций (сами проекции заданы в миксине).
defineProjections(Model);

// Экспорт модели.
export default Model;

Соответствующий модели миксин с таким же именем расположен в папке mixins/regenerated/models. Его структура следующая:

// Необходимые импорты.
import Mixin from '@ember/object/mixin';
import $  from 'jquery';
import DS from 'ember-data';
import { validator } from 'ember-cp-validations';

// Импорт для объявления проекций.
import { attr, belongsTo, hasMany } from 'ember-flexberry-data/utils/attributes';

// Импорт для перечислимого типа.
import Enum1TypeEnum from '../../../enums/i-i-s-gen-test-enum1-type';

// Объявление атрибутов модели.
export let Model = Mixin.create({
  ...
});

// Объявление типичных правил валидации.
export let ValidationRules = {
  ...
};

// Объявление проекций.
export let defineProjections = function (modelClass) {
  ...
};

Генерируемые из Flexberry Designer модели создаются в папку models и именуются следующим образом:

  • если соответствующий C#-класс на OData-бэкенде называется NewPlatform.Someproject.Somemodel, то файл с моделью в клиентском приложении должен называться new-platform-someproject-somemodel,
  • если на OData-бэкенде используется атрибут PublishName для упрощения именования моделей, то наименование пространства имен в этом случае в клиентской модели может отсутствовать (имя клиентской модели будет формироваться соответственно имени в EDM-модели на OData-бакенде).

Объявление модели, имеющей предка

Если у модели есть класс-предок, то для класса-потомка объявление модели будет чуть отличаться:

import $ from 'jquery';
import { buildValidations } from 'ember-cp-validations';

import {
  defineBaseModel, // Импорт метода для работы с моделью предка.
  defineProjections,
  ValidationRules,
  Model as Child1Mixin
} from '../mixins/regenerated/models/i-i-s-gen-test-child1';

// Импорт модели предка.
import ParentClassModel from './i-i-s-gen-test-parent-class';

// Импорт правил валидации для модели предка.
import { ValidationRules as ParentValidationRules } from '../mixins/regenerated/models/i-i-s-gen-test-parent-class';

// Определение правил валидации с учётом правил валидации модели предка.
const Validations = buildValidations($.extend({}, ParentValidationRules, ValidationRules), {
  dependentKeys: ['model.i18n.locale'],
});

// Модель является расширением ParentClassModel, а не EmberFlexberryDataModel.
let Model = ParentClassModel.extend(Child1Mixin, Validations, {
});

// Определение элементов от базовой модели.
defineBaseModel(Model);
defineProjections(Model);

export default Model;

Соответствующий миксин отличается тем, что добавлено определение defineBaseModel:

...
export let defineBaseModel = function (modelClass) {
  modelClass.reopenClass({
    _parentModelName: 'i-i-s-gen-test-parent-class'
  });
};
...

Базовые классы и миксины для моделей Flexberry Ember

В технологии Flexberry Ember доступны следующие базовые модели:

В технологии Flexberry Ember доступны следующие базовые миксины для моделей:

  • CopyableMixin - миксин для поддержки возможности создания по прототипу, используется в базовой модели ModelWithoutValidation.
  • AuditModelMixin - миксин для работы подсистемы аудита, используется в миксине OfflineModelMixin.
  • OfflineModelMixin - миксин, добавляющий свойства для решения вопросов синхронизации, используется в базовой модели OfflineModel, а также добавляется в сгенерированную модель.

Правила генерации атрибутов и связей в модели

Атрибуты в моделях определяются “стандартным” для Ember способом, также как и связи.

export let Model = Mixin.create({
  // Определение собственных атрибутов.
  doubleField: DS.attr('decimal'),
  stringField: DS.attr('string'),

  // Определение мастера.
  myMaster: DS.belongsTo('i-i-s-gen-test-master-for-child1', { inverse: null, async: false }),

  // Определение детейлов.
  detail1ForChild1: DS.hasMany('i-i-s-gen-test-detail1-for-child1', { inverse: 'child1', async: false }),
  detail2ForChild1: DS.hasMany('i-i-s-gen-test-detail2-for-child1', { inverse: 'child1', async: false })
});

Генерируемые типы и трансформации

Дополнительно к стандартным типам данных для атрибутов моделей (string (строка), number (число), boolean (логический тип) и date (дата)) в технологииFlexberry Ember были добавлены трансформации:

  • decimal (вещественный тип),
  • file (тип “Файл”),
  • flexberry-enum (тип для перечислений),
  • guid (тип “GUID”, который используется по умолчанию для идентификаторов).

inverse-связи в модели

Задание inverse-связи используется, например, при работе с детейлами.

Задание связи от агрегатора к детейлу.

export let Model = Mixin.create({
  ...
  detail1ForChild1: DS.hasMany('i-i-s-gen-test-detail1-for-child1', { inverse: 'child1', async: false }),
});

Задание связи от детейла к агрегатору.

export let Model = Mixin.create({
  ...
  child1: DS.belongsTo('i-i-s-gen-test-child1', { inverse: 'detail1ForChild1', async: false })
});

Первичный ключ в модели

Первичные ключи объекта не задаются в модели явно. В клиентском коде обращения к первичному ключу можно выполнить через свойство id. Как называется соответствующее свойство на сервере, определяется в сериализаторе.

import { Serializer as MasterForAgregatorSerializer } from
  '../mixins/regenerated/serializers/i-i-s-gen-test-master-for-agregator';
import __ApplicationSerializer from './application';

export default __ApplicationSerializer.extend(MasterForAgregatorSerializer, {
  /**
  * Имя поля, где хранится первичный ключ модели.
  */
  primaryKey: '__PrimaryKey'
});

Первичные ключи моделей в Ember-приложениях всегда являются строками, но на сервере это поведение можно изменить. При изменении типа первичного ключа на сервере необходимо переопределить статическое свойство idType в классе модели:

import EmberFlexberryDataModel from 'ember-flexberry-data/models/model';

...

let Model = EmberFlexberryDataModel.extend( ... );

...

Model.reopenClass({
  idType: '...',
});

export default Model;

Устанавливается свойство idType при помощи статической функции defineIdType в базовой технологической модели:

defineIdType: function (newIdType) {
  this.reopenClass({
    idType: newIdType,
  });
},

Вызвать этот метод можно следующим образом:

Model.defineIdType('string');

Тип первичного ключа - это метаданные модели, поэтому свойство idType определено именно в модели, а не, например, в адаптере.

Получить тип ключа можно через метод getMeta утилиты information (см. ниже).

В языке запросов тип ключа учитывается автоматически, и при построении запросов к OData-бакенду значения ключей в URL запросов “окавычиваются” только в том случае, если тип ключа string.

Проекции в моделях

Проекции используются для определения, какие свойства будут запрошены с сервера или отправлены на него. Определение проекций для модели осуществляется следующим образом:

Model.defineProjection('<Имя проекции>', '<Имя класса>', '<Атрибуты проекции>');
  • Имя проекции может быть произвольным. Чаще всего для форм редактирования и создания используют представления с именем “<Короткое имя="" класса="">E", а для [списковых форм](efd3_listform.html) - "<Короткое имя="" класса="">L" (например, для модели `new-platform-gen-test-agregator-class` это будут `AgregatorClassE` и `AgregatorClassL`).
  • Имя класса - это имя текущего класса, для которого определяется модель. Например, new-platform-someproject-somemodel.
  • Атрибуты проекции - это атрибуты модели и зависимых моделей, которые входят в проекцию.

В примере ниже для модели i-i-s-gen-test-agregator-class определяется проекция AgregatorClassE.

// Для модели i-i-s-gen-test-agregator-class определяется проекция AgregatorClassE.
modelClass.defineProjection('AgregatorClassE', 'i-i-s-gen-test-agregator-class', {
    // Добавлен атрибут перечислимого типа.
    enum1Field: attr('Перечисление 1', { index: 0 }),

    // Добавлена ссылка на мастера типа i-i-s-gen-test-child2.
    child2: belongsTo('i-i-s-gen-test-child2', 'Мастер потомок', {
      dateTimeField: attr('~', { index: 2, hidden: true })
    }, { index: 1, displayMemberPath: 'dateTimeField' }),

    // Добавлена ссылка на мастера типа i-i-s-gen-test-master-for-agregator.
    masterForAgregator: belongsTo('i-i-s-gen-test-master-for-agregator', 'Master for agregator', {
      enum2Field: attr('~', { index: 4, hidden: true })
    }, { index: 3, displayMemberPath: 'enum2Field' }),

    // Добавлена ссылка на детейл типа i-i-s-gen-test-detail-for-agregator.
    detailForAgregator: hasMany('i-i-s-gen-test-detail-for-agregator', 'Детейл агрегатора', {
      // Добавлен атрибут детейла целого типа.
      detailIntField: attr('Целое', { index: 0 }),

      // Добавлена ссылка на мастера детейла типа i-i-s-gen-test-master-for-agregator.
      masterForAgregator: belongsTo('i-i-s-gen-test-master-for-agregator', 'Мастеровое', {
        enum2Field: attr('~', { index: 2, hidden: true })
      }, { index: 1, displayMemberPath: 'enum2Field' })
    })
  });
  • enum1Field: attr('Перечисление 1', { index: 0 }) - в проекцию модели i-i-s-gen-test-agregator-class добавляется свойство enum1Field модели i-i-s-gen-test-agregator-class с заголовком Перечисление 1.
  • child2: belongsTo('i-i-s-gen-test-child2', 'Мастер потомок', { ... }, { index: 1, displayMemberPath: 'dateTimeField' }) - в проекцию модели i-i-s-gen-test-agregator-class добавляется ссылка на мастера child2 типа i-i-s-gen-test-child2 с заголовком ‘Мастер потомок’, при этом на форме у данного свойства будет отображаться атрибут мастера dateTimeField. Сам же атрибут мастера dateTimeField, добавляемый кодом dateTimeField: attr('~', { index: 2, hidden: true }), скрыт (такое скрытие свойств мастеров часто используется для работы лукапов).
  • detailForAgregator: hasMany('i-i-s-gen-test-detail-for-agregator', 'Детейл агрегатора', { ... }) - в проекцию модели i-i-s-gen-test-agregator-class добавляется ссылка на детейлы detailForAgregator типа i-i-s-gen-test-detail-for-agregator с заголовком ‘Детейл агрегатора’. Из детейлов в представление попадают собственные свойства детейла, а также ссылка на мастера детейлов.

Генерируемые сериализаторы для моделей

Метаданные модели и вспомогательный класс information

Вспомогательный класс Information позволяет получать метаданные модели. Список доступных методов можно посмотреть в автодокументации.

Ниже представлен пример использования Information: в роуте форм создания определяется тип поля stringField, и соответствующее значение записывается в поле stringField созданной модели.

import EditFormNewRoute from 'ember-flexberry/routes/edit-form-new';

// Экспорт вспомогательного класса для последующей работы с ним.
import Information from 'ember-flexberry-data/utils/information';

export default EditFormNewRoute.extend({
  modelProjection: 'Child1E',
  modelName: 'i-i-s-gen-test-child1',
  templateName: 'i-i-s-gen-test-child1-e',

  afterModel(resolvedModel, transition){
    this._super(...arguments);

    // Создание экземпляра класса Information. Во Flexberry-эмбер приложениях в роутах и контроллерах инжектится сервис store и явным образом его получать не требуется.
    let information = new Information(this.get('store'));

    // Получение типа поля 'stringField' созданной модели.
    let fieldType = information.getType(this.get('modelName'), 'stringField');

    // Задание типа поля в качестве значения 'stringField'.
    resolvedModel.set('stringField', fieldType);
  }
});

Вспомогательные модели Flexberry Ember

Вспомогательные модели Flexberry Ember включают:

Создание динамических моделей

Если необходимо зарегистрировать и использовать новую модель во время выполнения программы, то это можно сделать динамически. Изначально нужно иметь описание самой модели. Например:

    let dynamicModel = {
      "modelName": "dynamic-model",
      "attrs": [
        {
          "name": "text",
          "type": "string",
          "notNull": false,
          "defaultValue": "",
          "stored": true
        }
      ],
      "projections": [
        {
          "name": "BaseE",
          "attrs": [
            {
              "name": "text",
              "caption": "Text",
              "hidden": false,
              "index": 0,
            }
          ]
        }
      ]
    };

Тогда вызов можно определить следующим образом:

  import { getOwner } from '@ember/application';
  import { dynamicModelRegistration } from 'dummy/utils/create-dynamic-models';

  /* ... */

  dynamicModelRegistration(dynamicModel, getOwner(this));
  var base = store.createRecord('dynamic-model', { text: 'dynamic-model-text'});

  /* ... */

Далее необходимо реализовать функцию регистрации новой модели по заданному описанию. Например:

import { isNone } from '@ember/utils';
import DS from 'ember-data';
import { attr } from 'ember-flexberry-data/utils/attributes';
import Model from 'ember-flexberry-data/models/model';

  // Устанавливаем дефолтные значения для атрибутов модели и возвращаем модель.
  function createModel(attrs) {
    if (isNone(attrs)) {
      return;
    }

    let model = {};
    attrs.forEach((attribute) => {
      model[attribute.name] = DS.attr(attribute.type, { required: attribute.notNull });
    });

    let modelResult = Model.extend(model);
    return modelResult;
  }

  // Устанавливаем дефолтьные значения для атрибутов в представлении
  function createProjection(projectionObj) {
    let modelProjection = { };

    projectionObj.attrs.forEach((attribute) => {
      modelProjection[attribute.name] = attr('');
    });

    return modelProjection;
  }

 let dynamicModelRegistration = function(dynamicModelObj, owner) {
    let modelRegistered = owner.hasRegistration(`model:${dynamicModelObj.modelName}`);

    // Проверяем, что модель еще не была зарегистрирована
    if (!modelRegistered) {
      let model = createModel(dynamicModelObj.attrs);
    
    // Создание представлений для модели
      dynamicModelObj.projections.forEach((projection) => {
        model.defineProjection(projection.name, dynamicModelObj.modelName, createProjection(projection));
      });

    // Регистрируем модель
      owner.register(`model:${dynamicModelObj.modelName}`, model);
    }
  }

export {
  dynamicModelRegistration
};

Пример с реализацией на тестовом стенде.