(NET) NET (2013 год)

Мої поширення Linq-to-SQL

Нещодавно у VB.NET з'явилася нова цікава можливість, додавати до статичної частини базового класу свої власні функції і користуватися ними у своєму коді, звичайно також у всіх екземплярах класів, які побудовані від того базового класу, що був поширений. Технологія Extension виглядає якось протилежно Inherits, тому що при використанні Inherits ви далі будете працювати вже з новим класом Derived types, які наслідувані від базового, а при використанні Extension ви зможете працювати безпосередньо з базовим класом, який ви поширили. З другого боку Extension виглядає як протилежність до Implements, який переносить у новий класс з базового класу лише визначення інтерфейсів без коду, а Extension переносить і код і визначення інтерфейсів, та ще й нібито у протилежному напрямку.

Найбільш важливим визначенням Extension є те, що поширюватися може лише СТАТИЧНА секція коду базового класу. Якщо у Шарпі це забезпечується простим визначенням класу як static, то компілятор Бейсіка вимагає, щоб всі Extension були визначені у окремих модулях (Модулі бейсіка є щось середньо між Namespace і Static у Шарпі) - з додатком специфічного атрибута <System.Runtime.CompilerServices.Extension()>.

Синтаксично Extension-функціх трохи відрізняються від звичайних, бо мають на один параметр більше, ніж можна побачити при їх визові. Цей параметр записуються в загальному переліку параметрів функції першим і він задає той самий базовий клас, який поширюється цією функцією. Дуже цікаво, якщо цей класс взагалі System.Object - тоді поширюються усі об'єкти у пакеті компіляції. Саме таку функцію, що поширюється будь який класс - я покажу нище першою, у коді нище це параметр "value As T".

Тип класу, який поширюються (у данному випадку "T") задається у звичайному синтаксису Дженеріків, тобто після назви функції необхідно визначити "(Of T as constraints)", a constraints тут необхідні інтерфейси, яки повинні бути присутніми у класі, який поширюється. У данному випадку перша функція взагалі мега-універсальна і може бути використана взагалі для будь якої мети, навіть ніяк не пов'язаною з Linq. У таких випадках, коли ми ніяких інтерфейсів не вимагаємо - контрейнс можно просто записати "as class".

Ну і ось перша функція поширення.


   1:  Module LinqToSqlExtension0
   2:      ''' <summary>
   3:      ''' Create new object if it nothing (attention! parameters polimorphism not supported)
   4:      ''' </summary>
   5:      ''' <typeparam name="T"></typeparam>
   6:      ''' <param name="value"></param>
   7:      ''' <param name="ParametersForNew">If object instance created without parameters, ParametersForNew may be omitted or may be nothing  </param>
   8:      ''' <returns>return reference to object instance</returns>
   9:      ''' <remarks>(attention! parameters polimorphism not supported)</remarks>
  10:      <System.Runtime.CompilerServices.Extension()> _
  11:      Public Function NewIfNull(Of T As Class)(ByRef value As T, ByVal ParamArray ParametersForNew() As Object) As T
  12:          If value Is Nothing Then
  13:              Dim Types(0) As Type
  14:              Types(0) = GetType(T)
  15:              Dim ObjConstructors() As Reflection.ConstructorInfo = GetType(T).GetConstructors
  16:              If ParametersForNew Is Nothing Then
  17:                  'шукаємо CTOR без параметрів
  18:                  For Each One In ObjConstructors
  19:                      If One.GetParameters.Count = 0 Then
  20:                          value = ObjConstructors(0).Invoke(ParametersForNew)
  21:                          Return value
  22:                      End If
  23:                  Next
  24:                  Throw New Exception("ParametersForNew is nothing, but constructor wihtout parameters is absent")
  25:              Else
  26:                  Dim ParametersCount As Integer = ParametersForNew.Count
  27:                  For i As Integer = 0 To ObjConstructors.Count - 1
  28:                      If ObjConstructors(i).GetParameters.Count = ParametersCount Then
  29:                          'є конструктор, який має стільки ж параметрів, скілки передали у ParametersForNew
  30:                          value = ObjConstructors(i).Invoke(ParametersForNew) 'Polimorphism not supported !!!
  31:                          Return value
  32:                      End If
  33:                  Next
  34:                  Throw New Exception("ParametersForNewhas " & ParametersCount & " parameters, but constructor with " & ParametersCount & " parameters with the same type is absent")
  35:              End If
  36:          Else
  37:              Return value
  38:          End If
  39:      End Function
  40:  End Module

Як я вже казав вище, цю функцію можна використовувати для будь-якого об'єкту, але у LINQ я її звичайно використовую у для утворення DataContext'у.




Але зверніть увагу на цікавий побічний ефект при застосуванні цієї функції до ДатаКонтексту Linq. Використовування старого контексту Linq приводить до кешування даних! Тобто, якщо ви зробили оновлення даних, заходите на нову форму, там у вас записан виклик db1.NewIfNull(), то ви отримаєте старі дані з кешу, а не з бази. Що отримати оновлені дані вам потрібно отримати новий контекст. Тобто якщо цю функцію робити спеціально для Linq, менш універсальною, то можна зробити її варіант GetContext (ClearCache as boolean).


   1:  Module LinqToSqlExtension4
   2:      ''' <summary>
   3:      ''' Return Linq-to-SQL context
   4:      ''' </summary>
   5:      ''' <typeparam name="T"></typeparam>
   6:      ''' <param name="value"></param>
   7:      ''' <param name="ClearCache">True, if need clear cache</param>
   8:      ''' <returns>Linq-to-SQL context</returns>
   9:      <System.Runtime.CompilerServices.Extension()> _
  10:      Public Function GetContext(Of T As System.Data.Linq.DataContext)(ByRef value As T, ByVal ClearCache As Boolean) As T
  11:          If value IsNot Nothing And Not ClearCache Then
  12:              Return value
  13:          Else
  14:              'create new
  15:              Dim ObjConstructors() As Reflection.ConstructorInfo = GetType(T).GetConstructors
  16:              'шукаємо CTOR без параметрів
  17:              For Each One In ObjConstructors
  18:                  If One.GetParameters.Count = 0 Then
  19:                      value = ObjConstructors(0).Invoke(Nothing)
  20:                      Return value
  21:                  End If
  22:              Next
  23:          End If
  24:      End Function
  25:  End Module



Наступне найбільш поширене застосування Linq-to-SQL - це або оновлення деяких параметрів в таблі, або додавання нового запису до табли. Тобто це найбільш поширена дія ось такого плану:


   1:  Create procedure UpdateURL 
   2:  @URL as varchar (50)
   3:  as
   4:  IF NOT EXISTS (select 1 from  [Gruveo].[dbo].[ProxyTab] where URL=@URL)
   5:     BEGIN
   6:        Insert  [Gruveo].[dbo].[ProxyTab]
   7:        Values  (GETDATE(),@URL)
   8:     END
   9:  ELSE
  10:        Update  [Gruveo].[dbo].[ProxyTab]
  11:        set     CrDate=GETDATE() 
  12:        where   URL=@URL

У якості приклада тут і далі ми будемо дивитися на ось таку просту табличку, вона досить добре продемонструє весь код єкстеншен-функцій далі.




Дія Insert-or-Update повторюється і повторюється у багатьох проектах, тому я вирішив поширити Linq-to-SQL своєю власною функцією, яка б виконувала ті ж самі дії і якою було б зручно користуватися.

Якщо б я не зробив таке поширення Linq-to-SQL то кожна подібна операція Insert-or-Update вимагала би десь ось такого коду:


   1:              Dim X As IEnumerable(Of ProxyTab) = (From Y In db1.ProxyTabs Select Y Where Y.URL = Full_ProxyURL).ToList
   2:              If X.Count = 0 Then
   3:                  db1.ProxyTabs.InsertOnSubmit(New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL})
   4:              Else
   5:                  For Each One As ProxyTab In X
   6:                      One.CrDate = Now
   7:                  Next
   8:              End If

Такий стандартний код дуже зрозумілий та в ньому легко робити модифікації, але його завжди так багато, що він забруднює весь код і за ним не побачиш сенсу програми. До того ж, ця операція Insert-or-Update постійно повторюється з різними таблами, і цей шаблон коду повторюється кожний раз з іншими іменами.

Саме тому я поширив стандартні мікрософтовськи методи Linq-to-SQL таким чином, щоб можна було просто задавати різні имена табл та різні умови оновлення табл.


   1:  Module LinqToSqlExtension1
   2:      ''' <summary>
   3:      ''' First prm - new record in table ;
   4:      ''' Second prm - checkng expression, that apply to table ; 
   5:      ''' Return True if data inserted
   6:      ''' </summary>
   7:      ''' <typeparam name="T"></typeparam>
   8:      ''' <param name="Table"></param>
   9:      ''' <param name="SelectPredicate">First prm - checking expression, appling to table, for example:  Function(e) e.URL = Full_ProxyURL</param>
  10:      ''' <param name="NewEntity">Second prm - new record in table, for example:  New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL}</param>
  11:      ''' <returns>Return True if data inserted</returns>
  12:      ''' <remarks>If Not db1.ProxyTabs.InsertIfNotExists(Function(e) e.URL = Full_ProxyURL, New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL}) Then  db1.ProxyTabs.UpdateForCondition(Function(e) e.URL = Full_ProxyURL, Sub(e) e.CrDate = Now)</remarks>
  13:      <System.Runtime.CompilerServices.Extension()> _
  14:      Public Function InsertIfNotExists(Of T As Class)(ByVal Table As Data.Linq.Table(Of T),
  15:                                                             ByVal SelectPredicate As Expressions.Expression(Of Func(Of T, Boolean)),
  16:                                                             ByVal NewEntity As T) As Boolean
  17:          If Not Table.Any(SelectPredicate) Then
  18:              Table.InsertOnSubmit(NewEntity)
  19:              Table.Context.SubmitChanges()
  20:              Return True
  21:          Else
  22:              Return False
  23:          End If
  24:      End Function
  25:   
  26:  End Module



І відтепер той же самий код оновлення (який ви побачили вище вже двічи - у вигляді SQL та у вигляді простого бейсику) виглядає ось так:


   1:              If Not db1.ProxyTabs.InsertIfNotExists(Function(e) e.URL = Full_ProxyURL,
   2:                  New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL}) Then _
   3:                  db1.ProxyTabs.UpdateForCondition(Function(e) e.URL = Full_ProxyURL, Sub(e) e.CrDate = Now)

Взагалі це питання, настільки цей код зрозумілий, бо тут використовуються Lambda-Expression, Анонімні функції та Action. Які компілятор перекомпілює у більш зрозумілу форму, наприклад "Sub(e) e.CrDate = Now" компілюється у звичайну функцію з прихованим _Lambda$__XXXX




Тобто ми отримали значне скорочення синтаксису стандартних операцій проги. Ну і друга сіметрічна-протилежна функція UpdateForCondition, використання якої ви побачили вище, виглядає ось так:




   1:  Module LinqToSqlExtension2
   2:      ''' <summary>
   3:      ''' First prm - checking expression, appling to table, for example:  Function(e) e.URL = Full_ProxyURL;
   4:      ''' Second prm - Action, for example: Sub(e) e.CrDate = Now;
   5:      ''' Return True if data updated
   6:      ''' </summary>
   7:      ''' <typeparam name="T"></typeparam>
   8:      ''' <param name="table"></param>
   9:      ''' <param name="SelectPredicate">First prm - checking expression, appling to table, for example:  Function(e) e.URL = Full_ProxyURL</param>
  10:      ''' <param name="UpdateAction">Second prm - Action, for example: Sub(e) e.CrDate = Now</param>
  11:      ''' <returns>Return True if data updated</returns>
  12:      ''' <remarks>If Not db1.ProxyTabs.InsertIfNotExists(Function(e) e.URL = Full_ProxyURL, New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL}) Then  db1.ProxyTabs.UpdateForCondition(Function(e) e.URL = Full_ProxyURL, Sub(e) e.CrDate = Now)</remarks>
  13:      <System.Runtime.CompilerServices.Extension()> _
  14:      Public Function UpdateForCondition(Of T As Class)(ByVal table As Data.Linq.Table(Of T),
  15:                                                              ByVal SelectPredicate As Expressions.Expression(Of Func(Of T, Boolean)),
  16:                                                              ByVal UpdateAction As Action(Of T)) As Boolean
  17:   
  18:          'Dim X As IEnumerable(Of ProxyTab) = db1.ProxyTabs.Where(Function(e) e.URL = Full_ProxyURL).ToList
  19:          'For Each One As ProxyTab In SelectedRows
  20:          '    One.CrDate = Now
  21:          'Next
  22:   
  23:          Dim SelectedRows As System.Collections.Generic.IEnumerable(Of T) = table.Where(SelectPredicate)
  24:          If SelectedRows.Count > 0 Then
  25:              For Each One As T In SelectedRows
  26:                  UpdateAction.Invoke(One)
  27:              Next
  28:              table.Context.SubmitChanges()
  29:              Return True
  30:          Else
  31:              Return False
  32:          End If
  33:      End Function
  34:   
  35:  End Module

Ну, і як ви мабуть вже зрозуміли, буде і третя функція, яка зробить все, що було зроблено у SQL-процедурі UpdateURL в одну стрічку кода.




   1:  Module LinqToSqlExtension3
   2:      ''' <summary>
   3:      ''' First prm - checking expression, appling to table, for example:  Function(e) e.URL = Full_ProxyURL;
   4:      ''' Second prm - new record in table, for example:  New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL};
   5:      ''' Third prm - Action, for example: Sub(e) e.CrDate = Now;
   6:      ''' Return True if inserted or false if update
   7:      ''' </summary>
   8:      ''' <typeparam name="T"></typeparam>
   9:      ''' <param name="Table"></param>
  10:      ''' <param name="SelectPredicate">First prm - checking expression, appling to table, for example:  Function(e) e.URL = Full_ProxyURL</param>
  11:      ''' <param name="NewEntity">Second prm - new record in table, for example:  New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL}</param>
  12:      ''' <param name="UpdateAction">Third prm - Action, for example: Sub(e) e.CrDate = Now</param>
  13:      ''' <returns>Return True if inserted or false if update</returns>
  14:      ''' <remarks>db1.ProxyTabs.InsertOrUpdateTable(Function(e) e.URL = Full_ProxyURL, New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL}, Sub(e) e.CrDate = Now)</remarks>
  15:      <System.Runtime.CompilerServices.Extension()> _
  16:      Public Function InsertOrUpdateTable(Of T As Class)(ByVal Table As Data.Linq.Table(Of T),
  17:                                                               ByVal SelectPredicate As Expressions.Expression(Of Func(Of T, Boolean)),
  18:                                                               ByVal NewEntity As T,
  19:                                                               ByVal UpdateAction As Action(Of T)) As Boolean
  20:          Dim SelectedRows As System.Collections.Generic.IEnumerable(Of T) = Table.Where(SelectPredicate)
  21:          If SelectedRows.Count > 0 Then
  22:              For Each One As T In SelectedRows
  23:                  UpdateAction.Invoke(One)
  24:              Next
  25:              Table.Context.SubmitChanges()
  26:              Return False
  27:          Else
  28:              Table.InsertOnSubmit(NewEntity)
  29:              Table.Context.SubmitChanges()
  30:              Return True
  31:          End If
  32:      End Function
  33:   
  34:  End Module

Викликається ця функція ось так:


   1:              db1.ProxyTabs.InsertOrUpdateTable(Function(e) e.URL = Full_ProxyURL,
   2:                                                New ProxyTab With {.CrDate = Now, .URL = Full_ProxyURL},
   3:                                                Sub(e) e.CrDate = Now)

І виконує точно ту ж саму роботу, що і SQL-процедура UpdateURL.


Але що тут найбільше цікаве? Що трохи виходить за межи теми Extension-функцій. Я досить довго не приймав взагалі Linq, і мої нотатки на моєму сайті називалися якось так - Извлекаем пользу из LINQ. Чому так?

А тому, якщо ви подивитесь на простий виклик процедури і порівняєте його з кодом виклика (навіть найпросунутого та найскороченого варіанту InsertOrUpdateTable - то що ви побачите? Який код простіше




Молодим програмістам, кто взагалі не має реляційного складу думок, який почав займатися програмуванням починаючи з Linq - підходи маніпуляції з даними на рівні програми можуть здаватися нормальними. Але програмістам, які працюють 30-40 років з різноманітними базами даних, наприклад таким як я громіздкість та складність LINQ можуть перевищувати його переваги.

Особливо, якщо ви розумієте весь процесс еволюції засобів доступу до даних - Класифікація засобів роботи з даними. і (особливо) якщо в проєкті використовуються найбільш складні ORM. У яких навіть документацію можна вивчати місяцями. І все це замість того, щоб просто написати декілька простіших стрічок на SQL.

Але, як ви бачите, навіть я активно використовую Linq. Поширюю його власними Extension-функціями. І комбіную використання LINQ з прямим використанням SQL-процедур у тих випадках, коли це мені зручніше.





Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21
Link to this page: http://www.vb-net.com/Linq-To-SQL-Extension/index.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>