Connection, ListObject ve QueryTable nesneleri
Bu bölümde, DAO ve ADO'dan yöntem olarak biraz farklılaşan, refreshlenebilir bağlantı tekniklerine değineceğiz. Bu sayfada anlatılanlar için Dış veri kaynaklarıyla çalışmak sayfasında anlatılan konuları iyi bilmek gerekiyor. Eğer bunları bilmiyorsanız öncelikle o sayfayı incelemenizi sonra buraya gelmenizi tavsiye ederim. Örneklerdeki kodların olduğu dosyayı ve veritabanını buradan indirebilirsiniz.
Önceki bölümde DAO ve ADO ile bir veritabanına bağlanıp nasıl veri çekileceğini gördük. Bunların ortak noktası datayı değişkenler/diziler içinde depolayarak veya metodlar aracılığı ile Excel'e yapıştırmak şeklindeydi. Bu yöntemlerle elde edilen veri, Data menüsünden veya sağ tıklanarak refreshlenemez. Yani DAO/ADO refresh yapılamayan ve statik data getiren yöntemlerdir. Ancak ilgili kod tekrar çalıştırılarak (ilgili kod bir butona atanabilir), güncel data getirilebilir, yani dolaylı yoldan bir nevi refresh uygulanabilir. Refresh yapılabilirliğin VBA açısından önemi Refresh ve RefreshAll metodlarının kullanım kolaylığındandır.
Excel’den veritabanlarına ulaşmanın bir yolu da MS Query veya Data Connection Wizard aracılığı ile refreshlenebilir bir Tablo (VBA karşılığı ListObject) yaratmaktır. Bu kısımda bu yöntemlere bakıyor olacağız, tabi sadece VBA ile ilgili kısmına. Bu tür bağlantı yaratım tekniklerini biliyor olduğunuzu varsayacağım, bilmeyenler buradan detay bilgi edinebilir.
Refreshlenebilir bir dosyayı ilk kez oluşturma işini genelde Excel içinden manuel yaparız. Ancak bazı durumlarda VBA ile de yapmamız gerekebilmektedir. Yine de ilk Connection String bilgilerini vs. ilk başta Excel’den yapmak daha kolaydır, hatta macro recorder ile oluşan kodu alıp, aradaki gereksiz kodu temizledikten sonra VBA kodunu hazırlayabilirsiniz.
Bunun refresh işlemini ise VBA’e bırakabiliriz, tabii ki manuel olarak refresh edilebilir ancak bu sayfada VBA ile bunun yapılış yollarını öğreneceğiz. Zira bu bilgiler schedule işlemlerinde kritik öneme sahiptir.
Önemli Classlar
DAO ve ADO’dan farklı olarak bu sefer herhangi bir library’yi referans olarak eklememize gerek bulunmamaktadır. Bunun yerine Excel'in açılmasıyla mevcut bulunan 3 sınıf/nesne üzerinde çalışıyor olacağız:
- Connection(s) sınıfı: Varolan bağlantılarla ilgili işlem yapılmasını veya bilgi edinilmesini sağlar.
- ListObject(s) sınıfı: Yeni connection kurmamızı sağlar. Nihai liste Table şeklindedir.
- QueryTable(s) sınıfı: Yeni connection kurmamızı sağlar. Nihai liste Table şeklinde değildir.
Şimdi bunlara detaylıca bakalım.
Connections’ın sonundaki "s" harfinden bunun bir collection olduğunu anlıyoruz. Normalde hemen her collectiona ait bir de sonunda “s” olmayan nesnesi vardır, Workbooks/Workbook gibi. Ancak Connections için bu tipte bir nesne yok. Bununla ilgili hiçbir kaynak bulamadım ancak benim tahminim tek bir tür connection olmaması. Bizim burada ilgileneceğimiz nesne WorkbookConnection’dır. WorkbookConnection nesnesi tüm dış bağlantıları yöneten nesnedir. Bunun da aşağıdaki gibi kendi içinde alt türleri vardır:
- 2, xlConnectionTypeODBC
- 1, xlConnectionTypeOLEDB
- 4, xlConnectionTypeTEXT
- 5, xlConnectionTypeWEB
Diğer birkaç tip daha var, ama onlar bizi şu aşamada ilgilendirmiyor.
Connection’ların güzelliği bir kere yaratıldıklarında Excel içinde depolanabilir olmalarıdır. Connectionlar, ayrı bir connection dosyasında da tutulabilir. Bunlar genelde ODC uzantılı dosyalardır. Ama biz bu şekilde çalışmayacağız, Excel içinde depolayacağız. Daha önceden bahsettiğim gibi bazen de direkt kodun içinde connection yaratımı olabilir, dolayısıyla dosya ilk açıldığında connection yoktur, onu yaratan kod tetiklenince oluşur. Her ne kadar çok sık ihtiyacımız olmayacaksa da bu yöntemi de göreceğiz.
Her bir QueryTable, ListObject ve harici dataya başvuran bir PivotCache yaratıldığında yeni bir WorkbookConnection nesnesi yaratılır. Bu şekilde dolaylı olarak yaratılan connectionlara biraz aşağıda bakacağız.
Sonradan Existing Connections içinden kullanılmak üzere standalone (tek başına duran) WorkbookConnection nesnesi de yaratılabilir, ama buna çok gerek olacağını sanmıyorum, o yüzden yeni yaratım yerine varolan connectionlar üzerinde duracağız.
ConnectionString
Bir connection’ın en önemli öğesi Connection String'dir. DAO ve ADO’da gördüğümüz gibi bu öğe, dataya nasıl bağlanılacağını belirten yönergeyi içerir. Bunun içinde genellikle bağlantı türünün ODBC/OLEDB mi olduğu, Driver/Provider’in ne olduğu, varsa kullanıcı adı ve şifreler ve diğer parametreler bulunur.
Çeşitli veritabanlarına bağlanma yöntemlerini www.connectionstrings.com adresinden bulabilirsiniz.
Connection'a Erişim ve Varolan Connection'ı Editleme
Bir kez daha belirtmek gerekirse biz daha çok Connectionların yeni yaratımlarından ziyade mevcut Connectionların özelliklerini okuma/değiştirme işlemleri yapacağız.
Connection’ın kendisine Connections koleksiyonuna index veya isim vererek ulaşabiliyoruz.
Activeworkbook.Connections(1)
Activeworkbook.Connections("Query from PDWH_USR")
Connection’ın kendisini elde ettikten sonra bunla ilgili işlemleri yapmak için genelde Connection’ın tipine göre ilgili property’yi seçip daha çok bunun üzerinden işlem yapıyoruz, Connection’ın kendisiyle pek bi işimiz olmuyor. Yani bağlantı tipimiz ODBC ise Activeworkbook.Connections(1) yerine Activeworkbook.Connections(1).ODBCConnection üzerinde işlem yapıyoruz.
Connection’ı kendisiyle ilgili olarak tek ihtiyaç duyacağımız bilgi connection’ın tipidir. Yani ODBC ise farklı işler olsun OLEDB ise farklı şeyler olsun demek için tip bilgisine ihtiyaç duyarız. Bunları da yukarıda görmüştük; ODBC için tip xlConnectionTypeODBC olup bunun nümerik değeri 2'dir. Buna property Activeworkbook.Connections(1).Type şeklinde ulaşabiliriz.
Bu tip bilgisine ulaştıktan sonra connection tipine göre ilgili property seçilir: Activeworkbook.Connections(1).ODBCConnection gibi. Bunun da sadece birkaç propertysine ihtiyaç duyacağız. En önemlisi, bize ConnectionStringi veren Connection propertysi’dir. Gördüğünüz gibi ConnectionString diye bir property yok onun yerine Connection var: Activeworkbook.Connections(1).ODBCConnection.Connection
Mesela connection tipi ODBC ise ConnectionString ... olsun demek için aşağıdaki kodu yazarız.
If Activeworkbook.Connections(i).Type = 2 Then
Activeworkbook.Connections(i).ODBCConnection.Connection="....."
End If
Bu ConnectionString’i elde etmenin bir yolu daha var, bunu ilgili kısımda ayrıca göreceğiz ancak ön bilgi adına aşağıda veriyorum, QueryTable'lar üzerinden:
ActiveSheet.ListObjects(1).QueryTable.Connection 'access, oracle v.s
'Veya text/web connection ise Doğrudan QT üzerinden(web ve txt con için)
ActiveSheet.QueryTables(1).Connection
Refresh işlemleri
Workbook nesnesinin RefreshAll adında bir metodu vardır, ilgili dosyadaki tüm bağlantıları (ve pivot tabloları) refreshler. Bu bazı durumlarda işimizi gören pratik bir yöntemdir. Özellikle bu metod, kodumuzun son satırı yer alıyorsa. Mesela uzunca bir işten sonra ActiveWorkbook.RefreshAll derseniz, dosyadaki tüm bağlantılar sorunsuzca refreshlenir.
Ancak(aslında büyük bir ancaaaak), bu metod asenkron(asynchronous) bir metoddur. Yani, refresh işlemleri henüz bitmeden sonraki satırlar okunmaya devam edilir. Bu da her zaman olmasa da bazen sorunlara neden olur. Zira arkasından bir kaydetme işlem gibi bir işlem varsa hatalara ve kitlenmelere neden olabilir.
O yüzden kodun son satırı olmadığı sürece refresh işlemlerini connection nesnesi üzerinden(bir döngü içinde tüm bağlantıları ele alacak şekilde) yapmayı tercih etmeliyiz, workbook nesnesi üzerinden değil. Böylece tüm bağlantılar refresh olana kadar beklemiş oluruz. Zira Connection nesnesinin Refresh metodu, BackgroundQuery özelliğine False atandığında synchronous çalışır, yani bir sonraki satıra geçmeden önce mevcut satır icrası beklenir.
Sub AlternatifRefreshAll()
'Önceki kodlar
For Each cn In ActiveWorkbook.Connections
cn.ODBCConnection.BackgroundQuery = False 'kodun synchronous işlemesini sağlar
cn.Refresh
Next
'Sonraki kodlar
End Sub
Örneğin aşağıdaki tabloda mevcutta bulunan kayıt sayısını (listobjectin row sayısı) yazdırmak istiyoruz diyelim. Önce mevcudu yazdıracağız, hemen arkasından DAO ile bir kayıt ekleyeceğiz ve hemen arkasından tekrardan kayıt sayısını okuyacağız.
Bunun için kodumuzu şu şekilde hazırlarsak:
Sub async_refresh()
Dim ws As Worksheet
Set ws = ActiveSheet
Debug.Print "Yeni kayıt öncesi:" & ws.ListObjects(1).ListRows.Count, Time
Call KayıtEkle 'Kayıt ekleme işini yapan prosedür
ActiveWorkbook.RefreshAll 'yaklaşık 15-20 sn çalışır
Debug.Print "Yeni kayıt sonrası:" & ws.ListObjects(1).ListRows.Count, Time
End Sub
Bunun çıktısı şöyledir:
Yeni kayıt öncesi: 334 21:46:17
Yeni kayıt sonrası: 334 21:46:18
Gördüğünüz gibi, uzun süren refresh işini beklememiş ve önceki kayıt sayısı ile sonrakini aynı vermiş ve bunu 1 sn içinde yapmıştır.
Ancak güvenli yöntem ile çalıştırırsak:
Sub sync_refresh()
Dim con As WorkbookConnection
Dim ws As Worksheet
Set ws = ActiveSheet
Debug.Print "Yeni kayıt öncesi:" & ws.ListObjects(1).ListRows.Count, Time
Call KayıtEkle
For Each con In ThisWorkbook.Connections
con.ODBCConnection.BackgroundQuery = False
con.Refresh
Next con
Debug.Print "Yeni kayıt sonrası:" & ws.ListObjects(1).ListRows.Count, Time
End Sub
Sonuç şöyle çıkacaktır:
Yeni kayıt öncesi: 336 21:47:57
Yeni kayıt sonrası: 337 21:48:06
Görüldüğü üzere 9 sn'lik refresh işlemi beklenmiş ve kayıt sayısının 1 arttığı gösterilmiştir.
"KayıtEkle" prosedürümüz de şöyledir:
Sub KayıtEkle()
Dim db As Database
Dim rs As Recordset
Set db = DAO.OpenDatabase("buraya dosya adresini yazın")
Set rs = db.OpenRecordset("Anadata", dbOpenTable)
rs.AddNew
rs.Fields(0) = 2
rs.Fields(1) = 1
rs.Fields(2) = 2
rs.Fields(3) = WorksheetFunction.RandBetween(1000, 2000)
rs.Fields(4) = WorksheetFunction.RandBetween(1, 1000000)
rs.Update
rs.Close
db.Close
End Sub
ÖNEMLİ: RefreshAll yapılması durumunda ortaya çıkan sorunlar için birçok sitede önerilen DoEvents, Wait, Sleep yöntemlerinin hiçbiri işe yaramaz. Bu işin tek çözümü budur. İlgilenenler buradan da detay bakabilirler.
Data erişim yolları
Excel’den dış dataya çeşitli yollardan ulaşabiliyoruz. Bunların türlerini Excel ana menüsündaki şu sayfadan ulaşabilirsiniz. Bizim için önemli olan hangi yöntemle yaratıldığında (2013 ve 2016’da yeni gelen Get&Transform, New Query yöntemleri dahil) nereler değişiyor, ne tipte bir bağlantı oluşuyor detayı. Çünkü oluşan bağlantı tipinin ODBCConnection mı yoksa OLEDBConnection mı olduğuna bağlı olarak kodlarımızda ayarlama yapmamız gerekcek.
Genel olarak bakıldığında;
- MS Query her zaman ODBC yaratır
-
Data_Conn_Wizard:(2016'dan itibaren Legacy)
ODBC DSN-->ODBC
Other/Advanced-->OLEDB - Data_Import_Access:OLEDB
- Txt import :TEXT
- SQLServer:OLEDB
- PowerQuery:OLEDB(Mashup)
NOT:ODBC’de CommandType her zaman SQL’dir, yani xlCmdSql. OLEDB’de ise SQL olabileceği gibi doğrudan bir tablo/query de olabilir.
ListObject ve QueryTable türleri
ListObject dediğimiz nesne aslında gördüğümüz Table alanımızdır. Bu nesnenin Name özelliğine bakınca Table menüsündeki ismin aynısını görürüz.(ListObject’in eski adı zaten TableList idi, sonradan ListObject olmuş.)
İki tür listobject olabilir:
- Data bağlantılı: Böyleyse bunun QueryTable propertysi vardır, ve refreshelenebilir.
- Data bağlantısız: FormatAsTable ile Table yapıp uygulanmıştır. Bunun bir data bağlantısı yoktur. Querytable özelliği de yoktur, dolayısıyla refreshlenemez.
Biz doğal olarak burada ilk tür Listobject ile ilgileneceğiz.
QueryTable’a gelince, o her halükarda refreshable data içerir. ListObject’deki gibi QueryTable’ın da iki türünden bahsedebiliriz.
- ListObject ile ilişkili: Access, Oracle gibi DB’lerden kurulan bağlantılar.
- ListObject ile ilişkisiz: Text ve Web bağlantıları tek başına bulunan(standalone) QueryTable’lar üretir.
Excelde bir bağlantı kurduğumuzda bu bir text veya web bağlantısı değilse ListObject ve QueryTable ikisi de otomatikman oluşur, ister VBA içinden ister Excel menülerden oluşturmuş olalım farketmez. Yani VBA’de standalone ListObject yaratsak bile ona eşlik eden QueryTable de yaratılmış olur.
ListObject’e eşlik eden QueryTable’lar Worksheet.QueryTables koleksiyonun bir parçası değildir. Bunlara sadece ListObject.QueryTable özelliği ile erişilebilir.
Access,Oracle veya SQL Server gibi DB bağlantılarında Listobject'in bir adı varken, QueryTable'ın adı olmaz.(QueryTable'ın Name propertysi sadece text ve web queyler için var)
Yani bir text bağlantısı(Standalone Querytable) ve bir Oracle bağlantsı içeren bir dosyada şu sonuçları elde ederiz.
Debug.Print ActiveWorkbook.Connections.Count '2 (ikisi de)
Debug.Print ActiveSheet.ListObjects.Count '1 (Oracle)
Debug.Print ActiveSheet.QueryTables.Count '1 (T
Ama dosyamızda aslında iki tane QueryTable var, fakat bunlardan biri Listobject'n query'si. Bunlarda ayrı ayrı ilgil Listobject'in QueryTable özelliği ile ulaşaılabilir ama tek seferde hepsini saydıracak bir Count özelliği yok, zaten buna gerek de yok, Listobject sayısı kadar Listobjecte bağlı QueryTable sayısı vardır.
Yine mesela aşağıdaki kod bi Oracle bağlantısı olan dosyada hiçbirşey yazmazken text bağlantısı olan bir dosyada QueryTable’nin adını yazar.
Sub LOQT()
Dim ws As Worksheet
Dim q As QueryTable
Set ws = ActiveSheet
For Each q In ws.QueryTables 'bu bize standalonelarınkini verir
Debug.Print q.Name
Next q
End Sub
Yeni yaratımlar
Yukarıda kısaca değindiğimiz data erişim yollarından biri kullanılarak yeni bir bağlantı yaratabiliyoruz. Bu yeni yaratımlara çeşitli örnekleri aşağıda bulabileceğiniz gibi Makro Kaydetme aracı ile kendiniz de görebilirsiniz. Tüm yapmanız gerekn Record Macro tuşuna bastıktan sonra bağlantınızı kurmak ve sonrasında kodu incelemek. Tabi makro kaydedeci gereksiz uzunlukta bir kod üretmektedir, zira tüm parametrelere değer atamak gibi bir özelliği var bu aracın. Siz burada gereksiz parametre atamalarını çıkarın, diğerlerine otomatikman default değerleri atanacaktır. Array şeklindeki CommandText'i de tek bir string olarak yazın.
Örneğin Access'ten MS Query ile bir bağlantı kurduğumuzda aşağıdaki gibi bir kod oluşmakta
Application.CutCopyMode = False
With ActiveSheet.ListObjects.Add(SourceType:=0, Source:=Array(Array( _
"ODBC;DSN=MS Access Database;DBQ=C:\inetpub\wwwroot\aspnettest\excelefendiana\Ornek_dosyalar\Makrolar\vbadb.accdb;DefaultDir=C:\inetp" _
), Array( _
"ub\wwwroot\aspnettest\excelefendiana\Ornek_dosyalar\Makrolar;DriverId=25;FIL=MS Access;MaxBufferSize=2048;PageTimeout=5;" _
)), Destination:=Range("$A$1")).QueryTable
.CommandType = 0
.CommandText = Array( _
"SELECT Data.Bölge, Data.`Şube Adı`, Data.`Ürün Adı`, Data.`Aylık Gerç`" & Chr(13) & "" & Chr(10) & "FROM Data Data" _
)
.RowNumbers = False
.FillAdjacentFormulas = False
.PreserveFormatting = True
.RefreshOnFileOpen = False
.BackgroundQuery = True
.RefreshStyle = xlInsertDeleteCells
.SavePassword = False
.SaveData = True
.AdjustColumnWidth = True
.RefreshPeriod = 0
.PreserveColumnInfo = True
.ListObject.DisplayName = "Table_Query_from_MS_Access_Database"
.Refresh BackgroundQuery:=False
End With
Biz bunu maneul olarak yazmak istediğimizde aşağıdaki gibi çok daha sade yazarız.
Sub yeni_access_LO_odbc()
Dim constr As String
Dim qt As QueryTable
constr = "ODBC;DSN=MS Access Database;DBQ=C:\inetpub\wwwroot\aspnettest\excelefendiana\Ornek_dosyalar\Makrolar\vbadb.accdb;DefaultDir=C:\inetpub\wwwroot\aspnettest\excelefendiana\Ornek_dosyalar\Makrolar;DriverId=25;FIL=MS Access;MaxBufferSize=2048;PageTimeout=5;"
' OLEDB versiyonu--> constr = "OLEDB;Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\......."
Set qt = ActiveSheet.ListObjects.Add(SourceType:=0, Source:=constr, Destination:=ActiveCell).QueryTable
With qt
.CommandType = xlCmdSql 'recordmacro sırasında 0 yazar, aynen brakabilecğeiniz gibi manuel yazarken intellisense çıkar ve bunu seçersiniz
.CommandText = "select * from data" 'Record macroda otomatik oluşan Array yerine tek bir string içine yazarsınız
.ListObject.Name = "Acces bağlantı"
'Diğer öğeleri pas geçebiliriz, böylece default değerleri atanır
.Refresh BackgroundQuery:=False
End With
End Sub
Bu arada eğer dataya erişim yöntemi olarak Data>Get&Transform>From Database diyip Access seçerseniz, üretilen kod biraz daha farklı olmakta, zira burada Power Query devreye sokulmaktadır.
ActiveWorkbook.Queries.Add Name:="Data (2)", Formula:= _
"let" & Chr(13) & "" & Chr(10) & " Source = Access.Database(File.Contents(""C:\inetpub\wwwroot\aspnettest\excelefendiana\Ornek_dosyalar\Makrolar\vbadb.accdb""), [CreateNavigationProperties=true])," & Chr(13) & "" & Chr(10) & " _Data = Source{[Schema="""",Item=""Data""]}[Data]" & Chr(13) & "" & Chr(10) & "in" & Chr(13) & "" & Chr(10) & " _Data"
ActiveWorkbook.Worksheets.Add
With ActiveSheet.ListObjects.Add(SourceType:=0, Source:= _
"OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=""Data (2)"";Extended Properties=""""" _
, Destination:=Range("$A$1")).QueryTable
.CommandType = xlCmdSql
.CommandText = Array("SELECT * FROM [Data (2)]")
.RowNumbers = False
.FillAdjacentFormulas = False
.PreserveFormatting = True
.RefreshOnFileOpen = False
.BackgroundQuery = True
.RefreshStyle = xlInsertDeleteCells
.SavePassword = False
.SaveData = True
.AdjustColumnWidth = True
.RefreshPeriod = 0
.PreserveColumnInfo = True
.ListObject.DisplayName = "Data__2"
.Refresh BackgroundQuery:=False
End With
Biz şimdilik MS Query yöntemiyle ilerleyeceğiz. Zira birçok kullanıcıda şuan için PowerQuery entegrasyonunun olmadığını varsayabiliriz. Gerçi olsa da farketmez, kodlar büyük ölçüde aynı, sadece connection strginlerinde küçük farklar sözkonusu.
Standalone bağlantı
Öncelikle standalone(kendi başına) bağlantılara bakalım.
Bu örneğe bakarken hali hazırda üzerinde bağlantı olan bir dosya ile çalışmanızı tavsiye ederim. Bu dosya sizin kendinize ait bir dosya olabileceği gibi en üstte benim linkini verdiğim örnek dosya da olabilir.
Sub standalone_LO_QT()
'F8 ile tk tek çalıştırın
Dim ws As Worksheet
Dim qts As QueryTables
Dim qt1 As QueryTable, qt2 As QueryTable, yeniqt As QueryTable
Dim los As ListObjects
Dim lo1 As ListObject, lo2 As ListObject, yenilo As ListObject
Set ws = ActiveSheet
Set dest = ActiveCell
'1)*******QueryTable işlemleri************Bunlar standalone QueryTable'ler içindir.(Text dosyası gibi)
'1a)Mevcutlara erişim:Mevcutta standalone Querytable bulunan bir sayfada çalışın
Set qts = ws.QueryTables
Set qt1 = qts.Item(1) 'veya item kullanılmayadan doğrudan qts(1) yazılabilir
Set qt2 = qts("ExternalData_4") 'ismiyle de ulaşaibliriz
'1b)Yeni yaratım
Set yeniqt = qts.Add(connection:="TEXT;" & adres & "hatalog.txt", Destination:=dest)
yeniqt.Refresh
'!!!!!!!!!!!!!!!!!!!!!!----şimdi başka bir boş hücre seçin-----------!!!!!!!!!!!!!!!!
Set dest = ActiveCell
'2)*******standalone ListObject işlemleri*********(Access, Sqlserver, oracle v.s)
'2a)Mevcutlara erişim:Mevcutta listobject bulunan bir sayfada çalışın(standalone olup olmadığı farketmez)
Set los = ws.ListObjects
Set lo1 = los.Item(1) 'veya item kullanılmayadan doğrudan los(1) yazılabilir
Set lo2 = los("Table_Query_from_MS_Access_Database") 'ismiyle de ulaşaibliriz
'2b)yeni eklemek için querytable lazım zira refreshlenen şey aslında querytabledır. o yüzden bu madde standalon listobject içermez
strSource = "ODBC;DSN=MS Access Database;DBQ=" & adres & "vbadb.accdb;DefaultDir=" & _
adres & ";DriverId=25;FIL=MS Access;MaxBufferSize=2048;PageTimeout=5;"
Set yenilo = los.Add( _
SourceType:=xlSrcExternal, _
Source:=strSource, _
LinkSource:=True, _
XlListObjectHasHeaders:=xlGuess, _
Destination:=dest)
With yenilo.QueryTable
.CommandText = "SELECT Data.Bölge, Data.`Şube Adı`, Data.`Ürün Adı`, Data.`Aylık Gerç`" & Chr(13) & "" & Chr(10) & "FROM Data"
.CommandType = xlCmdSql
.Refresh BackgroundQuery:=False
End With
'Ancak yaratım olduktan sonra standalone listobject refreshlenebilir
yenilo.Refresh
End Sub
ListObject ile ilişkili bağlantılar
Aşağıkida örnekte ListObject ilişkili bir QueryTable'la ilgili çeşitli özelliklere bakıyoruz.
Sub qt_ilişkili_lo()
Dim lo As ListObject
Dim loqt As QueryTable
Set lo = ActiveSheet.ListObjects.Item(1)
Set loqt = lo.QueryTable
Debug.Print loqt.Connection 'connection string verir
Debug.Print loqt.CommandText 'SQLi verir
'Debug.Print loqt.FieldNames 'bişey dğeil, true/false, isimler gürnümsünmü
Debug.Print loqt.ListObject.Name 'Table_Query_from_MS_Access_Database7
'Debug.Print loqt.Name 'hata verir
Debug.Print loqt.QueryType '1 yazar. 1:xlODBCQuery 2: xlDAORecordset 7: xlADORecordset 5: xlOLEDBQuery 6: xlTextImport
Debug.Print loqt.WorkbookConnection.Name 'Query from PDWH_USR11
Debug.Print lo.DisplayName 'Query from MS Access Database1
Debug.Print lo.ListColumns.Count
Debug.Print lo.ListColumns(5) 'son eklediğim manuel alanı da saydı
Debug.Print lo.Name 'Table_Query_from_MS_Access_Database7
'Debug.Print lo.QueryTable.Name 'hata verir
Debug.Print lo.Range.Address 'son yeni eklenen kolon dahil verir
Debug.Print lo.SourceType '3 3:xlSrcQuery, 0:xlSrcExternal 1:xlSrcRange
loqt.Refresh 'lo.refresh de olur
End Sub
Text dosyası bağlantısı
Sub txt_yeni_con()
'sadece QueryTable, ListObject olmaz
kaynak = "TEXT;Q:\.......\kredilog.txt"
Set qt = ActiveSheet.QueryTables.Add(Connection:=kaynak, Destination:=ActiveCell)
qt.Name = "kredi sorgusu"
qt.Refresh
End Sub
Çeşitli üyeler
Unlink: Listobject'in veri kaynağıyla bağını koparır ama Table özelliği devam eder.
Unlist: Listobject'in hem veri kaynağıyla bağı koparır hem de Table özelliği bozulur, yani Table formatından normalde Range’e döndürür. Gerçi zebra desen olarak yine kalır ama fonksiyonalite olarak artık Table gibi davranamaz. Yani herhangi bir yere yeni satır eklenince zebra deseni de bozmuş olursunuz. (Table seçiliyken Design menüsündeki "Convert to Range" ile aynı işi yapar)
Ben daha çok Unlinki tercih etmenizi tavsiye ederim. Ne de olsaTable candır.
ListColumns: Bu özellik de yine ListObject'e ait olup, çoğu durumda çektiğimiz veri kaynağındaki kolon sayısını elde etmede kullanılır. Ancak bazı durumlarda biz Excel üzerinde de yeni kolonlar ekleyebilir ve bu kolonda çeşitli hesaplamalar yapabiliriz. Örneğin ListObjectin altında yatan QueryTable’ımıza baz teşkil eden SQL metni (veya tablo/query) 5 kolondan oluşuyor diyelim. Sonuç döndükten sonra biz bir de formül yazacağımız 6.kolon açarsak, ListObject’in kolon sayısı(ListColumns.Count) 5 değil 6 olacaktır.
ListRows: İlgili ListObject'te kayt sayısıyla ilgili işlemler yapmayı sğalayan collectiondır. Bunun da Count property'si ve index alma özelliği kullanılabilir. Toplam kayıt sayıt sayısını veren B ve son kaydı silen lo.ListRows(lo.ListRows.Count).Delete gibi.
Diğer üyelerini de Macro Recorder ile Table menüsündeki ayarlarla oynayarak kendinizi görebilirsiniz ama en önemlileri bunlardı diyebilirim.
Refresh
Bir Table'ı refreshleme 2 türlü olabilir.
1)ListObject veya QueryTable üzerinden
Refresh işlemleri için QueryTable’nin de ListObject’in de Refresh metodu kullanılabilir. Bu bağlamda Activesheet.ListObjects(1).QueryTable.Refresh ile Activesheet.ListObjects(1).Refresh tamamen özdeştir.
'i)Text ve Web dışı kaynaklarda
ActiveSheet.ListObjects(1).QueryTable.Refresh BackgroundQuery:=False
'veya
ActiveSheet.ListObjects(1).Refresh BackgroundQuery:=False
'ii)Text veya web kaynaklarda
ActiveSheet.QueryTables(1).Refresh BackgroundQuery:=F
2)Doğrudan connection üzerinden gidilebilir
Bu yöntemi yukarıda connections konusunda görmüştük.
ActiveWorkbook.Connections(1).ODBCConnection.BackgroundQuery = False
ActiveWorkbook.Connections(1).Refresh
NOT: Her iki grupta da senkron bir refresh işlemi için BackgroundQuery özelliklerine False değeri atanmıştır.
Şifre Güvenliği
ListObject ve QueryTable kullanmanın bir sakıncası veritabanı bağlantı şifrelerinizin Excel içinde gömülü olarak bulunması olacaktır. Bunun ele geçmemesi için alınabilecek birkaç önlem olmakla birlikte bilgili (ve kötü niyetli) bir kullanıcı tarafından ele geçirilebilir. Bu önlemlere aşağıda yer verilmiştir.
- Bir sayfa Activesheet.Visible= xlVeryHidden yapılıp şifre buradan okunabilir.
-
Ribbon ilgili dosya aktifken geçici olarak kapatılabilir. Böylece Data veya Table menüsünden Properties
butonuna tıklayamazlar.
Private Sub Workbook_Activate() If Environ("UserName") = 12345 Then Exit Sub Application.ExecuteExcel4Macro "SHOW.TOOLBAR(""Ribbon"",False)" Application.DisplayFormulaBar = False Application.DisplayStatusBar = Not Application.DisplayStatusBar ActiveWindow.DisplayWorkbookTabs = False End Sub '------------------------ Private Sub Workbook_Deactivate() If Environ("UserName") = 12345 Then Exit Sub Application.ExecuteExcel4Macro "SHOW.TOOLBAR(""Ribbon"",True)" Application.DisplayFormulaBar = True Application.DisplayStatusBar = True ActiveWindow.DisplayWorkbookTabs = True End Sub '------------------------- Private Sub Workbook_Open() If Environ("UserName") = 12345 Then Exit Sub MsgBox "Ribbon bu dosya aktif olduğu sürece devre dışı olacaktır. Başka bir excel dosyasına geçerseniz tekrar aktif olacaktır." End Sub
-
Unlist veya Unlink kullanımı: Sözkonusu dosya günlük oluşan bir rapor ise
şifreniz Format dosyanızda durur, dosya hergün kaydolurken ilgili günün
tarihi ile kaydolur ve bu dosyada Unlist/Unlink yaparak şifrenin
görünmesini engellemiş olursunuz. Ancak Format dosyasında şifreniz hala
kalacaktır.
Kullanıcıların şifreyi içeren Format dosyasını açmasını engellemek için de ilgili dosyanın Workbook_Open markosuna sizden başkasının açmasını engelleyecek bir kod koyabilirsiniz. Üstelik bu tür giriş denemelerini Logger gibi bir fonksiyonla da kayıt altına alabilirsiniz. (Tabi kötü niyetli kullanıcı makroları disable yaparak bu engeli de aşabilir.)
If readonlykontrol = True Then If Environ("UserName") <> 12345 Then 'bu sizin siciliniz olsun If InStr(ActiveWorkbook.Name, "Format") > 0 Then Logger WorksheetFunction.Rept(" ", 50 - Len(raporad)) & raporad, "İzinsizGiriş", 0, Environ("UserName") + " nolu sicil Q'dan giriş yapmaya çalıştı" MsgBox "Hey dostum, ortalığı karıştırmayı bırak. Format dosyama sakın bir daha dokunayım deme" ActiveWorkbook.Close savechanges:=False Exit Sub End If End If End If
- ListObject ve QueryTable’yi VBA ile oluşturmak: Son çözüm, Workbook_Open içinde ilgili bağlantıların yaratıldığı kodları yazmaktır. VBA projesine de Protection koydunuzmu işlem tamamdır. Bu da kırılamaz bir şifre değildir tabi ancak bu hinlikleri bilen biri tarafından kırılabilir.
- Çözüm değil ancak alternatif olarak yukarıdaki ADO ve DAO yöntemlerini kullanmak da işinize yarayabilir. Ancak SQL çok çok çoooook uzunsa bunu direkt VBA ortamında hazırlamak sıkıntılı olabilir, onun yerine SQL kısmını bir txt/sql uzantılı dosyasından okutmakta fayda var.
Aşağıdaki kodlar benim günde yaklaşık 40 raporun kendiliğinden çalışmasını, refresh olmasını ve yeni günün tarihiyle kaydedilmesini, gerekiyorsa ilgili alıcılara maille iletimini sağlar. (NOT:Ben yerimde yokken departmaan benden mailler geldiğinde arkadşaların "Volkan sen yokken senin hayaletin ortalıkta dolaşıp mailler atıyordu" demesinden kaynaklı olarak bu sistemin adını Hayalet Protokol olarak belirledim.)
Gerek konu gerek kodlar ilk başta karışık gelebilir. Anlaşılırlığı artırmak adına kodlardan hemen önce bir akış diyagramı da ekledim. Udemy'deki eğitimime kaydolursanız orada görsel olarak da akışı takip edebilirsiniz. Düzenli rapor üreten biriyseniz bu konuyu mutlaka anlamaya çalışın. Size ve bölümünüze sunacağı fayayı tahmin bile edemezsiniz. En az 3-4 adamlık tasarruftan bahsediyorum. Şahsen tüm sitede benim en önem verdiğim konu burasıdır.
Ön bilgiler:
- İki bilgisayarla çalışıyorum. Biri, sadece birkaç makronun çalıştığı PC'm, biri de schedule edilmiş işlerin ağırlıklı olarak çalıştığı laptop.
- Kodların bazısı her iki makinada da çalışıyor, bazısı sadece PC'de, bazısı da sadece laptopta
- PC'de çalışanlar genelde mesai saati dışında çalışıp bitiyor. Gün içinde çalışanlar da hep laptopta çalışıyor.
- Diyelim ki sicilim 12345. PC ismim B12345, laptop ismim L12345
- Raporların bir kısmı Veri Ambarı(kısaca DWH) tablolarına SQL atıyor, bazısı DWH'tan türeyen datamartlara, bazıları da online sistemlere. Online olanlar için herhangi bir "tablo dolmuş mu" kontrolü yapmıyorken, DWH ve ondan türeyen datamartlar için dünün datası dolmuş mu diye kontrol yapıyorum
- İlk kod bloğunda kapatOnMakroPC diye bir modül var. Bunun hikayesi şu. Her akşam laptopta(makroPC) Exceli otomatik kapattırıyorum, ki güncel Personal.xlsb ve Schedule.xlsb dosyaları laptopa yedeklenebilsin. Böyelce kendi PC'mde shcedule ayarlarında bir değişiklik yaptığımda veya Personal.xlsb dosyama yeni bir prosedür eklediğimde laptopa da aynen geçmesini sağlıyorum ki işlerde aksama olmasın. Bunun için PC'de bu iki dosyayı ne zaman kaydetsem, bunların "Workbook_AfterSave" eventi devreye giriyor ve dosyaların güncel halini ortak alana da kaydediyor. Laptopta da Windows Task Scheduler ile her gece yarısı önce yedeklemeyi yaptırıyorum, sonra Excel'i tekrar açtırıyorum
- Hem PC'de hem laptop'ta Personal.xlsb ve Schedule.xlsb dosyam XLSTART klasöründe yer alıyor, böylece Excel açılır açılmaz onlar da gizli olarak açılıyorlar
- Keza, her iki bilgisayarda da bilgisayar açılır açılmaz Excel'in de açılmasını sağlayan bir kurgu var. Bunu windows ayarlarından siz de yapabilirsniz.
-
Bundan sonra süreç şu şekilde devam ediyor.
- Schedule dosyasının Workbook_Open eventi devreye giriyor, ve bu event de StartTimer prosedürünü çalıştırıyor.
- Bu prosedür, Dwh(Veri ambarı) dolmuş mu yani güncel data yüklenmiş mi kontrolünü yapıyor.
- Dwh dolmadıysa 10 dk sonrasına kendisini tekrar schedule ediyor, dwh dolduysa "dwh raporları" isimli prosedürü çalıştııryor
- Dwhçılar içindeki tüm prosedürler sırayla çalıştırılıyor.
- Varsa diğer datamart kontrolleri de aynı süreçte yapılmaktadır.
Akış şeması
Bu şemayı Udemy kursum için hazırlamıştım. Videoda animasyonlu olarak teker teker uygun sırada açıldıkları için oradan bakmanızı da tavsiye ederim.
Aşağıdaki DWH(HR) diye geçen şey, bilgisayarıma kurduğum Oracle veritabanındaki HR şeması. Temsili olarak DWH, yani veri ambarı tabloları bu şemada olacak, Volkan şemasında da DM yani datamart olacak. Bunlardaki tablolar dolmuş mu diye kontrol edeceğiz.
Schedule dosyasını buradan indirebilirsiniz. Açılır açılma çalışmasını engellemek için Workbook_Open'ın başlangıcına geçici olarak Exit Sub satırı ekledim.
Şimdi bir de örnek klasörlere bakalım.
Aşağıdaki vereceğim tüm prosedürler Schedule.xlsb dosyamda bulunmaktadır. Şimdi, ilk olarak Workbook_Open prosedürüne bakalım.
Private Sub Workbook_Open()
ilksaat = Date + TimeSerial(23, 30, 0)
'Debug.Assert False
'PC'yi mesai başlangıç saatinden sonra açmışsak kontrol raporları hemen başlasın, diğerlerine gerek yok
If TimeValue(Now) > ilksaat Then
Application.OnTime Now + TimeSerial(0, 0, 10), Procedure:="dwh_kontrol"
Application.OnTime Now + TimeSerial(0, 0, 20), Procedure:="dm_kontrol"
Else 'gece 00:00'da programlandığı saatte açıldıysa
Call StartTimer
End If
End Sub
Şimdi de StartTimer prosedürünü inceleyelim. Bu, schedule işlemlerini ele aldığım ana prosedürdür.
Sub StartTimer()
'anapc veya makropc dışında bir bilgisayarda çalışırsa çıksın
If Environ("computername") <> volkanpc And Environ("computername") <> makropc Then Exit Sub
'hem anapc hem makropcde şunlar çalışsın
Application.OnTime Now + TimeSerial(24, 0, 0), Procedure:="StartTimer"
If Environ("computername") = volkanpc Then
Application.OnTime TimeValue("00:05:00"), Procedure:="eskilerisil"
Application.OnTime ilksaat, Procedure:="dwh_kontrol"
Application.OnTime ilksaat + TimeSerial(0, 1, 0), Procedure:="dm_kontrol"
Application.OnTime TimeValue("11:00:00"), Procedure:="AccessRaporu"
Application.OnTime TimeValue("11:30:00"), Procedure:="SQLServerRaporu"
Else 'makropc
'.....
Application.OnTime TimeValue("23:55:00"), Procedure:="kapatOnMakroPC" 'gece diğer pclere yedekleme yapılsın diye dosyayı kapatmamız gerekiyor
End If
End Sub
Şimdi de kontroller modülündeki prosedürlere bakalım. Burada da kalabalık olmaması adına tüm kontrol prosedürlerini koymadım, onun yerine iki farklı yöntemi göstermesi adına iki prosedürü verdim. İlkinde , BT ekiplerinden gelen "Tablo doldu" tarzındaki mailin varlığını kontrol ediyorum. İkincisinde ise farklı dinamiğe sahip olduğundan "Tablo doldu" maili gelmeyen bir datamart için bir tabloya SQL atıp kayıt dönüp dönmediğine bakıyorum.
'1.yöntem
Sub dwh_kontrol()
Const raporad As String = "dwh_kontrol"
Const adresKontrol As String = "yükleme kontrol\dwh\dwhyüklemekontrol - "
Dim outApp As New Outlook.Application
Dim ns As Outlook.Namespace
Dim myroot As Outlook.Folder, klasör As Outlook.Folder
Dim eItems As Outlook.Items
Dim eItem As Outlook.MailItem
Dim fso As New FileSystemObject
'Debug.Assert False
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "Girildi"
On Error GoTo hata
If Mevcutmu(adres & adresKontrol & Date - 1 & " Sonuçları.txt") Then
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "ZatenOK"
Call dwh_raporları
Exit Sub
End If
AlertUpdatingEvent False, False 'normalde commentsiz olmalı
Set ns = outApp.GetNamespace("MAPI")
'hem benim hem serkanın pcsinde aynı klasör olduğunu varsayıyorum
Set myroot = ns.Folders("volkan.yurtseven@hotmail.com")
Set klasör = myroot.Folders("Kontrol Mailleri")
Set eItems = klasör.Items
Kriter1 = "[Subject]=""DWH yüklemesi tamamlanmıştır"""
Kriter2 = "[ReceivedTime]>'" & Date & "'" 'Tarihin saat kısmından dolayı > olmalı
Set kriterliler = eItems.Restrict(Kriter1).Restrict(Kriter2)
If kriterliler.Count < 1 Then 'maili bulamadıysak 10 dk sonra yeniden schedule ederiz
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "Reschedule"
Application.OnTime Now + TimeSerial(0, 10, 0), Procedure:="dwh_kontrol"
Else 'mail varsa sürece devam
fso.CreateTextFile adres & adresKontrol & Date - 1 & " Sonuçları.txt"
Call MailBilgi("DWH Kontrolü", anaAlicilar)
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "OK"
Call dwh_raporları
GoTo çıkış
End If
çıkış:
Set myroot = Nothing: Set klasör = Nothing
Set ns = Nothing: Set outApp = Nothing
Set eItems = Nothing: Set eItem = Nothing: Set fso = Nothing
AlertUpdatingEvent True, True
Exit Sub
hata:
AlertUpdatingEvent True, True
Set myroot = Nothing: Set klasör = Nothing
Set ns = Nothing: Set outApp = Nothing
Set eItems = Nothing: Set eItem = Nothing: Set fso = Nothing
Logger Now, Environ("computername"), raporad, "Kontrol", Err.Number, Err.Description
End Sub
'----------------------------------------------------------------
'2.yöntem
Sub dm_kontrol()
Const raporad As String = "dm_kontrol"
Const adresKontrol As String = "yükleme kontrol\dm\dmyüklemekontrol - "
'Debug.Assert False
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "Girildi"
AlertUpdatingEvent False, False 'normalde commentsiz olmalı
On Error GoTo hata
If Mevcutmu(adres & adresKontrol & Date - 1 & " Sonuçları.xlsb") Then
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "ZatenOK"
Call dm_raporları
Exit Sub
End If
Workbooks.Open adres & adresKontrol & "Format.xlsb" 'workbook_open kodu çalışacak
'veritabanı yüklenmediyse 10 dk sonraya reschedule
If Not Mevcutmu(adres & adresKontrol & Date - 1 & " Sonuçları.xlsb") Then
ActiveWorkbook.Close savechanges:=False
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "Reschedule"
Application.OnTime Now + TimeSerial(0, 10, 0), Procedure:="dm_kontrol"
Exit Sub
Else 'başarılıysa devam edelim
ActiveWorkbook.Close
Kill "C:\geçici\geçici2.xlsb"
Call MailBilgi("DM Kontrolü", anaAlicilar)
Logger Now, Environ("computername"), raporad, "Kontrol", 0, "OK"
Call dm_raporları
GoTo çıkış
End If
çıkış:
AlertUpdatingEvent True, True
Exit Sub
hata:
AlertUpdatingEvent True, True
End Sub
Şimdi de dwh_raporları ve dm_raporları prosedürlerine bakalım. Burada raporlar çalışma sırasına sokuluyor.
Sub dwh_raporları()
'Debug.Assert False
Application.OnTime Now + TimeSerial(0, 0, 10), Procedure:="dwhrapor1"
Application.OnTime Now + TimeSerial(0, 0, 20), Procedure:="dwhrapor2"
Application.OnTime Now + TimeSerial(0, 0, 30), Procedure:="dwhrapor3"
End Sub
Sub dm_raporları()
'Debug.Assert False
Application.OnTime Now + TimeValue("00:00:10"), Procedure:="dmrapor1"
Application.OnTime Now + TimeSerial(0, 0, 20), Procedure:="dmrapor2"
Application.OnTime Now + TimeSerial(0, 0, 30), Procedure:="mixrapor1"
End Sub
Şimdi de online olan, ve dolayısıyla "dolmuş mu?" kontrolüne gerek olmayan prosedürlere bakalım.
Sub AccessRaporu()
rprLog = "AccessBlg"
rprMail = "Accesten Bölge Rakamları"
yol = "Accesten Bölge Rakamları\Accesten Bölge Rakamları - "
kontrol1 = vbNullString
alıcı = diğerAlıcılar
RutinKod rprLog, rprMail, yol, kontrol1, alıcı
End Sub
'-----------------------
Sub SQLServerRaporu()
rprLog = "dwh1"
rprMail = "dwh 1 raporu"
yol = "Sqlserverdan site konuları\Sqlserverdan site konuları - "
kontrol1 = vbNullString
alıcı = diğerAlıcılar
RutinKod rprLog, rprMail, yol, kontrol1, alıcı
End Sub
Şimdi dwh raporlarından birine ait prosedüre bakalım; şuan hala schedule.xlsb içindeyiz.
<
Sub dwhrapor1()
'Debug.Assert False
rprLog = "dwh1"
rprMail = "dwh 1 raporu"
yol = "Oracle dwh - hr şemasından country\Oracle dwh - hr şemasından country - "
kontrol1 = "yükleme kontrol\dwh\dwhyüklemekontrol - " & Date - 1 & " Sonuçları.txt"
alıcı = dwhRapor1Alıcıları
RutinKod rprLog, rprMail, yol, kontrol1, alıcı
End Sub
Bazı raporlar ise hem dwh hem dm tablolarının yüklenmesini gerekebilir.
Sub mixrapor1()
rprLog = "mix"
rprMail = "Mix raporu"
yol = "Mix raporlar\Mix rapor1 - "
kontrol1 = "yükleme kontrol\dm\dmyüklemekontrol - " & Date - 1 & " Sonuçları.xlsb"
kontrol2 = "yükleme kontrol\dwh\dwhyüklemekontrol - " & Date - 1 & " Sonuçları.txt"
alıcı = diğerAlıcılar
RutinKod rprLog, rprMail, yol, kontrol1, alıcı, kontrol2
End Sub
Şimdi de bu son iki kodun sonundaki RutinKod prosedürüne bakalım. Gördüğünüz üzere bu prosedür, oldukça çok parametre alıyor. Bunları anlayabileceğinizi düşünüyorum. Son parametre opsiyonel olup, mixrapor1'deki gibi, iki rapor kaynağından(dwh ve dm) da beslenen raporlar için kullanılmaktadır.
Sub RutinKod(ByVal raporLogger As String, ByVal raporMail As String, ByVal folderfile As String, _
ByVal kontrol1 As String, ByVal alıcılar As String, _
Optional ByVal kontrol2 As Variant)
'Debug.Assert False
'normalde kontrol dosyalarını kontrole gerek yok. ihtiyaden kontrol yapıyoruz
If Not Mevcutmu(adres + kontrol1) Then Exit Sub
If Not IsMissing(kontrol2) Then
If Not Mevcutmu(adres + kontrol2) Then Exit Sub
End If
'Daha önce oluştuysa çıkalım
If Mevcutmu(adres & folderfile & Date - 1 & " Sonuçları.xlsb") Then Exit Sub
On Error GoTo hata
AlertUpdatingEvent False, False
Workbooks.Open adres & folderfile & "Format.xlsb" 'çalışmaya başlayacak
ActiveWorkbook.Close
Kill "C:\geçici\geçici2.xlsb"
Call MailBilgi(raporMail, alıcılar)
Logger Now, Environ("computername"), raporLogger, "OK", 0, "rapor başarıyla çalıştı"
AlertUpdatingEvent True, True
Exit Sub
hata:
AlertUpdatingEvent True, True
Logger Now, Environ("computername"), raporLogger, "Hata", 0, Err.Description
End Sub
Şimdi schedule.xlsb'den çıkıp, veritabanı sorgulaması yapılacak rapor dosyasındaki koda bakalım.
Private Sub Workbook_Open()
Const raporad = "dwh-country"
Application.Run "schedule.xlsm!wbopen", raporad
End Sub
Gördüğünüz üzere kod çok kısa ve bizi tekrar schedule.xlsb'ye yönlendiriyor. Bunu böyle yaptık, çünkü yaklaşık 40 rapor içinde her birine aynı kodu yazmanın anlamı yok. Onun yerine bi kere yazıyoruz, ve her defasında bu kodu, uygun parametreyle(rapor adı) çağırıyoruz. Hadi şimdi bir de bu son koda bakalım.
Sub wbopen(ByVal raporad As String, Optional ptrefresh As Boolean = False)
Dim lo As ListObject
Dim con As WorkbookConnection
Dim ws As Worksheet
Dim pt As PivotTable
On Error GoTo hata
'Debug.Assert False
'yetkili kişiler dışında direkt açan kişilere uyarı çıksın
If Environ("username") <> "Volkan" And Environ("username") <> "Serkan" Then
If ActiveWorkbook.ReadOnly = False Then
'Date - 1'de bazen manuel değişiklikler yapmak gerekebiliyor
If InStr(ActiveWorkbook.Name, "Format") > 0 Or InStr(ActiveWorkbook.Name, Date - 1) > 0 Then
Logger Now, Environ("computername"), raporad, "YetkisizGiriş", 0, ""
MsgBox "Dosyaya doğrudan değil, Kokpit formundan ulaşınız"
ActiveWorkbook.Close savechanges:=False
Exit Sub
Else 'date - 2 ve öncesini açarsa sorun yok, ama burda koddan çıksın
Exit Sub
End If
Else 'readonly açılmazsa yani kokpitten açılmışsa sorun yok, ama burda koddan çıksın
Exit Sub
End If
End If
'volkan ve serkansa devam ediyoruz. İçinde refreshlenebilir bir Table yoksa çıksın
'excel versiyonunuza göre Queries soruglaması desteklenmeyebilir, o zaman bunu kaldırırsınız
If ActiveWorkbook.Queries.Count = 0 And ActiveWorkbook.Connections.Count = 0 Then Exit Sub
Logger Now, Environ("computername"), raporad, "WBOpen", 0, "WB giriş yapıldı"
isim = Left(ActiveWorkbook.Name, Len(ActiveWorkbook.Name) - 14) 'Neden 14, "dd.mm.yyy - Sonuçları"nın uzunluğu
'tüm connectionlar refresh
For Each con In ActiveWorkbook.Connections
If con.Type = xlConnectionTypeODBC Then
con.ODBCConnection.BackgroundQuery = False
Else
con.OLEDBConnection.BackgroundQuery = False
End If
con.Refresh
Next con
For Each ws In ActiveWorkbook.Sheets
ws.Activate
If ws.ListObjects.Count > 0 Then 'refresh olacak sayfalarda
'refresh sonucu data gelmiyorsa çıkalım
If Range("a1").End(xlDown).Row = 2 Then GoTo iptal '??????dosya açık kalmıyor mu????
'refresh sonucu data geliyorsa unlink yapalım
For Each lo In ws.ListObjects
lo.Unlink
Next lo
End If
'pivot refresh
If ptrefresh = True Then
For Each pt In ws.PivotTables
pt.RefreshTable
Next pt
End If
Next ws
hedef = ActiveWorkbook.Path & "\" & isim & " - " & Date - 1 & " Sonuçları.xlsb"
saveas_işlemi_inworkbook isim, hedef
Logger Now, Environ("computername"), raporad, "WBOpen", 0, "OK"
iptal:
'???wb close??
Exit Sub
hata:
Logger Now, Environ("computername"), raporad, "WBOpen", Err.Number, Err.Description
ActiveWorkbook.Close savechanges:=False
End Sub
Bu kod da çalışınca sıra, schedule kuyruğundaki diğer kodlara geçer ve işlemler aynen devam eder.
Rapor log dosyamızın içine de bakalım.
Schedule.xlsb dosyamızdaki Thisworkbook modülünde bir kodumuz daha var, ona da bakalım. Açıklamasını en başta yapmıştık.
Private Sub Workbook_AfterSave(ByVal Success As Boolean)
yol = "C:\inetpub\wwwroot\aspnettest\excelefendiana\Ornek_dosyalar\Makrolar\makroyedek\"
FileSystem.SetAttr yol & Me.Name, vbNormal
fso.CopyFile Me.FullName, yol
FileSystem.SetAttr yol & Me.Name, vbReadOnly
End Sub
Son olarak Schedule.xlsb dosyamdaki diğer kodlar ise şöyle;
Sub AlertUpdatingEvent(a As Boolean, u As Boolean, Optional e As Boolean = True)
With Application
.DisplayAlerts = a
.ScreenUpdating = u
.EnableEvents = e
End With
End Sub
Function Mevcutmu(adres As String) As Boolean
On Error GoTo hata
If GetAttr(adres) >= 0 Then Mevcutmu = True
Exit Function
hata:
Mevcutmu = False
End Function
Sub kapatOnMakroPC()
Application.Quit
End Sub
Sub Logger(ByVal tarih As String, ByVal pc As String, ByVal rapor As String, ByVal logtipi As String, ByVal hatano As Integer, _
ByVal açıklama As String)
Dim dosya As String
Dim dosyano As Integer
dosyano = FreeFile()
dosya = adres & "RaporLog.txt"
Open dosya For Append As #dosyano
Print #dosyano, tarih, pc, rapor, logtipi, hatano, açıklama
Close #dosyano
End Sub
Sub MailBilgi(ByVal rapor As String, ByVal alıcılar As String)
Dim outApp As New Outlook.Application
Dim outMail As MailItem
Set outMail = outApp.CreateItem(olMailItem)
With outMail
.Subject = rapor & " raporu yüklemesi"
.Body = "Güncel " & rapor & " raporu oluşmuştur"
.To = alıcılar
.Send
End With
End Sub
Sub saveas_işlemi_inworkbook(ByVal dosyaismi As String, ByVal hedef As String)
ActiveWorkbook.SaveAs "C:\geçici\geçici.xlsb" 'dosya tipine göre kontrol de yapılabilir
kaynak = ActiveWorkbook.FullName
ActiveWorkbook.SaveAs "C:\geçici\geçici2.xlsb" 'dosya tipine göre kontrol de yapılabilir
kaynak2 = ActiveWorkbook.FullName
FileCopy kaynak, hedef
Kill kaynak
'kaynak 2 nerde kullanım?
End Sub
Sub eskilerisil()
'Ödev olacak
End Sub
Özet
Tüm bu süreç kendini besleyen otomatik bir süreçtir. Başarılı bir şekilde kurulması durumunda müthiş bir verimlilik sağlanacaktır.
Yukarıda belirttiğim üzere, konu biraz karmaşık olup, daha verimli bir şekilde algılanabilmesi için Udemy eğitimininden yararlanmanızı tavsiye ederim.
Scheduled program akışını kesintiye uğratan unsurlar
Bazen schedule edilmiş dosyalarınızı güniçinde de açmanız ve kullanmanız gerekebilir. O yüzden belli saatlerde açıldığında faklı bir davranış göstermesini isteyebilirsiniz. Keza dosyayı açan sizseniz başka, diğer kişilerse başka şekilde davranmasını isteyebilirsiniz.
Bu davranış şekillerinden biri de kullanıcıya bir mesaj göstermek olabilir. Eğer programın bir yerinde mesaj verirseniz, bu mesaj kutusundaki bir düğmeye basılana kadar ekranda kalır, bu da takip eden schedule kodlarınızın çalışmasını engellemiş olur. O yüzden MsgBox'lı uyarılarınızı Schedule zamanı içinde size kesinlikle çıkarmamalı, sadece diğer kişilere çıkarmalıdır. Gerekiyorsa kendinize bir maille bilgilendirme yaptırabilir veya Statusbara mesaj yazdırabilir veya logger prosedürüyle kayıt altına alabilirsiniz.
Benim kullandığım yöntem şudur. Kullanıcılara UserForm kullanarak Kokpit diye bir arayüz oluşturdum ve bunu onlara verdiğim Add-in'deki menüye yerleştirdim. Kullanıcılar ilgili dosyayı kokpitten açtıklarında, dosyanın readonly açılmasını sağlıyorum. Normalde herkes hep dünün raporunu açmak ister , o yüzden kokopitten de sadece dünün raporlarını açabiliyorlar. Daha eski raporları açmak için ortak alana gidip klasörden manuel açmaları gerekiyor. Şimdi öyle bir kod yazmam gerekiyor ki, dünkü raporu ortak alandan açamasınlar, zira bir rapor gün içinde iki defa farklı zamanlarda refresh ediliyor; aynı zamanda ana format dosyamın da açılmaması lazım, zira onu açarlarsa schedule makrom takılmaktadır. O yüzden dünün dosyasını veya Format dosyasını klasörden manuel açmaya çalışırlarsa uyarı verdiriyorum. Şimdi aşağıdaki kodu inceleyelim.
Private Sub Workbook_Open()
On Error GoTo hata
If Environ("UserName") <> 12345 Then 'Dosya benim dışında biri tarafından açıldıysa
If Right(Me.Name, 11) = "Format.xlsm" Or (Right(Me.Name, 25) = Date - 1 & " Sonuçları.xlsm" And Not Me.ReadOnly) Then
MsgBox "Dosyayı ortak alandan değil, Kokpitten açın."
Me.Close savechanges:=False
End If
End If
If Environ("computername") <> "L12345" Then Exit Sub 'dosyayı bazen PC'mde açıp SQL'inde veya genel sayfa düzeninde güncelleme yapmam gerekiyor. Açılır açılmaz da refresh olmasın diye laptop dışında açılırsa yani PC'mde açılırsa kalan kodlar çalışmasın istiyorum
'Diğer kodlar
End Sub
Bir diğer önemli husus da, programınızda son derece ince ayarlanmış bir hata yönetimi mekanizması olması gerekir. Detaylar için DebuggingveHataYonetimi_HataYakalama.aspx"hata yakalama bölmünü iyice özümsemiş olmanızı tavise ederim. Schedule edilmiş programlarda hataları hata mesajı olark vermek yerine bir logger prosedürüyle bir dosyaya yazdırmayı tercih edebilirsiniz.
- QueryTable’lar oldukça hızlıdır ve özellikle VBA bilmeyenler için bile dataya ulaşmada kolaylık sağlar. ADO için mutlaka VBA bilmek gerekir. Ancak ADO’nun da QueryTable’ya göre avantajları vardır.
- ADO ile aynı anda birden çok DB(Ör:SQL Server ve Access) sorgulanabilirken QueryTable ile tek seferde sadece tek DB sorgulanabilir.
- ADO ve DAO ile gelen data formatsız bir data olacaktır. Bunu formatlı hale getirmek için ek kod yazmak gerekir. Halbuki bir WorkbookConnection kurulduğunda(text/web değilse) Table formatı otomatik gelmektedir.
- Ayrıca manuel refreshlemek gereken durumlarda ListObject yine bir kolaylık sağlamakta iken ADO/DAO için kod sayfasına girip kodu manuel çalıştırmak gerekmektedir. Belki bunun için sayfaya bir buton konularak kodun refresh mantığıyla çalıştırılması sağlanabilir.
Bir grup dosyadaki şifreleri (ve/veya userları) toplu olarak değiştirme
Bu kod özellike benim gibi günde 30-40 civarı dosyayı schedule etmiş kişiler için çok faydalı olacaktır. Oracle/DB2 gibi veritabanlarına bağlanıp veri çekiyorsanız çok büyük ihtimalle ayda bir şifrenizi değiştirmeniz gerekmektedir. Excel içine gömdüğünüz şifre bilgilerinin de aynı şekilde değişmesi gerekir. Sadece şifre değil, bazen schedule işlemini başka birinine devretmeniz de gerekebilir, bu durumda user bilgilerinin de değişmesi gerekir. İşte böyle bir durumda bu 30-40 dosyayı tek tek açıp, kimisinde 5-6 connection da bulunabilir, tek tek user/şifre bilgisi değiştirmek tüm gününüzü alabilir, zira tüm sorguların çalışması gerekecektir. İşte bu aşağıdaki kod, sadece birkaç dakika içinde tüm dosyalarda gerekli değişklikleri yapacaktır.(Dosyaları kaydedip kapatma işlemini de prosedürün sonuna siz ekleyin)
Burda toplamda üç ayrı prosedür bulunuyor. Bunlardan biri, diğer iki prosedür tarafından çağrılan bir Function. Zaten iki kez kullanılması gerektiği için bunu Function yaptım. (Bu Function'da çoklu değer döndürmek için Collection kullandım). Yaptığımız değişiklikler olmuş mu diye görmek için baktığımız üçüncü prosedüre gerek duymazsanız, Function içindeki kodu ana prosedüre yedirebilrsiniz(isterseniz tabi).
Kodun içinden de gördüğünüz üzere, oracle bağlantısı olanlar için ayrı, DB2 bağlantıları için ayrı olmak üzere ana prosedürü toplamda 2 kez çalıştırıyorum. Siz, kaç farklı veritabanına bağlanıyorsanız o kadar çalıştırmanız gerekecek. (Database'lerin illa farklı makinalarda olması gerekmiyor, Oracle üzerinde bulunan 3 farklı database'e de bağlanmanız durumunda yine 3 kez çalıştırmanız gerekir.)
Dim files As Collection
'-----------------------
Sub toplu_sifre_degisme()
'bir grup dosyadaki şifreleri (ve/veya userları) toplu olarak değiştirmek için kullanılır
On Error Resume Next
Dim wB As Workbook
Dim cvp As VbMsgBoxResult
Dim protectlimi As Boolean
Application.Calculation = xlCalculationManual 'Dosyalarda yoğun formülasyon varsa bunlar devreye girmesin diye
hey = MsgBox("Tüm makrolar çalıştıktan sonra değiştiiryosun dimi, çünkü bazı raporlarda DateModifed kontrolü var?", vbYesNo)
If hey = vbNo Then
Exit Sub
End If
cvp = MsgBox("Tüm dosyalar kapalı ve collectionın son hali güncel, değil mi?", vbYesNo)
If cvp = vbNo Then
Exit Sub
End If
eski = InputBox("Eski şifreyi girin")
If eski = "" Then Exit Sub
yeni = InputBox("Yeni şifreyi girin")
If yeni = "" Then Exit Sub
'çalışmasınlar diye event disable yapalım
Application.EnableEvents = False
'collection fonksiyonu çağıralım
Set files = dosyacoll
'şimdi dosyaları açıp şifreleri değiştirelim
For Each File In files
Workbooks.Open Filename:=File
Set wB = ActiveWorkbook
If wB.ProtectStructure = True Then 'protection varsa kaldıralım, aksi halde değişiklik yapamayız
protectlimi = True
wB.Unprotect (1234) 'haftalık skorkart, tmv
End If
For Each cn In wB.Connections
If cn.Type = xlConnectionTypeODBC Then
If InStr(cn.ODBCConnection.connection, eski) > 0 Then
cn.ODBCConnection.connection = Replace(cn.ODBCConnection.connection, eski, yeni)
End If
End If
Next cn
'protection varsa tekrar koyalım
If protectlimi = True Then wB.Protect (1234)
protectlimi = False 'tekrar false yapıyorum ki, mevcut değeri taşımasın
Next File
'son olarak da doğru değişmiş mi diye bakalım
Call toplu_connecition_string_gorme
Application.EnableEvents = True
Set files = Nothing
Application.Calculation = xlCalculationAutomatic
End Sub
'--------------------------------------
Function dosyacoll() As Collection
Const gunlukyol As String = "..."
Dim files As Collection
Dim DBtür As Byte
DBtür = Application.InputBox("DB türünü girin. Oracle için 1, DB2 için 2", Type:=1)
'collectionı oluşturalım
Set files = New Collection
With files
If DBtür = 1 Then 'oracle
.Add (gunlukyol + "…...xlsb")
'diğer dosyalar da aynı şekilde collectiona eklenir
ElseIf DBtür = 2 Then 'DB2
.Add (gunlukyol + "…...xlsb")
'diğer dosyalar da aynı şekilde collectiona eklenir
Else
MsgBox "yanlış DB türü girdiniz"
Application.EnableEvents = True
Exit Function
End If
End With
db = DBtür
Set dosyacoll = files
End Function
'--------------------------------------
Sub toplu_connecition_string_gorme()
'pwdyi bul sonraki 10 karakteri al
On Error Resume Next
Dim wB As Workbook
Dim cn As WorkbookConnection
Dim dizi() As String
Dim fso As New FileSystemObject
Application.EnableEvents = False
'files dolu mu diye kontrol edelim, doluysa ilk prosedürün devamıdır, değilse bağımsız açılmıştır
If Not files Is Nothing Then
GoTo atla
Else
Set files = dosyacoll
For Each c In files
Workbooks.Open Filename:=c
Next c
End If
atla:
N = 0 'maksimum connection sayısını bulmaya çalışıyoruz
For Each f In files
dosyaad = fso.GetFileName(f)
If Workbooks(dosyaad).Connections.Count > N Then
N = Workbooks(dosyaad).Connections.Count
End If
Next f
ReDim dizi(1 To files.Count, 1 To N)
i = 1
j = 1
dosyalar = ""
done = False
For Each f In files
dosyaad = fso.GetFileName(f)
dizi(i, j) = dosyaad
For Each cn In Workbooks(dosyaad).Connections
j = j + 1
dizi(i, j) = cn.ODBCConnection.Connection 'hepsinin odbc olduğunu bildiğim için
Next cn
j = 1
i = i + 1
Next f
Workbooks.Add
For i = 1 To files.Count
For j = 1 To N
If InStr(1, dizi(i, j), "UID") = 0 Then
Cells(i, j).Value2 = dizi(i, j)
Else
Cells(i, j).Value2 = Mid(dizi(i, j), InStr(1, dizi(i, j), "UID"), 30)
End If
Next j
Next i
Application.EnableEvents = True
Exit Sub
exudo:
Application.EnableEvents = True
MsgBox "Bir hata oluştu, örneğin protectli bir dosyada şifre değiştirmeye çalışıyor olabilirsiniz"
End Sub
(NOT:Her ne kadar şifre güvenliği kısmında şifrenizi connection string içinde yazmamanızı önerdiysem de, olur da böyle birşey yaptıysanız diye bu örneği koydum. Aslında şifreyi tek bir yerden okumanız durumunda sadece orada değiştirmeniz yeterli olacaktır ve yukarıdaki zahmete de girmemiş olursunuz. Dediğim gibi bir nedenden bu yolu benimsediyseniz diye bu örneği koydum)
Açık olan dosyalardaki Connectionların Şifre/user ve SQL bilgilerini değiştirme
Diyelim ki içinde bir veya daha fazla connection olan bir veya daha fazla dosya açık durumda. Bir önceki örnekte dosyalar kapalıydı, bu örnekte bu sefer açık durumdalar. Bir diğer fark da bu sefer SQL değişikliği de yapmak isteyebilirsiniz. Mesela açık olan tüm dosyalarda 1 gün öncesinin datasını çeken SQL’ler(“sysdate-1” satırını içeren) olduğunu düşünün. Bunların hepsini 2 gün öncesinin datasını gösteren SQL’e(sysdate-2) çevirmek isteyebilirsiniz. Bunu manuel yaparsanız yine şifre değişimde olduğu gibi SQL değişiminde de tüm bağlantıların çalışmasını beklemek zorundasınızdır. İşte bu aşağıdaki kod ile beklemeden tek seferde hızlıca gerekli kod değişikliklerini yapabilirsiniz.
Kodun yeterince açık olduğunu düşündüğüm için ilave bir açıklama yazmıyorum. Anlaşılmayan birşey olması durumunda aşağıdaki Yorum bölümüne sorunuzu yazabilirsiniz.
Sub ConString_veya_SQL_degistir()
'açık olan bir dosyada SQL veya connectionstring(genellikle şifre ve user için) değiştirir
On Error GoTo çıkış
Dim wB As Workbook
Dim cn As WorkbookConnection
Dim eski As String
Dim yeni As String
Dim contype As Byte
Dim neyi As Byte
Do
neyi = InputBox("SQL için 1, connection string için 2 girin")
Loop Until neyi < 3
eski = InputBox("Eski değeri girin")
If eski = "" Then Exit Sub
yeni = InputBox("Yeni değeri girin")
If yeni = "" Then Exit Sub
N = 0
dosyalar = ""
For Each wB In Application.Workbooks
If Windows(wB.Name).Visible = True Then 'Personal.xlsb gibi gizli dosyalarda çalışmasına gerek yok
If wB.Connections.Count > 0 Then 'içinde bağlantı olan dosyalarda yapsın
For Each cn In ActiveWorkbook.Connections
'sadece ODBC ve OLEDB'lerde yapsın, diğerlerinde bi işlem yapmasın
If cn.Type = xlConnectionTypeODBC Or cn.Type = xlConnectionTypeOLEDB Then
If neyi = 1 Then 'SQL-comnandtext
If cn.Type = xlConnectionTypeODBC Then
cn.ODBCConnection.CommandText = Replace(cn.ODBCConnection.CommandText, eski, yeni)
Else
cn.OLEDBConnection.CommandText = Replace(cn.OLEDBConnection.CommandText, eski, yeni)
End If
Else 'con string
If cn.Type = xlConnectionTypeODBC Then
cn.ODBCConnection.connection = Replace(cn.ODBCConnection.connection, eski, yeni)
Else
cn.OLEDBConnection.connection = Replace(cn.OLEDBConnection.connection, eski, yeni)
End If
End If
End If
Next cn
N = N + 1
dosyalar = wB.Name & ";" & dosyalar
End If
End If
Next wB
MsgBox N & " adet dosyada şifre değişti. "
Workbooks.Add
Range("a1") = dosyalar
Exit Sub
çıkış:
MsgBox "Bir hata oluştu"
End Sub
Tüm workbookların refresh olmasını sağlama
Diyelim ki, o anda açık olan tüm dosyaların refresh olmasını istiyorsunuz. Normalde Data menüsündeki RefreshAll sadece açık olan doyadaki bağlantıları refreshler. Biz ise açık olan tüm dosyalarda bunu yapmak istiyoruz. Tabi gizli olan Personal.xlsb tarzı makro dosyalarımız hariç. Bunun için gerekli kod aşağıdaki gibidir.
Sub tum_wblarda_tum_connectionlar_refresh()
On Error GoTo çık
For Each wB In Application.Workbooks
If Windows(wB.Name).Visible = True Then
If wB.Connections.Count > 0 Then
Debug.Print wB.Name
For Each cn In ActiveWorkbook.Connections
Debug.Print cn.Name
bBackground = cn.ODBCConnection.BackgroundQuery
cn.ODBCConnection.BackgroundQuery = False
cn.Refresh
cn.ODBCConnection.BackgroundQuery = bBackground
Next cn
End If
End If
Next wB
Exit Sub
çık:
MsgBox "hata"
End Sub