(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 - тоді поширюються усі обьекти у пакеті компяляції. Саме таку функцію, що поширюєтьcя будь який класс - я покажу нище першою, у коді нище це параметр "value As T".

Тип класу, який поширюються (у данному випадку "T") задається у звичайному сінтаксісу Дженеріків, тобто після назви функції необхідно визначити "(Of T as contraints)", a contraints тут необхідні інтерфейси, яки повинні бути присутними у класі, який поширюється. У данному випадку перша функція взагалі мега-універсальна і може бути використана взагалі для будь якої мети, навіть ніяк не пов'язаною з 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
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>  <MAIL ME>  <ABOUT ME>  < THANKS ME>