Как разрешить функции SQL CLR выполняться в плане параллельного запроса, а также иметь разрешения на доступ к данным

У меня есть несколько функций SQL CLR (UDF), которые считывают данные из внешней базы данных DB2, размещенной на IBM iSeries (используя IBM DB2 .Net Provider). Чтобы у функции были необходимые разрешения для чтения этих данных, мне нужно украсить функцию атрибутом SqlFunction, у которого для свойства DataAccess установлено значение DataAccessKind.Read. Я также развертываю сборку как UNSAFE.

Время, затрачиваемое на чтение данных из базы данных DB2, относительно велико (например, 3 мс для простейшего ExecuteScalar).

Я использую эти UDF для эффективного слияния данных из базы данных DB2 с представлениями Sql Server.

Например, предположим, что моя UDF определена как

[SqlFunction(DataAccess = DataAccessKind.Read, IsDeterministic = true)] 
public static SqlMoney GetCostPrice(SqlString partNumber)
{
    decimal costPrice;
    // open DB2 connection and retrieve cost price for part
    return new SqlMoney(costPrice);
}

а затем используйте это в моем представлении SQL как:

select Parts.PartNumber,
       dbo.GetCostPrice(Parts.PartNumber) as CostPrice
from Parts

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

Существуют задокументированные методы принудительного выполнения плана запроса параллельно, а не последовательно, но эти методы имеют ограничения, налагаемые SQL Server, одно из которых заключается в том, что определенная в SQL CLR функция ДОЛЖНА иметь DataAccess = DataAccessKind.None.

Но если я установлю для DataAcessKind значение None, я получу исключение при попытке открыть любой DbConnection внутри функции.

И это моя проблема! Как я могу запустить свою определяемую пользователем функцию в плане параллельных запросов, при этом позволяя ей считывать данные из внешней базы данных?

Лучшая идея, которую я должен решить, - это жестко закодировать DataAccess = DataAccessKind.None в моем атрибуте SqlFunction, а затем во время выполнения в теле функции повысить разрешения с помощью Code Access Security, чтобы последующий код имел разрешения на открытие объектов DbConnection. .

Но я не могу понять, как это сделать? В качестве эксперимента я попробовал следующее

    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
    public static SqlMoney TestFunction()
    {
        var sqlPerm = new SqlClientPermission(PermissionState.Unrestricted);
        sqlPerm.Assert();

        using (var conn = new SqlConnection("context connection=true"))
        {
            conn.Open();
        }

        return new SqlMoney();
    }

и вызовите из Sql Server Management Studio с помощью:

select dbo.TestFunction()

но я продолжаю получать исключение безопасности...

Произошла ошибка .NET Framework во время выполнения определяемой пользователем подпрограммы или агрегата "TestFunction": System.InvalidOperationException: в этом контексте доступ к данным запрещен. Либо контекст является функцией или методом, не отмеченным DataAccessKind.Read или SystemDataAccessKind.Read, либо обратным вызовом для получения данных из метода FillRow табличной функции, либо методом проверки определяемого пользователем типа. System.InvalidOperationException: в System.Data.SqlServer.Internal.ClrLevelContext.CheckSqlAccessReturnCode(SqlAccessApiReturnCode eRc) в System.Data.SqlServer.Internal.ClrLevelContext.GetCurrentContext(приемник SmiEventSink, логический throwIfNotASqlClrThread, логический fAllowLinkImpersonationServer) в Microsoft.Server.SqlProperty .GetCurrentContext(SmiEventSink eventSink) в Microsoft.SqlServer.Server.SmiContextFactory.GetCurrentContext() в System.Data.SqlClient.SqlConnectionFactory.GetContextConnection(параметры SqlConnectionString, Object providerInfo, DbConnection owningConnection) в System.Data.SqlConnection.SqlConnectionFactory.CreateConnection(DbConnection параметры, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection) в System.Data.ProviderBase.DbConnectionFactory.CreateNonPooledConnection(DbConnection owningConnection, DbConnectionPoolGroup poolGroup) в System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection o wningConnection) в System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection externalConnection, DbConnectionFactory connectionFactory) в System.Data.SqlClient.SqlConnection.Open() в UserDefinedFunctions.UserDefinedFunctions.TestFunction()

У кого-нибудь есть идеи?

Заранее спасибо.

(кстати, я использую SQL 2008 с использованием .Net 3.5)


person Kev    schedule 23.02.2016    source источник
comment
Почему бы не использовать подключенную базу данных?   -  person Matthew Whited    schedule 26.02.2016
comment
Что вы подразумеваете под подключенной базой данных? Вы имеете в виду связанный сервер??? (Извините, если я туплю!)   -  person Kev    schedule 26.02.2016
comment
msdn.microsoft.com/en-us/library/ms188279.aspx   -  person Matthew Whited    schedule 26.02.2016
comment
Да, я имею в виду связанные серверы   -  person Matthew Whited    schedule 26.02.2016
comment
Провайдеры Linked Server Db2 (от MS) работают невероятно медленно. Поэтому никогда не использовал их, за исключением случайных заданий SSIS. Однако, если вы говорите, что я мог бы использовать его, не требуя прав UserDataAccess в моей пользовательской функции (?), тогда выигрыш в производительности от параллелизма может перевесить это.   -  person Kev    schedule 26.02.2016
comment
Прошли годы с тех пор, как я использовал DB2, поэтому я понятия не имею, как это повлияет на производительность, но я предполагаю, что она будет работать быстрее, чем SQL CLR... но мое предположение может быть неверным :)   -  person Matthew Whited    schedule 26.02.2016
comment
Если у вас возникли проблемы с производительностью, я бы посоветовал кэшировать ваше приложение или синхронизировать данные из DB2 в эту базу данных.   -  person Matthew Whited    schedule 26.02.2016


Ответы (1)


Насколько показывает мое тестирование (против SqlConnection для SQL Server), это можно сделать только с помощью обычного/внешнего соединения (т.е. не Context Connection = true) и добавления ключевого слова Enlist в строку подключения, установите значение false:

Server=DB2; Enlist=false;

Но, похоже, нет никакого способа заставить это работать при использовании Context Connection = true. Контекстное соединение автоматически становится частью текущей транзакции, и вы не можете указать какие-либо другие ключевые слова строки соединения при использовании контекстного соединения. При чем тут сделка? Ну, по умолчанию для Enlist стоит true, так что даже если у вас есть обычное/внешнее подключение, если вы не укажете Enlist=false;, то вы получите то же самое

Доступ к данным в этом контексте запрещен.

ошибка, которую вы получаете сейчас.

Конечно, это спорный вопрос, потому что в этом конкретном случае нет смысла использовать контекстное соединение, поскольку в этом случае потребуется использовать связанный сервер, и в комментарии к Вопросу было указано, что «Связанный сервер Db2 Провайдеры (от MS) невероятно медленные».

Также было указано, что, возможно, сработает использование TransactionScope с опцией Suppress. Это не может работать, потому что вам не разрешено создавать экземпляр объекта TransactionScope (с любой из трех опций: Required, RequiresNew или Suppress), если для DataAccess и SystemDataAccess установлено значение None (это их значение по умолчанию).

Также по поводу желания

повысить статус UserDataAccess пользовательской функции во время выполнения.

это просто невозможно из-за того, что UserDataAccess не является параметром времени выполнения. Он определяется при выполнении оператора CREATE FUNCTION (тот, который имеет AS EXTERNAL NAME [Assembly]... в качестве определения. Свойства UserDataAccess и SystemDataAccess — это метаданные, которые хранятся вместе с функцией. Вы можете увидеть настройку любого из них, используя встроенный OBJECTPROPERTYEX. в функции:

SELECT OBJECTPROPERTYEX(OBJECT_ID(N'SchemaName.FunctionName'), 'UserDataAccess');

Ваши два варианта кажутся:

  1. Используйте поставщика, который поддерживает ключевое слово Enlist, чтобы для него можно было установить значение false, или, если он не зачисляется по умолчанию, то в противном случае не требуется, чтобы для DataAccess было установлено значение Read. В соответствии с предлагаемой документацией для ознакомления (Интеграция универсальной универсальной базы данных DB2 для iSeries с for iSeries с Microsoft ADO .NET ), варианты выглядят так:

    • OleDb
    • ODBC
    • IBM DB2 для LUW .NET
  2. Создайте средний разрыв, представляющий собой веб-службу, которой функция SQLCLR может передать запрос, она будет использовать любого поставщика для получения информации и ответит информацией. В этом случае функция SQLCLR не выполняет никакого прямого доступа к данным, и веб-служба может выполнять собственное кэширование (вы сказали, что исходные данные меняются не так часто) для повышения производительности (даже если только кэширование значения для 1 - 5 минут). Да, это вводит внешнюю зависимость, но в остальном все должно работать как надо.

person Solomon Rutzky    schedule 23.02.2016
comment
Спасибо за ответ. Я оставил свойство IsDeterministric по умолчанию для краткости, но вы правы в том, что я ввел читателя в заблуждение (через месяц я отредактирую вопрос). На самом деле я установил его в true - исходные данные меняются не часто, и если кэширование пропускает изменение исходных данных (очень малая вероятность), то это не оказывает существенного влияния. Набор разрешений SAFE не требуется, я доказал, что могу создать параллельный план с UNSAFE сборкой. - person Kev; 23.02.2016
comment
@Kev Интересно сделать это в UNSAFE. Я никогда этого не видел. Не существует прямого способа обойти часть DataAccess; это даже используется для функции T-SQL (посмотрите на OBJECTPROPERTYEX(object_id, 'UserDataAccess')). У меня есть одна или две идеи, которые я могу опробовать позже и сообщить о результатах сегодня вечером. - person Solomon Rutzky; 23.02.2016
comment
@Kev Я обновил свой ответ после проведения некоторых тестов. - person Solomon Rutzky; 26.02.2016
comment
@Сруцкий. Спасибо. enlist=false не поддерживается поставщиком .net (см. стр. 118 redbooks. ibm.com/redbooks/pdfs/sg246440.pdf) — по умолчанию он все равно не включается в транзакции DTS. Но (более медленные) поставщики MS OLEDB DB2 поддерживают эту опцию, поэтому я буду экспериментировать с ними. Что касается контекстного соединения, это все еще возможно, если обернуть открытие/закрытие соединения внутри использования (new TransactionScope(TransactionScopeOptions.Suppress)), что может привести к желаемому результату. Я тоже попробую это. (Сегодня на это может не хватить времени /завтра). - person Kev; 26.02.2016
comment
@Kev Может быть, более подробно об этих деталях в вопросе? Код вашего примера очень вводит в заблуждение и далек от того, что вы пытаетесь сделать. Мне было интересно, почему это было SqlConnection при переходе на DB2, но я подумал, что у вас что-то получилось. Context Connection также не имеет смысла, так как он является только локальным и потребует связанного сервера для доступа к DB2, а вы указали, что не используете связанные серверы. Так что забудьте Context Connection, а TransactionScope не поможет, так как его нельзя использовать, если DataAccess = None (я протестировал все 3 варианта, и он так и не попал в строку Connection). - person Solomon Rutzky; 26.02.2016
comment
@Kev Я обновил свой ответ, добавив больше информации. Я просмотрел эту документацию IBM (очень устаревшую — относится только к .NET 1.1 ;-), но оказалось, что вы должны иметь возможность указать enlist=false для поставщика, который вы используете в настоящее время (начало страницы 119). Стоит попробовать. В противном случае вариант LUW выглядит многообещающе. - person Solomon Rutzky; 28.02.2016
comment
@srutsky enlist=false не подходит для моего провайдера .net. ODBC не вариант, потому что он медленный. Я экспериментировал с подходом левого поля, чтобы запустить отдельный поток внутри функции CLR и выполнить доступ к данным в этом потоке и вернуть результат вызывающему потоку. Это работает, но не элегантно, а также не допускает использования контекста conntect=true. В настоящее время пробую альтернативные драйверы, такие как LUW и Progress datadirect. вернусь сюда, как только у меня будут конкретные результаты. - person Kev; 04.03.2016
comment
@Кев Хорошо. Есть ли причина избегать драйвера LUW? А зачем вам контекстная связь? Вы используете обычное/внешнее соединение, а не контекст. - person Solomon Rutzky; 04.03.2016
comment
@Сруцкий. не избегая LUW, просто, возможно, мою проблему с производительностью можно было бы просто решить, если бы я нашел более быстрый драйвер. LUW (на бумаге) примерно такой же производительности, как текущий провайдер .net, но не исключает этого. Некоторым функциям было бы полезно читать данные о контекстном соединении, но я могу обойти это, так что ничего страшного. Было бы неплохо, если бы можно было найти решение, требующее минимальных обходных путей/хаков, вот и все. - person Kev; 04.03.2016
comment
@Kev При необходимости вы можете использовать разных провайдеров. Кроме того, вам нужно использовать SqlConnection для контекстного подключения и другой драйвер для прямого доступа к DB2. Если производительность LUW примерно такая же, как у текущего драйвера, почему бы просто не переключиться на него, поскольку он определенно поддерживает Enlist=false, по крайней мере, согласно документу, на который вы мне указали. Затем вы можете установить ReadAccess в None и покончить с этим, верно? - person Solomon Rutzky; 04.03.2016
comment
@Сруцкий. На 100% согласен с LUW, просто соединение не работает - думаю, это проблема либо с портом/брандмауэром, либо с конфигурацией сервера - возможно, такая же проблема с другими провайдерами - скоро разберемся, и тогда у меня будет более конкретный отзыв для тебя. подпишусь сейчас, так как здесь, в Великобритании, уже поздно - person Kev; 04.03.2016
comment
Эй, @Kev, ты когда-нибудь продвинулся в этом вопросе? - person Solomon Rutzky; 15.10.2016
comment
@Сруцкий. У меня было одно решение, которое, благодаря электронному письму от Адама Мачаника, состоит в том, чтобы создать еще один поток из метода SQLCLR, и этот поток выполняет доступ к базе данных, ждет завершения потока, возвращается к вызывающему потоку и возвращает результат. Выполнение доступа к БД в другом потоке означает, что метод sqlclr можно определить как DataAccess = DataAccessKind.None. Я поэкспериментировал с решением, и оно заработало, но решил, что проблемы со стабильностью, присущие многопоточности внутри SQLCLR, означают, что для получения надежного решения требуется много работы. Так что решил не запускать идею в производство :-( - person Kev; 30.10.2016
comment
@Кев Интересно. Итак, есть ли какая-то причина не принимать мой ответ, поскольку он фактически правильный, учитывая, что единственный способ обойти это довольно опасен и что на самом деле никто не будет делать? Если хотите, я могу добавить к моему ответу примечание о предложении Адама вместе с предупреждением не использовать такой метод, поскольку он может легко привести к непредсказуемому поведению. - person Solomon Rutzky; 31.10.2016