Мої поширення 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-процедур у тих випадках, коли це мені зручніше.
<SITEMAP> <MVC> <ASP> <NET> <DATA> <KIOSK> <FLEX> <SQL> <NOTES> <LINUX> <MONO> <FREEWARE> <DOCS> <ENG> <CHAT ME> <ABOUT ME> < THANKS ME> |