Есть ли жизнь на MARSe?


 


Давнишний постинг с sqlclub.ru, перепостуемый сюдыть (http://blogs.msdn.com/alexejs/archive/2009/07/10/marse.aspx) по причине недоступности оного.


-----------------------------------------------------------------------------------------


 


Вообще-то тему Марса я уже как-то затрагивал: http://sqlclub.ru/forum/viewtopic.php?t=538 J, посему сегодня под MARSом будем понимать Multiple Active Result Sets. Скорее всего, многие слышали, что да, появилась подобная фишка в 2005-м, а некоторые даже ее заюзали в своих приблудах. Для остальных этот топег дает краткое введение, че это за и почему оно надо.


 


Сперва смотрим, как оно было. Открываем студию, делаем какой-нибудь консольный проект и подключаем в References старенькую ADOшную (не .NET) библиотеку.


 


image001


 


В моем случае их нашлось целых 3: две Primary Interop Assemblies в VSTO к старому и новому офису и третья в стандартных .NET-обертках. После чего пишем следующий код. Я не буду его комментировать – здесь и так все понятно: две разные команды на одной коннекции, одна выполняется один раз, другая несколько последовательно.


 


static void Main(string[] args)


{


ADODB.Connection cnn = new ADODB.Connection();   


cnn.Open("Provider=SQLOLEDB;Server=(local);


Database=AdventureWorksLT;Integrated Security=SSPI", "", "",


(int) ADODB.ConnectOptionEnum.adConnectUnspecified);


 


      object пустышка = Type.Missing;


 


      ADODB.Command cmd1 = new ADODB.Command();


      cmd1.CommandText = "select * from SalesLT.Product";


      cmd1.ActiveConnection = cnn;


cmd1.Execute(out пустышка, ref пустышка, (int) ADODB.ExecuteOptionEnum.adOptionUnspecified);


 


      ADODB.Command cmd2 = new ADODB.Command();


      cmd2.CommandText = "select * from SalesLT.Customer";


      cmd2.ActiveConnection = cnn;


      for (int i = 0; i < 3; i++)


cmd2.Execute(out пустышка, ref пустышка, (int) ADODB.ExecuteOptionEnum.adOptionUnspecified);


 


      cnn.Close();


 }


 


Вот, говорят отдельные неустойчивые элементы, тыча приблизительно этим кодом, все работает. Все же ведь выполняется с одного соединения и еxceptionов не выкидывает. На самом деле, конечно, не с одного. Поставьте брейкпойнт на втором Execute:


 


image003


 


И выполните из SSMS следующий запрос


 


select s.session_id, s.host_process_id, s.client_interface_name,


db_name(r.database_id) db_name, user_name(r.user_id) user_name, st.text sql,


r.open_resultset_count, r.open_transaction_count, s.status, r.wait_type


from sys.dm_exec_sessions s


join sys.dm_exec_requests r on s.session_id = r.session_id


outer apply sys.dm_exec_sql_text(r.sql_handle) st


where s.program_name = 'Microsoft (R) Visual Studio (R) 2008'


 


который позвольте мне сейчас не объяснять, чтобы не отвлекаться. Это приблизительный аналог sp_who / sp_who2, они же вирт.табл. sysprocesses. Вот, что там будет:


 


 


image005


 


В VS прокрутите цикл один раз, снова встаньте на брейкпойнте. Повторите запрос в SSMS. Мы видим, что в действительности ADO втихаря открыла новое соединение под 2-й Execute:


 


image007


 


Прокрутите цикл еще раз. Под очередное выполнение той же команды завелось еще одно соединение:


 


image009


 


и т.д.


 


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


 


cnn.Properties["Multiple Connections"].Value = false;


 


Тогда мы просто получим ошибку при попытке открытия нового рекордсета на соединении, где уже есть живой


 


image011


 


Второй способ – следить самому и закрывать Recordset прежде, чем открывать новый на том же соединении. Т.е. примерно так:


 


static void Main(string[] args)


{


ADODB.Connection cnn = new ADODB.Connection();


            cnn.Open("Provider=SQLOLEDB;Server=(local);Database=AdventureWorksLT;Integrated Security=SSPI",


                "", "", (int)ADODB.ConnectOptionEnum.adConnectUnspecified);


      object пустышка = Type.Missing;


 


      ADODB.Command cmd1 = new ADODB.Command();


      cmd1.CommandText = "select * from SalesLT.Product";


      cmd1.ActiveConnection = cnn;


      ADODB.Recordset rst1 = cmd1.Execute(out пустышка, ref пустышка, (int)ADODB.ExecuteOptionEnum.adOptionUnspecified);


 


      ADODB.Command cmd2 = new ADODB.Command();


      cmd2.CommandText = "select * from SalesLT.Customer";


      cmd2.ActiveConnection = cnn;


      ADODB.Recordset rst2;


      for (int i = 0; i < 3; i++)


      {


            rst2 = cmd2.Execute(out пустышка, ref пустышка, (int)ADODB.ExecuteOptionEnum.adOptionUnspecified);


            rst2.Close();


      }


      cnn.Close();


}


 


Изменения по сравнению с предыдущим вариантом кода выделены. Если на соединении висит результат, провайдер SQLOLEDB (да и native тоже) порождает под новую команду новое соединение в дополнение к занятому. А что делать, если вам единомоментно нужно иметь два или более рекордсетов? Ну там вы их сравниваете или производите над ними еще какие-то совместные операции. Ответ очевиден – открывать для них новые соединения, либо ADO это сделает за вас (если ему явно не запретить) . Открытие соединения дорогостоящая операция, соединение отнимает много ресурсов – это общеизвестные истины. К тому же эти дополнительные соединения не пулируются, как вы видите. Если бы после первого прохода цикла соединение вернулось в пул, на второй итерации мы бы видели по-прежнему два соединения, а не три.  Короче, все плохо. Пишем следующий код:


 


static void Main(string[] args)


{


SqlConnection cnn = new SqlConnection("Server=(local);Database=AdventureWorksLT;" +


                "Integrated Security=true;MultipleActiveResultSets=true");


      cnn.Open();


      SqlCommand cmd1 = new SqlCommand("select * from SalesLT.Product", cnn);


      cmd1.ExecuteReader();


 


      for (int i = 0; i < 3; i++)


      {


            SqlCommand cmd2 = new SqlCommand("select * from SalesLT.Customer", cnn);


            cmd2.ExecuteReader();


      }


      cnn.Close();           


}


 


Принципиальный момент в строке соединения выделен болдом. Опция MultipleActiveResultSets=true включает возможность иметь несколько активных рекордсетов на одном соединении. Без нее поведение будет ровно таким же, как мы наблюдали в случае ADO: провайдер будет неявно плодить соединения под каждый новый рекордсет. Ставим брейкпойнт на cnn.Close(). Как мы помним, в этом месте у нас открывалось 4 соединения: одно от cmd1 и 3 от cmd2. Смотрим, что произойдет сейчас


 


image013


 


image015


 


Мы видим, что у нас открыто всего одно соединение с session_id = 52, на котором висят 4 exec_requesta соответственно тому, как мы поочередно отправляли на SQL Server команды. Обратите внимание, как прирастал open_resultset_count. Еще обратите внимание, если будете воспроизводить это у себя, что в запросе нужно поменять условие where


 


where s.program_name = '.Net SqlClient Data Provider'


 


Изменение провайдера на Native привело к тому, что наше приложение теперь стало по-другому представляться SQL Server. На что еще нужно обратить внимание? MARS позволяет иметь несколько активных результатов на одном соединении, но это не значит, что эти результаты могут относиться к одному и тому же объекту команды. Именно поэтому мы спрятали SqlCommand cmd2 внутрь цикла:


 


for (int i = 0; i < 3; i++)


{


      SqlCommand cmd2 = new SqlCommand("select * from SalesLT.Customer", cnn);


      cmd2.ExecuteReader();


}


 


Если бы было вот так:


 


SqlCommand cmd2 = new SqlCommand("select * from SalesLT.Customer", cnn);


for (int i = 0; i < 3; i++)


{               


cmd2.ExecuteReader();


}


 


было бы вот что:


 


image017


 


Имейте это в виду, потому что народ часто на этом попадается. Достаточно сделать поиск на «There is already an open DataReader associated», чтобы увидеть, сколько народу жалуется по форумам. Типа как же так, а как же MARS? Но еще веселее почитать ответы «спецов». Гы. Поэтому еще раз: MARS – это возможность иметь несколько активных результатов на одном соединении, но не на одной команде.


 


Как я уже говорил, с практической точки зрения MARS нужен, чтобы не плодить соединения, когда на клиенте или на middleware требуется провести совместную обработку нескольких рекордсетов. Например, вот такой извращенный вариант джойна ProductCategory c Products.


 


static void Main(string[] args)


{


SqlConnection cnn = new SqlConnection("Server=(local);Database=AdventureWorksLT;" +


                "Integrated Security=true;MultipleActiveResultSets=true");


            cnn.Open();


 


      SqlCommand cmd1 = new SqlCommand("select * from SalesLT.ProductCategory", cnn);


      SqlDataReader sdr1 = cmd1.ExecuteReader();


 


      while (sdr1.Read())


      {


            SqlCommand cmd2 = new SqlCommand("select * from SalesLT.Product " + "where ProductCategoryID = @ProductCategoryID", cnn);


            cmd2.Parameters.AddWithValue("@ProductCategoryID", sdr1["ProductCategoryID"]);


            SqlDataReader sdr2 = cmd2.ExecuteReader();


 


            while (sdr2.Read()) Console.WriteLine("{0} <- {1}", sdr1["Name"], sdr2["Name"]);


      }


      Console.WriteLine("\nPress any key to continue or any other key to stop...");


      Console.ReadKey(true);


      cnn.Close();


}


 


MARS доступен для нативного клиента SQL Server 2005 и поддерживается во всех редакциях, включая Express. Про его наличие в других провайдерах мне неизвестно.


В этой жизни ничего не дается даром, и применение MARS может повлечь свои негативные моменты. Информация к размышлению. В ту пору, когда SQL Server 2005 звался еще Юкон, MARS был включен по умолчанию, т.е. вот эта опция MultipleActiveResultSets=true  подразумевалась автоматически. Где-то на промежутке между второй бетой и RC мужики подумали и решили ее по умолчанию загасить нафиг от греха. Как говорил известный системный аналитик Винни-Пух, «Это ж-ж-ж неспроста». Но об этом в следующий раз.


 


Для нативного клиента SQL Server 2008 MARS, само собой, тоже доступен. Просто этот пост давно писался.


 


 


 


 


 


Skip to main content