Skip to content

Type Projection в SCSM

Type Projection в SCSM published on 5 комментариев к записи Type Projection в SCSM

Если вы захотите создавать объекты в SCSM с помощью кода или скриптов, рано или поздно (скорее всего рано) вы столкнетесь с таким понятием как Type Projection. В данной статье я попытаюсь разъяcнить, что же это такое, для чего нужно и как его можно использовать, но я не буду рассматривать создание собственных Type Projection.

В эпоху, “когда небо было голубее, а деревья больше” даже трехзвенные системы (клиент-сервер-БД) редко использовали свою собственную модель данных. Чаще всего классы и методы подстраивались под структуру и особенности базы данных. Это и понятно – БД уже имеет все средства для работы с объектами, и изобретать что-то новое смысла не было. Если кто-то не знаком с реляционными базами данных, в частности с MSSQL, то давайте немного истории:

  • Все данные в MSSQL хранятся в виде таблиц.
  • Связи между таблицами можно устанавливать по определенному столбцу, поддерживается отношение один-к-одному, один-к-многим и многие-ко-многим.
  • Для получения данных сразу и нескольких связанных таблиц можно с помощью оператора SELECT JOIN.

Отличительная особенность таких систем – возможность оперировать данными напрямую в базе данных. Поэтому часто даже для коммерческих систем появлялись альтернативные интерфейсы, переписывалась логика и т.д.

С ростом объемов данных и сложности приложений, разработчики пришли к тому, что необходимо было создавать свою модель данных, и приспосабливать базу данных под эту модель. Для меня первой такой системой стал Sharepoint, где модель данных была абстрагирована от базы данных. В свою очередь это приводило к тому, что работать напрямую с базой данной стало практически невозможно, поэтому на плечи разработчика, кроме всего прочего, ложилась ответственность за создание интерфейсов доступа к данным. Одним из ключевых моментов таких интерфейсов является обслуживание связей. К примеру, у нас есть Инцидент. У него есть ряд полей, таких как название, описание, дата создания. Всё они являются простыми типами данных – строка, дата, число. Кроме этого, в инциденте есть поля Пользователь, Связанные инциденты и так далее. Логично, что эти поля должны являться ссылкой на другие объекты в модели данных. В Sharepoint это и была именно ссылка (reference) – вы получали значение, являющиеся просто указанием на другой объект в системе с указанием его идентификатора. Такой подход имеет ряд минусов:

  • в связи с тем, что в поле хранится только идентификатор, разработчик должен точно знать тип данных, используемый в данном поле, чтобы использовать затем нужные методы.
  • сложно получить связанный набор данных в одном запросе.

В SCSM разработчики Microsoft предложили радикально другой подход. Для получения объектов используется два вида методов: получение самих объектов и получение связанных данных. В первом случае мы получаем объект(ы) только с простыми полями, без ссылок на связанные объекты. Во втором случае обязательным параметров метода для получения объектов является описание отношений. Это описание определяет, какие связанные поля будут получены в запросе. Это описание получило название Type Projection (далее по тексту – TP). К сожалению, я не смог подобрать адекватного перевода этого термина, поэтому буду пользоваться английским вариантом.

Type Projection описывает, какие связи между различными классами будут использованы для запроса. К примеру, класс Инцидент имеет связь со многими классами в системе. Но при этом получать все эти поля в каждом запросе нет необходимости, чаще всего используется запрос, в котором необходимо получить сам инцидент и связанного с ним пользователя. Type Projection может быть вложенным, т.е. в одном запросе мы можем получать данные сразу о цепочке связанных объектов.

При описании Type Projection указывается целевой класс, для которого создается TP, а также компоненты этого TP. Каждый компонент определяет, какая из связей будет использоваться в данном TP.

Рассмотрим Type Projection на примере класса Инцидент. Как я уже говорил, он имеет массу связей, но чаще всего (по мнению разработчиков MS) используется связь инцидента с классом Пользователь в полях “Affected User” и “Assigned User”. Определение этого Type Projection в пакете управления выглядит следующим образом (да, это XML, а вы на что надеялись ;) ):

<typeprojection id="System.WorkItem.Incident.View.ProjectionType" type="CoreIncident!System.WorkItem.Incident" accessibility="Public">
   <component alias="AffectedUser" path="$Target/Path[Relationship='WorkItem!System.WorkItemAffectedUser']$"></component>
   <component alias="AssignedUser" path="$Target/Path[Relationship='WorkItem!System.WorkItemAssignedToUser']$"></component>
</typeprojection>

В строке 1 находится определение. ID – это внутреннее имя TP. Здесь стоит сделать оговорку. В документации и самом SCSM вы можете встретить еще один ID, обычно он имеет префикс InernalID и тип GUID. Этот InternalID хранится только в базе данных и генерируется в момент появления объекта в базе данных. Но т.к. этот GUID формируется по тем же принципам, что и в OpsMgr, для каждого уникального объекта он будет всегда одним и тем же на любой инсталляции.

Кроме этого в определении указывается целевой класс, для которого мы создаем TP, в данном случае это класс Инцидента.

В строках 2 и 3 мы определяем компоненты, т.е. какие связи класса мы будет использовать в этом ТР. Path является ссылкой на отношение, Alias – название связи.

ТР используются повсеместно и в консоли SCSM. Когда вы создаете то или иное представление, вы всегда используете одно из ТР. Вот так выглядит настройка представления, использующего показанный выше ТР:

4yhq24un

Вы видите, что доступны только два связанных поля. Но достаточно выбрать другой ТР, и мы получим полный набор связанных полей:

1slyhrkc

Такой подход имеет огромное количество плюсов, это и простота получения данных, и получение только необходимого объема данных т.д. Но несколько минусов могут перечеркнуть все плюсы. Как вы уже заметили, каждый класс имеет несколько ТР и количество полей сильно зависит от того, какой из ТР мы используем. В случае, если мы не верно выбрали ТР в момент проектирования, затем мы можем не получить необходимые нам данные. Скажем, мы хотим получить связанные с инцидентом запросы на изменение. Если мы будем использовать для этого ТР “System.WorkItem.Incident.View.ProjectionType”, то получим пустые поля RelatedWorkItem. Еще один огромный минус – это довольно большая сложность при использовании ТР в Powershell. Хотя тут скорее сама модель ТР не причем, а дело в реализации. Для унификации работы с объектами разработчики воспользовались generic-методами и классами. Такие методы могут работать с данными разных типов, собственно определение типа происходит в момент использования метода или определения класса. В C# такая конструкция экономит очень и очень много строчек кода, а вот в Powershell мы имеем ровно противоположную картину – для использования generic-методов надо написать довольно много дополнительного кода. Еще один минус, и честно говоря мне абсолютно не понятный – данные из ТР нельзя использовать как столбцы в представлениях. На картинке выше вы видите все поля, и можете их использовать, но это лишь в критерии. В отображаемых столбцах мы можем выбрать только связанные поля класса Пользователь (Affected User и Assigned User). По этой же причине мы не можем вставлять в шаблоны писем данные о связанных элементах.

Итак, мы разобрались (надеюсь :) ) что такое ТР, самое время показать как их использовать. Т.к. вряд ли тут наберется много программистов, все примеры будут на Powershell (к моему сожалению, т.к. на C# это выглядит гораздо проще и изящнее). Для начала получим все ТР для конкретного класса:

# Подгружаем библиотеку SCSM
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core") | Out-Null

$managementGroup = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup("localhost");
$mpCR = $managementGroup.ManagementPacks.GetManagementPack("System.WorkItem.ChangeRequest.Library", "31bf3856ad364e35", "7.0.5826.0")
# Этот guid - это класс инцидента, посмотреть все можно с помощью команды
#$managementGroup.EntityTypes.GetClasses() | select ID, Name, DisplayName
#$classCR = $managementGroup.EntityTypes.GetClass("e6c9cf6e-d7fe-1b5d-216c-c3f5d2c7670c")
$classCR = $managementGroup.EntityTypes.GetClass("A604B942-4C7B-2FB2-28DC-61DC6F465C68")

# Получаем все ТР для класса Инцидент.
# Мы могли вместо получения экземпляра класса использовать фильтр $_.TargetType.Name -eq  "System.WorkItem.Incident"
# но экземпляр нам всё равно понадобиться потом, так что я предпочитаю делать именно так:
$managementGroup.EntityTypes.GetTypeProjections() | ? {$_.TargetType -eq  $classCR} | % {
# выводим ключевые свойства
write-host "ID:"$_.ID
write-host "Name:"$_.Name
write-host "Display Name:"$_.DisplayName
write-host "TargetType:"$_.TargetType.Name
write-host "Endpoint:"$_.TargetEndpoint.Name
write-host "Components: Endpoint (Class)"
    $_.ComponentCollection | % {write-host "`t"$_.TargetEndpoint.Name"("$_.TargetType.Name")"}
write-host
}

Часть вывода:

ID: 1862825e-21bc-3ab2-223e-2a7f2439ba75
Name: System.WorkItem.Incident.View.ProjectionType
Display Name: Инцидент (типовой)
TargetType: System.WorkItem.Incident
Endpoint:
Components: Endpoint (Class)
	 RequestedWorkItem ( System.User )
	 AssignedWorkItem ( System.User )

Теперь мы можем получать данные из объектов. Для получения всех объектов (без фильтрации) необходимо:

  1. Получить экземпляр ТР
  2. Создать экземпляр класса ObjectProjectionCriteria, передав в качестве параметра ТР
  3. Выполнить метод GetObjectProjectionReader
  4. Получить данные

Для примера давайте получим все запросы на изменение:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core") | Out-Null

$managementGroup = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup("localhost");

# В данном случае мы получаем ТР по его имени с помощью возможностей Powershell
$projCR = $managementGroup.EntityTypes.GetTypeProjections() | ? {$_.Name -eq "System.WorkItem.ChangeRequestProjection"}

# Создаем критерий для отбора. Конструктор имеет несколько вариантов,
# один из них позволяет также сказать фильтр для отбора определенных объектов.
# см. http://msdn.microsoft.com/en-us/library/microsoft.enterprisemanagement.common.objectprojectioncriteria.ctor.aspx
# Мы получаем все объекты без всякого фильтра:
$criteria = new-object Microsoft.EnterpriseManagement.Common.ObjectProjectionCriteria($ProjCR)

# Далее мы создаем метод из generic-метода
# Первое, что необходимо сделать, найти определение нужной нам функции. В нашем случае это GetObjectProjectionReader:
# http://msdn.microsoft.com/en-us/library/ee455921.aspx
# Как видно, он должна принимать два параметра типа ObjectProjectionCriteria и ObjectQueryOptions
# Создает массив из этих типов
[Type[]]$MethodType = ([Microsoft.EnterpriseManagement.Common.ObjectProjectionCriteria],[Microsoft.EnterpriseManagement.Common.ObjectQueryOptions])
# Получаем нужный нам метод с соответствующими параметрами
$GetObjectProjectionReader = $managementGroup.EntityObjects.GetType().GetMethod("GetObjectProjectionReader",$MethodType)
# Т.к. метод GetObjectProjectionReader еще и возвращает данные в виде generic-типа,
# нам необходимо создать его с нужными выходным типом,
# в данном случае это EnterpriseManagementObject
$GetObjectProjectionReaderMethod = $GetObjectProjectionReader.MakeGenericMethod([Microsoft.EnterpriseManagement.Common.EnterpriseManagementObject])
# Теперь мы можем выполнять этот метод. Для этого надо вызвать Invoke,
# первым параметром мы передаем объект, для которого мы хотим вызвать метод, вторым параметром - аргументы этого метода
$allCRs = $GetObjectProjectionReaderMethod.Invoke($managementGroup.EntityObjects, ($criteria, [Microsoft.EnterpriseManagement.Common.ObjectQueryOptions]::Default))
# Если бы это был обычный метод, а не generic, вызов бы выглядел более привычно:
# $managementGroup.EntityObjects.GetObjectProjectionReader($criteria, [Microsoft.EnterpriseManagement.Common.ObjectQueryOptions]::Default)
# Как видите, в Powershell мы вынуждены выполнять три операции вместо одной. В C# всё выглядит гораздо более проще:
# allCRs = managementGroup.EntityObjects.GetObjectProjectionReader<EnterpriseManagementObject>(criteria, ObjectQueryOptions.Default);

#Теперь остается извлечь все CR-ы и вывести их свойства
foreach($cr in $allCRs)
{
    # Имя объекта - это его заголовок
    $cr.Object.Name
    # Перебираем все компоненты в ТР
    foreach($comp in $projCR.ComponentCollection)
    {
        # В данном случае Endpoint - это название поля в объекте
        write-host "`t"$comp.TargetEndpoint.Name
        # Перебираем все связанные объекты для каждого компонента, ведь их может быть несколько
        $cr[$comp.TargetEndpoint.Name] | % { write-host "`t`t"$_.Object.DisplayName }
    }

    # Данный код делает тоже самое, что и выше, но выводит только НЕ пустые связанные элементы
    #$keys = $cr | %{ $_.key.name } | sort -uniq
    #foreach($key in $keys)
    #{
    #    write-host "`t"$key
    #    $cr[$key] | % {  write-host "`t`t"$_.Object.DisplayName}
    #}

    # Теперь получаем все простые свойства объекта
    $cr.Object.Values | % {write-host "`t"$_.Type":"$_.Value}
    write-host
}

Часть вывода скрипта:

CR241
	 Activity
		 MA243: Apply Change
		 MA275:
		 RA242
		 MA277:
	 CreatedWorkItem
		 Антон М. Гриценко
	 AssignedWorkItem
	 RequestedWorkItem
	 RelatesToWorkItem_
		 IR164 - This is a test
	 RelatesToWorkItem
	 RelatesToConfigItem
	 HasRelatedWorkItems
	 FileAttachment
	 KnowledgeDocument
	 Reason :
	 Notes :
	 ImplementationPlan :
	 RiskAssessmentPlan :
	 BackoutPlan :
	 TestPlan :
	 PostImplementationReview :
	 TemplateId : StandardChangeRequest
	 RequiredByDate :
	 Status : ChangeStatusEnum.InProgress
	 Category : ChangeCategoryEnum.Standard
	 Priority :
	 Impact :
	 Risk :
	 ImplementationResults :
	 Area :
	 Id : CR241
	 Title : Standard Change Request from incident
	 Description :
	 ContactMethod :
	 CreatedDate : 15.10.2010 8:12:43
	 ScheduledStartDate :
	 ScheduledEndDate :
	 ActualStartDate : 15.10.2010 8:13:27
	 ActualEndDate :
	 DisplayName : CR241: Standard Change Request from incident

Поделиться в соц. сетях

Primary Sidebar