Импорт данных из АИС
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:
- Загрузить исходные данные из АИС
- Загрузить существующие записи из целевой БД по
AisId - Если запись с таким
AisIdесть — обновить поля через Set-методы - Если нет — создать новую сущность и добавить в 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— upsertRegBuildingEntity. Возвращает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⟨/⟩
- Последовательное выполнение — все блоки внутри каждого сервиса выполняются строго последовательно из-за зависимостей по данным.
- Нет частичного повторного запуска — при падении на блоке N блоки 1..N-1 уже сохранены. Повторный запуск безопасен благодаря upsert.
- Фильтрация по компаниям — весь импорт ограничен компаниями из
AllowedCompanyNames(LIKE-паттерны). - Оба сервиса независимы — импортируют в разные схемы БД, могут запускаться независимо.
- Орг. структура дублируется —
NavEntity,NavCompanyEntityи др. импортируются в обе БД отдельно.
Структурные связи
Использует
- Доменная сущность Вид ворот
- Доменная сущность Вид остекления
- Доменная сущность Классы энергетической эффективности
- Доменная сущность Климатический район (сущность)
- Доменная сущность Материал
- Доменная сущность Материал утеплителя
- Доменная сущность Назначение зданий
- Доменная сущность Тип крыши