Импорт данных из АИС

Инфраструктура Версия: 1.0 accepted

⟨/⟩ Исходник

1 НазначениеAisNaznachenie⟨/⟩

Два отдельных сервиса выполняют импорт данных из внешней системы АИС (read-only БД AisReadDbContext) в соответствующие целевые БД:

Сервис Целевая БД Точка входа Результат
AisToNtrImportService NtrDbContext ImportAsync AisToNtrImportResult
AisToMverImportService MverDbContext ImportAsync AisToMverImportResult

2 Общие архитектурные решенияAisObschieArhitekturnyeResheniya⟨/⟩

2.1 Управление памятью EF Core (Change Tracker)AisUpravleniePamyatyuEfCoreChangeTracker⟨/⟩

Главная проблема: при импорте «в лоб» EF съедает ~1 ГБ памяти из-за Change Tracker.

Решение: оба сервиса принимают IServiceScopeFactory (не DbContext напрямую) и создают отдельный DI-scope на каждый логический блок импорта. После завершения scope DbContext и его Change Tracker освобождаются.

2.2 ТранзакцииAisTranzakcii⟨/⟩

Каждый блок/шаг импорта оборачивается в явную транзакцию (BeginTransactionAsync → CommitAsync / RollbackAsync). При ошибке откатывается только текущий блок; ранее сохранённые данные остаются в БД.

2.3 Передача данных между блокамиAisPeredachaDannyhMezhduBlokami⟨/⟩

Между блоками данные передаются через словари маппинга Dictionary<Guid, Guid> (AisId → NtrId/MverId). Это необходимо, т.к. каждый блок работает в своём scope и не имеет доступа к tracked-сущностям предыдущего блока.

2.4 Идемпотентность (upsert / insert-only)AisIdempotentnostUpsertInsertOnly⟨/⟩

Большинство методов импорта реализуют паттерн upsert:

  1. Загрузить исходные данные из АИС
  2. Загрузить существующие записи из целевой БД по AisId
  3. Если запись с таким AisId есть — обновить поля через Set-методы
  4. Если нет — создать новую сущность и добавить в DbContext

Исключения (insert-only): некоторые модели не обновляют существующие записи, а только создают новые при отсутствии. Это осознанные решения — такие сущности являются корневыми или не содержат данных из АИС.

2.5 Стратегия обработки ошибокAisStrategiyaObrabotkiOshibok⟨/⟩

Оба сервиса используют стратегию fail-fast: если при импорте невозможно разрешить FK-зависимость, выбрасывается NtrEntityNotFoundException или NtrApplicationException. Текущая транзакция откатывается, ранее сохранённые блоки остаются в БД. Повторный запуск безопасен благодаря upsert.

2.6 КонфигурацияAisKonfiguraciya⟨/⟩

ImportAisDataConfiguration содержит AllowedCompanyNames — список LIKE-паттернов для фильтрации компаний. Фильтр применяется в обоих сервисах при загрузке орг. структуры, зданий и связанных данных.


3 AisToNtrImportServiceAisAistontrimportservice⟨/⟩

Импортирует данные НТР (нормативное теплопотребление). Работает с NtrDbContext.

3.1 Блоки импортаAisBlokiImporta⟨/⟩

3.1.1 Блок 1: Справочники (одна транзакция, один scope)AisBlok1SpravochnikiOdnaTranzakciyaOdinScope⟨/⟩

Записей мало, грузим все разом.

Метод Источник АИС Целевая сущность Возвращает
ImportClimateRegionsAsync RefDClimateData RefClimateRegionEntity Dict<AisId, NtrId>
ImportBasementAtticHeatingTypesAsync RefDBasementAtticHeatingTypes RefBasementAtticHeatingTypeEntity Dict<AisId, NtrId>
ImportBuildingPurposesAsync RefDBuildingTypes RefBuildingPurposeEntity Dict<AisId, NtrId>
ImportBuildingShapesAsync RefDBuildingShapes RefBuildingShapeEntity Dict<AisId, NtrId>
ImportElectricityConsumerTypesAsync RefDConsumerDeviceKinds RefElectricityConsumerTypeEntity Dict<AisId, NtrId>
ImportGateKindsAsync RefDGateKinds RefGateKindEntity Dict<AisId, NtrId>
ImportGlazingKindsAsync RefDGlazingKinds RefGlazingKindEntity Dict<AisId, NtrId>
ImportInsulationMaterialsAsync RefDHeaterMaterials RefInsulationMaterialEntity Dict<AisId, NtrId>
ImportMaterialsAsync RefDConstructionMaterials RefMaterialEntity Dict<AisId, NtrId>
ImportRoofTypesAsync RefDRoofTypes RefRoofTypeEntity Dict<AisId, NtrId>
ImportEnergyEfficiencyClassesAsync RefDEnergyEfficiencyClasses RefEnergyEfficiencyClassEntity int (count)
ImportSolarRadiationByDirectionsAsync RefDSsrs RefSolarRadiationByDirectionEntity int (count)
ImportSpecificHeatConsumptionNormsAsync — RefSpecificHeatConsumptionNormEntity int (count)

Нюанс: ImportSolarRadiationByDirectionsAsync выполняет маппинг сокращённых месяцев (янв, фев...) в полные названия и номера.

3.1.2 Блок 2: Орг. структура (одна транзакция, один scope)AisBlok2OrgStrukturaOdnaTranzakciyaOdinScope⟨/⟩

~1240 записей. Порядок импорта строго иерархический (FK-зависимости):

NavCompanies → NavIa (зависит от Companies)
             → NavMes (зависит от Companies)
                 → NavPmes (зависит от Mes)
                     → NavSubstations (зависит от Pmes, ClimateRegions)
                         → Navs (зависит от всех вышеперечисленных)
Метод Целевая сущность
ImportNavCompaniesAsync NavCompanyEntity
ImportNavIaAsync NavIaEntity
ImportMesAsync NavMesEntity
ImportPmesAsync NavPmesEntity
ImportSubstationsAsync NavSubstationEntity
ImportNavsAsync NavEntity

Нюанс ImportNavsAsync: ParentId устанавливается в два прохода — сначала создаются все Nav, затем второй проход устанавливает ParentId через словарь result.

Отличие от Mver: ImportSubstationsAsync дополнительно принимает climateRegionsDictByAisId и устанавливает ClimateRegionId подстанции.

3.1.3 Блок 3: Здания (одна транзакция, один scope)AisBlok3ZdaniyaOdnaTranzakciyaOdinScope⟨/⟩

~6450 записей.

Метод Описание
FetchBuildingSourceDataAsync Загружает сырые данные зданий из АИС (включая параметры стен, крыши, подвала). Не создаёт сущности — только DTO.
ImportBuildingsAsync Создаёт/обновляет RegBuildingEntity. Возвращает Dict<AisId, BuildingDto>.

Нюанс: BuildingSourceDataDto содержит данные, которые позже распределяются по разным моделям (стены, крыша, контакт с грунтом, форма, условия эксплуатации). Этот DTO живёт в памяти до конца импорта.

3.1.4 Блок 4: Модели потребления (каждая — отдельная транзакция через `ExecuteInSeparateScopeAsync`)AisBlok4ModeliPotrebleniyaKazhdayaOtdelnayaTranzakciyaCher⟨/⟩

6450 зданий × 11 типов моделей = ~70950 потенциальных insert/update. Каждый тип создаётся в отдельной транзакции.

# Метод Целевая сущность Возвращает
1 ImportConsumptionModels CmConsumptionModelEntity Dict<BuildingId, CmId> (insert-only)
2 ImportWindowsModelsAsync CmWindowsModelEntity Dict<CmId, WindowsModelId>
3 ImportGateModelsAsync CmGateModelEntity Dict<CmId, GateModelId>
4 ImportHeatingPeriodModelsAsync CmHeatingPeriodModelEntity Dict<CmId, HeatingPeriodModelId> (insert-only)
5 ImportRoofModelsAsync CmRoofModelEntity int
6 ImportGroundContactsAsync CmGroundContactEntity int
7 ImportVentilationsAsync CmVentilationEntity int (insert-only)
8 ImportLightingModelsAsync CmLightingModelEntity Dict<CmId, LightingModelId>
9 ImportEeConsumerModelAsync CmEeConsumerModelEntity Dict<CmId, EeConsumerModelId>
10 ImportOperatingConditions CmOperatingConditionsEntity int
11 ImportWallsModelsAsync CmWallsModelEntity int
12 ImportShapeAndDimensionsAsync CmShapeAndDimensionsEntity int

Нюансы:

  • ImportConsumptionModels создаёт базовую модель через CmConsumptionModelEntity.CreateBaseModel(buildingId, CmCalcKind.Custom.Id, buildingName). Insert-only.
  • ImportVentilationsAsync создаёт заглушки (isMechVentExist=false, isRecuperatorExist=false) — данных в АИС нет. Insert-only.
  • ImportHeatingPeriodModelsAsync создаёт модель с CmHeatingPeriodConditionsSrc.ReferenceClimateData.

3.1.5 Блок 5: Сущности моделей (каждая — отдельная транзакция через `ExecuteInSeparateScopeAsync`)AisBlok5SuschnostiModeleyKazhdayaOtdelnayaTranzakciyaChere⟨/⟩

Дочерние записи моделей из блока 4. Могут быть множественными на одно здание. Используют 3-параметровый ExecuteInSeparateScopeAsync (NtrDbContext + AisReadDbContext + CancellationToken).

Метод Целевая сущность Примерное кол-во
ImportWindowsEntitiesAsync CmWindowsEntity N на здание
ImportGateEntitiesAsync CmGateEntity N на здание
ImportLightingEntitiesAsync CmLightingEntity N на здание
ImportEeConsumerKindEntitiesAsync CmEeConsumerKindEntity ~22641
ImportHeatingPeriodsAsync CmHeatingPeriodEntity N на здание

3.2 ExecuteInSeparateScopeAsync — три перегрузкиAisExecuteinseparatescopeasyncTriPeregruzki⟨/⟩

// 1. С результатом, без AisReadDbContext (блок 4)
Task<TResult> ExecuteInSeparateScopeAsync<TResult>(Func<NtrDbContext, CancellationToken, Task<TResult>>)

// 2. Без результата, с AisReadDbContext (блок 5)
Task ExecuteInSeparateScopeAsync(Func<NtrDbContext, AisReadDbContext, CancellationToken, Task>)

// 3. Без результата, без AisReadDbContext
Task ExecuteInSeparateScopeAsync(Func<NtrDbContext, CancellationToken, Task>)

3.3 Граф зависимостейAisGrafZavisimostey⟨/⟩

Блок 1 (справочники)
  ├─→ climateRegionsDictByAisId ──────────→ Блок 2 (Substations), Блок 3 (Buildings)
  ├─→ basementAtticHeatingTypesDictByAisId → Блок 4 (RoofModels, GroundContacts)
  ├─→ buildingPurposesDictByAisId ────────→ Блок 4 (OperatingConditions)
  ├─→ buildingShapesDictByAisId ──────────→ Блок 4 (ShapeAndDimensions)
  ├─→ electricityConsumerTypesDictByAisId → Блок 5 (EeConsumerKindEntities)
  ├─→ gateKindsDictByAisId ──────────────→ Блок 5 (GateEntities)
  ├─→ glazingKindsDictByAisId ───────────→ Блок 5 (WindowsEntities)
  ├─→ insulationMaterialsDictByAisId ────→ Блок 4 (RoofModels, WallsModels)
  ├─→ materialsDictByAisId ─────────────→ Блок 4 (RoofModels, WallsModels)
  └─→ roofTypesDictByAisId ─────────────→ Блок 4 (RoofModels)

Блок 2 (орг. структура)
  └─→ navsDictByAisId ──→ Блок 3 (Buildings)

Блок 3 (здания)
  ├─→ buildingSourceDataDtosDictByAisId ──→ Блок 4 (Roof, GroundContact, Walls, Shape, OperatingConditions)
  └─→ buildingsDictByAisId ──────────────→ Блок 4, Блок 5

Блок 4 (модели)
  ├─→ cmDictByBuildingId ───────→ Блок 4 (все модели), Блок 5
  ├─→ windowsModelsDictByCmId ─→ Блок 5 (WindowsEntities)
  ├─→ gateModelsDictByCmId ────→ Блок 5 (GateEntities)
  ├─→ heatingPeriodModelsDictByCmId → Блок 5 (HeatingPeriods)
  ├─→ lightingModelDictByCmId ─→ Блок 5 (LightingEntities)
  └─→ eeConsumerModelDictByCmId → Блок 5 (EeConsumerKindEntities)

4 AisToMverImportServiceAisAistomverimportservice⟨/⟩

Импортирует данные МВЭР (матрица выбора энергоэффективных решений). Работает с MverDbContext.

4.1 Блоки импортаAisBlokiImporta2⟨/⟩

4.1.1 Блок 1: Справочники МВЭР (одна транзакция, один scope)AisBlok1SpravochnikiMverOdnaTranzakciyaOdinScope⟨/⟩

Записей мало, грузим все разом.

Метод Источник АИС Целевая сущность Возвращает
ImportHeatingTypesAsync RefDHeatingTypes RefHeatingTypeEntity Dict<AisId, MverId>
ImportElectricDeviceTypesAsync RefDConsumerDeviceKinds RefElectricDeviceTypeEntity Dict<AisId, MverId>
ImportHeatAdjustMethodsAsync RefDHeatAdjustMethods RefHeatAdjustMethodEntity Dict<AisId, MverId>
ImportPeriodsAsync RefPeriods RefPeriodEntity Dict<AisId, MverId>
ImportTransformerCoolTypesAsync RefDTransformerCoolingTypes RefTransformerCoolTypeEntity Dict<AisId, MverId>
ImportTransformerTypesAsync RefDTransformerTypes RefTransformerTypeEntity Dict<AisId, MverId>

4.1.2 Блок 2: Орг. структура (одна транзакция, один scope)AisBlok2OrgStrukturaOdnaTranzakciyaOdinScope2⟨/⟩

Идентична орг. структуре Ntr-импорта по логике, но работает с MverDbContext.

NavCompanies → NavIa → NavMes → NavPmes → NavSubstations → Navs

Отличие от Ntr: ImportSubstationsAsync не принимает climateRegionsDictByAisId — подстанции в МВЭР не привязаны к климатическому региону.

4.1.3 Блок 3: Здания (одна транзакция, один scope)AisBlok3ZdaniyaOdnaTranzakciyaOdinScope2⟨/⟩

~6450 записей. Создаёт/обновляет RegBuildingEntity.

  • FetchBuildingSourceDataAsync — загружает сырые данные зданий из АИС
  • ImportBuildingsAsync — upsert RegBuildingEntity. Возвращает Dict<AisId, Guid>.

4.1.4 Блок 4: Mver данные (каждая — отдельная транзакция через `ExecuteInSeparateScopeAsync`)AisBlok4MverDannyeKazhdayaOtdelnayaTranzakciyaCherezExecut⟨/⟩

Метод Целевая сущность Источник АИС Стратегия
ImportMverBuildingEeConsumersAsync MverBuildingEeConsumerEntity DocBuildingConsumersEes upsert
ImportMverHeatedEquipmentsAsync MverHeatedEquipmentEntity DocHeatedEquipments upsert
ImportMverTransformersAsync MverTransformerEntity RefETransformers upsert
ImportMverTransformerWorkInfosAsync MverTransformerWorkInfoEntity DocTrWorks + DocTrWorksValues upsert, батчи по 10К
ImportMverTsnsAsync MverTsnEntity RefETsns upsert

Нюансы:

  • Фильтр по типу прибора: ImportMverBuildingEeConsumersAsync — только RefDConsumerDeviceKind.Code == "conditioner".
  • Name для EeConsumer формируется как "{DeviceKindName} ({Amount} шт., {Power} кВт)".
  • ImportMverTransformerWorkInfosAsync фильтрует по Year >= 2020. Обходит данные постранично батчами по 10К с вызовом SaveChanges + ChangeTracker.Clear() на каждый батч. Фильтр по типу периода задаётся через константу MonthPeriodType в сервисе.

4.2 ExecuteInSeparateScopeAsync — одна перегрузкаAisExecuteinseparatescopeasyncOdnaPeregruzka⟨/⟩

// С результатом, с AisReadDbContext
Task<TResult> ExecuteInSeparateScopeAsync<TResult>(Func<MverDbContext, AisReadDbContext, CancellationToken, Task<TResult>>)

4.3 Граф зависимостейAisGrafZavisimostey2⟨/⟩

Блок 1 (справочники)
  ├─→ heatingTypesDictByAisId ──────────→ Блок 3 (Buildings)
  ├─→ electricDeviceTypesDictByAisId ───→ Блок 4 (MverBuildingEeConsumers)
  ├─→ heatAdjustMethodsDictByAisId ────→ Блок 4 (MverHeatedEquipments)
  ├─→ periodsDictByAisId ──────────────→ Блок 4 (MverTransformerWorkInfos)
  ├─→ transformerCoolTypesDictByAisId ─→ Блок 4 (MverTransformers)
  └─→ transformerTypesDictByAisId ─────→ Блок 4 (MverTransformers)

Блок 2 (орг. структура)
  └─→ navsDictByAisId ──→ Блок 3 (Buildings), Блок 4 (все)

Блок 3 (здания)
  └─→ buildingsDictByAisId ────────────→ Блок 4 (MverBuildingEeConsumers)

Блок 4 (Mver данные)
  └─→ transformersDictByAisId ─────────→ Блок 4 (MverTransformerWorkInfos)

5 Маппинг полей справочников (АИС → НТР)AisMappingPoleySpravochnikovAisNtr⟨/⟩

Большинство справочников маппят поля 1:1 (Id → AisId, Name → Name, Code → Code). Ниже перечислены только нетривиальные маппинги:

5.1 RefGlazingKindEntity (вид остекления) — только NTRAisRefglazingkindentityVidOstekleniyaTolkoNtr⟨/⟩

АИС (RefDGlazingKinds) НТР (RefGlazingKindEntity)
CoefRf0 ThermalResistance
CoefTf ShadingCoefficient
CoefKf SolarHeatGainCoefficient
BreathabilityKgPerM2h AirPermeability
Emissivity EmissionCoefficient

5.2 RefGateKindEntity (вид ворот) — только NTRAisRefgatekindentityVidVorotTolkoNtr⟨/⟩

АИС (RefDGateKinds) НТР (RefGateKindEntity)
CoefRv ThermalResistance
BreathabilityKgPerM2h AirPermeability

5.3 RefInsulationMaterialEntity / RefMaterialEntity — только NTRAisRefinsulationmaterialentityRefmaterialentityTolkoNtr⟨/⟩

АИС НТР
LVtMK ThermalConductivity

5.4 RefClimateRegionEntity (климатический район) — только NTRAisRefclimateregionentityKlimaticheskiyRayonTolkoNtr⟨/⟩

АИС (RefDClimateData) НТР (RefClimateRegionEntity)
LatitudeSr Latitude
LongitudeSr Longitude
DaysAvgTempLe8 DaysAvgTempLe8
AvgTempLe8 AvgTempLe8
MaxAvgWindSpeedLe8 MaxAvgWindSpeedLe8

5.5 RefBuildingPurposeEntity (назначение зданий) — только NTRAisRefbuildingpurposeentityNaznachenieZdaniyTolkoNtr⟨/⟩

АИС (RefDBuildingTypes) НТР (RefBuildingPurposeEntity)
AirM3PerHumanhour SpecificAirConsumptionM3PerHumanHour

6 Ограничения и особенностиAisOgranicheniyaIOsobennosti⟨/⟩

  1. Последовательное выполнение — все блоки внутри каждого сервиса выполняются строго последовательно из-за зависимостей по данным.
  2. Нет частичного повторного запуска — при падении на блоке N блоки 1..N-1 уже сохранены. Повторный запуск безопасен благодаря upsert.
  3. Фильтрация по компаниям — весь импорт ограничен компаниями из AllowedCompanyNames (LIKE-паттерны).
  4. Оба сервиса независимы — импортируют в разные схемы БД, могут запускаться независимо.
  5. Орг. структура дублируется — NavEntity, NavCompanyEntity и др. импортируются в обе БД отдельно.