Skip to content

Commit

Permalink
Режим экономной загрузки мастеров объекта
Browse files Browse the repository at this point in the history
  • Loading branch information
inaidanov committed Feb 2, 2024
1 parent a37785c commit 6c8e849
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
using System.Web.Http.Validation;
using NewPlatform.Flexberry.ORM.ODataService.Events;
using NewPlatform.Flexberry.ORM.ODataService.Handlers;
using Newtonsoft.Json.Linq;
using System.Web;
#endif
#if NETSTANDARD
using Microsoft.AspNet.OData.Formatter;
Expand Down Expand Up @@ -787,13 +789,47 @@ private static void AddObjectToUpdate(List<DataObject> objsToUpdate, DataObject
if (insertToEnd)
{
objsToUpdate.Add(dataObject); // Добавляем в конец списка.
} else
}
else
{
objsToUpdate.Insert(0, dataObject); // Добавляем объект в начало списка.
}

}
}
}

/// <summary>
/// Получить значение ключа у указанной сущности.
/// </summary>
/// <param name="edmEntity">Сущность.</param>
/// <returns>Значение ключа.</returns>
private object GetKey(EdmEntityObject edmEntity)
{
object key;

// Получим значение ключа.
IEdmEntityType entityType = (IEdmEntityType)edmEntity.ActualEdmType;
IEnumerable<IEdmProperty> entityProps = entityType.Properties();
var keyProperty = entityProps.FirstOrDefault(prop => prop.Name == _model.KeyPropertyName);
edmEntity.TryGetPropertyValue(keyProperty.Name, out key);

return key;
}

/// <summary>
/// Построение объекта данных по сущности OData без загрузки свойств и мастеров/детейлов. Загружен только первичный ключ.
/// </summary>
/// <param name="edmEntity">Сущность OData.</param>
/// <returns>Объект данных.</returns>
private DataObject GetDataObjectByEdmEntityLight(EdmEntityObject edmEntity)
{
var masterType = _model.GetDataObjectType(edmEntity);
var masterKey = GetKey(edmEntity);
var dataObject = (DataObject)Activator.CreateInstance(masterType);
dataObject.SetExistObjectPrimaryKey(masterKey);

return dataObject;
}

/// <summary>
/// Построение объекта данных по сущности OData.
Expand All @@ -811,21 +847,7 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke
return null;
}

// Значение свойства.
object value;

// Получим значение ключа.
IEdmEntityType entityType = (IEdmEntityType)edmEntity.ActualEdmType;
IEnumerable<IEdmProperty> entityProps = entityType.Properties().ToList();
var keyProperty = entityProps.FirstOrDefault(prop => prop.Name == _model.KeyPropertyName);
if (key != null)
{
value = key;
}
else
{
edmEntity.TryGetPropertyValue(keyProperty.Name, out value);
}
key = key ?? GetKey(edmEntity);

// Загрузим объект из хранилища, если он там есть, или создадим, если нет, но только для POST.
// Тем самым гарантируем загруженность свойств при необходимости обновления и установку нужного статуса.
Expand All @@ -840,7 +862,7 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke
view = _model.GetDataObjectDefaultView(objType);
}

DataObject obj = ReturnDataObject(objType, value, view);
DataObject obj = ReturnDataObject(objType, key, view);

// Добавляем объект в список для обновления, если там ещё нет объекта с таким ключом.
AddObjectToUpdate(dObjs, obj, endObject);
Expand All @@ -850,12 +872,17 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke
IEnumerable<string> changedPropNames = edmEntity.GetChangedPropertyNames();

// Обрабатываем агрегатор первым.
IEdmEntityType entityType = (IEdmEntityType)edmEntity.ActualEdmType;
IEnumerable<IEdmProperty> entityProps = entityType.Properties().ToList();
List<IEdmProperty> changedProps = entityProps
.Where(ep => changedPropNames.Contains(ep.Name))
.OrderBy(ep => ep.Name != agregatorPropertyName)
.ToList();
foreach (var prop in changedProps)
{
object value;
edmEntity.TryGetPropertyValue(prop.Name, out value);

string dataObjectPropName;
try
{
Expand All @@ -875,8 +902,6 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke
// Обработка мастеров и детейлов.
if (prop is EdmNavigationProperty navProp)
{
edmEntity.TryGetPropertyValue(prop.Name, out value);

EdmMultiplicity edmMultiplicity = navProp.TargetMultiplicity();

// Обработка мастеров.
Expand All @@ -886,11 +911,25 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke
{
// Порядок вставки влияет на порядок отправки объектов в UpdateObjects это в свою очередь влияет на то, как срабатывают бизнес-серверы. Бизнес-сервер мастера должен сработать после, а агрегатора перед этим объектом.
bool insertIntoEnd = string.IsNullOrEmpty(agregatorPropertyName);
DataObject master = GetDataObjectByEdmEntity(edmMaster, null, dObjs, insertIntoEnd, useUpdateView);
bool masterOwnPropsUpdated = edmMaster.GetChangedPropertyNames().Any(propName => propName != _model.KeyPropertyName);
bool isAggregator = dataObjectPropName == agregatorPropertyName;
DataObject master = null;

Type masterType = _model.GetDataObjectType(edmEntity);
bool masterLightLoad = _model.IsMasterLightLoad(masterType);

if (masterLightLoad && !masterOwnPropsUpdated && !isAggregator)
{
master = GetDataObjectByEdmEntityLight(edmMaster); // здесь мастер не добавляется в dObjs (объекты на обновление) т.к. мы точно знаем что он будет в состоянии UnAltered
}
else
{
master = GetDataObjectByEdmEntity(edmMaster, null, dObjs, insertIntoEnd, useUpdateView);
}

Information.SetPropValueByName(obj, dataObjectPropName, master);

if (dataObjectPropName == agregatorPropertyName)
if (isAggregator)
{
master.AddDetail(obj);

Expand Down Expand Up @@ -946,10 +985,9 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke
else
{
// Обработка собственных свойств объекта (неключевых, т.к. ключ устанавливаем при начальной инициализации объекта obj).
if (prop.Name != keyProperty.Name)
if (prop.Name != _model.KeyPropertyName)
{
Type dataObjectPropertyType = Information.GetPropertyType(objType, dataObjectPropName);
edmEntity.TryGetPropertyValue(prop.Name, out value);

// Если тип свойства относится к одному из зарегистрированных провайдеров файловых свойств,
// значит свойство файловое, и его нужно обработать особым образом.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,13 @@ public View GetDataObjectUpdateView(Type dataObjectType)
return _metadata[dataObjectType].UpdateView?.Clone();
}

/// <summary>
/// Возвращает информацию, должны ли мастера объекта загружаться в экономном режиме (только __PrimaryKey).
/// </summary>
/// <param name="dataObjectType">Тип объекта данных.</param>
/// <returns>Мастера должны загружаться экономно.</returns>
public bool IsMasterLightLoad(Type dataObjectType) => _metadata[dataObjectType].MasterLightLoad;

/// <summary>
/// Получает список зарегистрированных в модели типов по списку имён типов.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public sealed class DataObjectEdmTypeSettings
/// </summary>
public View UpdateView { get; set; }

/// <summary>
/// Whether to load object masters in LightLoaded state (load only primary key).
/// </summary>
public bool MasterLightLoad { get; set; }

/// <summary>
/// The list of exposed details.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public class DefaultDataObjectEdmModelBuilder : IDataObjectEdmModelBuilder
/// </summary>
private Dictionary<Type, View> UpdateViews { get; set; }

/// <summary>
/// Types for which masters should be light-loaded on updates (load only __PrimaryKey).
/// </summary>
private IEnumerable<Type> MasterLightLoadTypes { get; set; }

private readonly PropertyInfo _keyProperty = Information.ExtractPropertyInfo<DataObject>(n => n.__PrimaryKey);

/// <summary>
Expand All @@ -88,7 +93,8 @@ public DefaultDataObjectEdmModelBuilder(
bool useNamespaceInEntitySetName = true,
PseudoDetailDefinitions pseudoDetailDefinitions = null,
Dictionary<Type, IEdmPrimitiveType> additionalMapping = null,
IEnumerable<KeyValuePair<Type, View>> updateViews = null)
IEnumerable<KeyValuePair<Type, View>> updateViews = null,
IEnumerable<Type> masterLightLoadTypes = null)
{
_searchAssemblies = searchAssemblies ?? throw new ArgumentNullException(nameof(searchAssemblies), "Contract assertion not met: searchAssemblies != null");
_useNamespaceInEntitySetName = useNamespaceInEntitySetName;
Expand All @@ -105,6 +111,11 @@ public DefaultDataObjectEdmModelBuilder(
{
SetUpdateView(updateViews);
}

if (masterLightLoadTypes != null)
{
SetMasterLightLoadTypes(masterLightLoadTypes);
}
}

/// <summary>
Expand Down Expand Up @@ -232,6 +243,26 @@ private void SetUpdateView(Type dataObjectType, View updateView)
UpdateViews[dataObjectType] = updateView;
}

/// <summary>
/// Sets DataObject types for which masters will be light-loaded on updates (load only __PrimaryKey).
/// </summary>
/// <param name="masterLightLoadTypes">Types for which masters should be light-loaded.</param>
private void SetMasterLightLoadTypes(IEnumerable<Type> masterLightLoadTypes)
{
if (masterLightLoadTypes != null)
{
foreach (Type type in masterLightLoadTypes)
{
if (!type.IsSubclassOf(typeof(DataObject)))
{
throw new ArgumentException("MasterLightLoad option can be set only for a DataObject.", nameof(masterLightLoadTypes));
}
}

MasterLightLoadTypes = masterLightLoadTypes;
}
}

/// <summary>
/// Adds the property for exposing.
/// </summary>
Expand Down Expand Up @@ -310,6 +341,7 @@ private void AddDataObjectWithHierarchy(DataObjectEdmMetadata meta, Type dataObj
CollectionName = EntitySetNameBuilder(dataObjectType),
DefaultView = DynamicView.Create(dataObjectType, null).View,
UpdateView = updateView,
MasterLightLoad = MasterLightLoadTypes?.Contains(dataObjectType) ?? false,
};

AddProperties(dataObjectType, typeSettings);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#if NETCOREAPP
namespace NewPlatform.Flexberry.ORM.ODataService.Tests.CRUD.Update
{
using System;
using System.Net;
using System.Net.Http;
using ICSSoft.STORMNET;
using NewPlatform.Flexberry.ORM.ODataService.Tests.Extensions;
using NewPlatform.Flexberry.ORM.ODataService.Tests.Helpers;

using Xunit;
using Xunit.Abstractions;

/// <summary>
/// Тесты для проверки работы MasterLightLoad. Для запуска OData backend используется модифицированная версия Startup - <see cref="MasterLightLoadTestStartup"/>,
/// которая задаёт флаг экономной загрузки мастеров для <see cref="Котенок"/>.
/// </summary>
public class MasterLightLoadTest : BaseODataServiceIntegratedTest<MasterLightLoadTestStartup>
{
/// <summary>
/// Конструктор по-умолчанию.
/// </summary>
/// <param name="factory">Фабрика для приложения.</param>
/// <param name="output">Вывод диагностической информации по тестам.</param>
public MasterLightLoadTest(CustomWebApplicationFactory<MasterLightLoadTestStartup> factory, ITestOutputHelper output)
: base(factory, output)
{
}

/// <summary>
/// Проверка экономной загрузки мастера при активной настройке MasterLightLoad при смене мастера.
/// </summary>
[Fact]
public void MasterLightLoadSettingTest()
{
ActODataService(args =>
{
// Создаем объекты данных, которые потом будем обновлять, и добавляем в базу обычным сервисом данных.
Порода порода = new Порода { Название = "Сиамская" };
Кошка кошка1 = new Кошка { Кличка = "Болтушка", Агрессивная = true, Порода = порода };
Кошка кошка2 = new Кошка { Кличка = "Петрушка", Агрессивная = false, Порода = порода };
args.DataService.UpdateObject(порода);
args.DataService.UpdateObject(кошка1);
args.DataService.UpdateObject(кошка2);
Котенок котенок = new Котенок { Кошка = кошка1, КличкаКотенка = "Котенок Гав", Глупость = 10 };
args.DataService.UpdateObject(котенок);
// Обновляем ссылку на мастера
котенок.Кошка = кошка2;
// Представление, по которому будем обновлять.
string[] котенокPropertiesNames =
{
Information.ExtractPropertyPath<Котенок>(x => x.__PrimaryKey),
};
var котенокDynamicView = new View(new ViewAttribute("котенокDynamicView", котенокPropertiesNames), typeof(Котенок));
// Преобразуем котенка в JSON-строку.
string requestJsonData = котенок.ToJson(котенокDynamicView, args.Token.Model);
// Добавляем в payload информацию о том, что поменяли ссылку на мастера
requestJsonData = ODataTestHelper.AddEntryRelationship(requestJsonData, котенокDynamicView, args.Token.Model, кошка2, nameof(Котенок.Кошка));
// Формируем URL запроса к OData-сервису (с идентификатором изменяемой сущности).
var requestUrl = ODataTestHelper.GetRequestUrl(args.Token.Model, котенок);
using (HttpResponseMessage response = args.HttpClient.PatchAsJsonStringAsync(requestUrl, requestJsonData).Result)
{
// Если приходит код 200, значит настройка не ломает загрузку. Фактическую проверку того, что кошка загрузилась в LightLoaded надо делать через отладчик.
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
});
}
}
}
#endif
Loading

0 comments on commit 6c8e849

Please sign in to comment.