2016-11-22 83 views
4

我写了一个版本控制模块。只要我或其他维护人员登录,AutoExec宏就会启动它。它会查找自上次更新后创建或修改的数据库对象,然后向Versions表中添加一个条目,然后打开该表(已过滤为最后一条记录),因此我可以键入我执行的更改的摘要。如何检查VBA模块何时被修改?

它非常适用于表格,查询,表单,宏等,但我无法让它正确地为模块工作。

我发现提出一个最后修改日期两种不同的属性...

CurrentDB.Containers("Modules").Documents("MyModule").Properties("LastUpdated").Value 
CurrentProject.AllModules("MyModule").DateModified 

第一个(CurrentDB)始终显示为创建日期,除非你修改的说明“LASTUPDATED”该模块或界面中的某些东西。这告诉我,这个属性纯粹是为容器对象 - 而不是它的内容。

第二个工程更好。它准确地显示了我修改和编译/保存模块的日期。唯一的问题是,当您保存或编译模块时,它会再次保存/编译所有模块,因此将DateModified字段设置为全局相同的日期。它有点违背了在单个模块上使用DateModified属性的目的吗?

所以我的下一步行动将会更加剧烈。我想我需要维护所有模块的列表,并使用VBA Extensions对每个模块中的代码行进行计数。然后,如果代码行与列表中记录的不同 - 那么我知道模块已被修改 - 除了“自上次检查以来”我不知道什么时候“

有没有人更好的方法?我宁愿不采取我的下一步行动,因为我可以看到它显着影响数据库性能(以糟糕的方式)

+1

将模块导出为文本文件非常简单,那么您可以比较全文。我不会建议只检查行数,因为有人可以在一行中更改代码,并且不会被检测到。几年前,我喜欢'WinDiff.exe',它会显示两个文件的差异(我发现它仍然可以下载)。我建立了一个具有所有对象,控件,属性和值的'数据字典'数据库,然后可以比较旧的和新的来显示更改。此外,如果您使用SourceSafe(或现代版本),可以跟踪更改。 –

+0

无耻的插件,但[Rubberduck加载项](https://github.com/rubberduck-vba/Rubberduck)具有源代码管理功能。 – Comintern

+0

@Comintern当然,但它不处理任何Access特定的组件。 –

回答

2

这里有一个简单的建议:

  1. 计算MD5哈希每个模块。
  2. 将其存储在版本表中。
  3. 在AutoExec中为每个模块重新计算它并将其与Versions表中的值进行比较。如果不同,你可以认为它已经改变了(虽然MD5对安全性不好,但对于完整性来说它仍然是可靠的)。

要获得使用VBE扩展模块的文字,你可以做

Dim oMod As CodeModule 
Dim strMod As String 
Set oMod = VBE.ActiveVBProject.VBComponents(1).CodeModule 
strMod = oMod.Lines(1, oMod.CountOfLines) 

然后你就可以从this answer如下使用以下修改MD5哈希函数,你可以把每个哈希模块来存储它,然后在你的AutoExec中进行比较。

Public Function StringToMD5Hex(s As String) As String 
    Dim enc 
    Dim bytes() As Byte 
    Dim outstr As String 
    Dim pos As Integer 
    Set enc = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider") 
    'Convert the string to a byte array and hash it 
    bytes = StrConv(s, vbFromUnicode) 
    bytes = enc.ComputeHash_2((bytes)) 
    'Convert the byte array to a hex string 
    For pos = 0 To UBound(bytes) 
     outstr = outstr & LCase(Right("0" & Hex(bytes(pos)), 2)) 
    Next 
    StringToMD5Hex = outstr 
    Set enc = Nothing 
End Function 
+0

比什么更简单?这完全是我给出的完全相同的答案,除非您将数据存储在表中,并且您没有提及在模块被重命名或删除时无效的陈旧记录的警告。研究一种延迟绑定到.net散列函数的方式的荣誉,并没有意识到你可以做到这一点。 +1。 –

+0

@ Mat'sMug哈哈!对不起:PI意味着比OP的段落更简单“因此,我的下一步行动将变得更加激烈,我想我需要维护所有模块的列表,并且使用VBA统计每个模块中的代码行数然后,如果代码行与列表记录不同 - 那么我知道该模块已被修改 - 除了“自上次检查以来”,我只是不知道什么时候“ – Blackhawk

+0

我也太喜欢了如何迟到.NET功能 - 一直想知道如何做到这一点。去看看。实际上我曾想过使用MD5或我自己的校验和算法(它不是火箭科学),但认为它会比它的价值更麻烦。至于在哪里存储MD5哈希,我正在考虑将它作为一个属性存储在每个模块上。还没有尝试过 - 但它会避免使用表格并解决重命名问题。 – DHW

4

当模块被修改时,您不可能知道。 VBIDE API甚至不告诉你是否模块被修改,所以你必须自己弄清楚。


VBIDE API令人难以忍受 - 正如您已经注意到的那样。

Rubberduck尚未处理特定于主机的组件(例如表,查询等),但其解析器在自上一次解析后判断模块是否被修改方面做了很好的工作。

“自上次检查后修改”实际上是您所需要知道的。你不能靠行虽然统计,因为这个:

Option Explicit 

Sub DoSomething 
    'todo: implement 
End Sub 

将与此相同:

Option Explicit 

Sub DoSomething 
    DoSomethingElse 42 
End Sub 

而且很明显,你会希望这种改变被拾起和跟踪。比较每一行代码中的每个字符都可以工作,但速度要快得多。

总体思路是获取CodeModule的内容,对其进行散列,然后与之前的内容散列进行比较 - 如果有任何修改,我们正在查看“脏”模块。它是C#,我不知道是否有一个COM库可以容易地从VBA中散列字符串,但最糟糕的情况是,您可以在.NET中编译一个小型实用程序DLL,公开一个需要String并返回的COM可见函数一个散列,不应该太复杂。

下面是Rubberduck.VBEditor.SafeComWrappers.VBA.CodeModule相关的代码,如果它的任何帮助:

private string _previousContentHash; 
public string ContentHash() 
{ 
    using (var hash = new SHA256Managed()) 
    using (var stream = Content().ToStream()) 
    { 
     return _previousContentHash = new string(Encoding.Unicode.GetChars(hash.ComputeHash(stream))); 
    } 
} 

public string Content() 
{ 
    return Target.CountOfLines == 0 ? string.Empty : GetLines(1, CountOfLines); 
} 

public string GetLines(Selection selection) 
{ 
    return GetLines(selection.StartLine, selection.LineCount); 
} 

public string GetLines(int startLine, int count) 
{ 
    return Target.get_Lines(startLine, count); 
} 

这里TargetMicrosoft.Vbe.Interop.CodeModule对象 - 如果你在VBA的土地是那么这只是一个CodeModule,从VBA扩展性库;这样的事情:

Public Function IsModified(ByVal target As CodeModule, ByVal previousHash As String) As Boolean 

    Dim content As String 
    If target.CountOfLines = 0 Then 
     content = vbNullString 
    Else 
     content = target.GetLines(1, target.CountOfLines) 
    End If 

    Dim hash As String 
    hash = MyHashingLibrary.MyHashingFunction(content) 

    IsModified = (hash <> previousHash) 

End Function 

所以是的,你的“激烈”的解决方案几乎是唯一可靠的方法去实现它。几件事情要记住:

  • “保持所有模块的列表”,将工作,但如果你只储存模块名称和模块改名,缓存是陈旧的,你需要一种方法来使它无效。
  • 如果你存储每个模块对象的ObjPtr而不是他们的名字,我不确定它在VBA中是否可靠,但我可以告诉你,通过COM互操作,COM对象的哈希码不会始终一致在调用之间 - 所以你会有一个陈旧的缓存和一种方法来使它失效,也是如此。尽管如此,可能不是100%VBA解决方案的问题。

我会去Dictionary存储模块的对象指针作为一个关键,他们的内容哈希作为一个值。


这就是说作为Rubberduck项目的管理员,我宁愿看到你加入我们,帮助我们整合全功能的源代码控制(即特定主机的功能)直接进入VBE =)

Rubberduck's Source Control panel

+0

垫子,感谢您花时间在您的答案中提供如此多的细节。我不喜欢使用第三方API。根据我的经验,它们变得更加麻烦“我们必须重新安装访问并且数据库不能正常工作”。理想情况下,我想完全保留在核心参考内,但如果我需要使用VBE,我可以弯曲,因为它非常标准。足够容易做一个后期绑定和避免参考问题。 – DHW

+0

@DHW无论你是迟绑定还是早绑定第三方库都没有区别,如果库失败了,它会失败。如果库没有出现在它正在运行的机器上,那么只有在运行时才会知道它是否与迟绑定,编译时与早期绑定。对于早期绑定库的恐惧似乎是不合理的,特别是当你谈论VBIDE API时,任何运行VBA的计算机(甚至是脚本运行时 - 这些库都是任何Windows盒子上的标准)都是标准的。 –

+2

除非在更高版本的Access版本中进行了更改:Early Binding将您与特定版本的DLL联系在一起。后期绑定只会将您与任何具有您要查找的对象的dll绑定。因此,以Microsoft Excel库为例。如果我早期与版本15绑定 - 并且机器较旧,并且只有版本14,那么将会缺少引用问题。但是,如果我延迟绑定 - 它会看到我正在查找“Application.Excel”对象并将其解析为任何存在的库。 – DHW

2

我想我要补充,我想出了一个哈希/校验生成模块的最终代码,因为这真的是件我失踪。感谢@BlackHawk回答,通过表明您可以延迟.NET类的绑定来填补这个空白 - 现在这将为我开创很多可能性。

我已完成编写我的版本检查器。我遇到的几个警告使得很难依赖LastUpdated日期。

  1. 调整表或查询中的列的大小更改了LastUpdated日期。
  2. 编译编译所有模块的任何模块,从而更新了所有模块的LASTUPDATED日期(如已经指出的)
  3. 添加过滤器,以查看模式形式使形式的过滤器字段中被更新,这又更新LASTUPDATED日期。
  4. 当窗体或报表上使用SaveAsText,改变打印机或显示器驱动程序会影响PrtDevMode编码,所以需要计算校验

对于表之前带他们出去我建了一个字符串,它是表名称,所有字段名称及其大小和数据类型的拼接。然后我计算了这些散列。

对于查询,我简单地计算了SQL上的散列值。

对于模块,宏,表单和报告我使用Application.SaveAsText将其保存到临时文件。然后我把这个文件读入一个字符串并计算出一个散列值。对于表单和报告,我没有开始添加到字符串,直到“开始”行通过。

似乎现在工作,我还没有遇到过任何情况下,当事情没有真正改变时,它会提示版本修订。

为了计算校验和或散列,我构建了一个名为CryptoHash的类模块。以下是完整的源代码。我将字节数组优化为十六进制字符串转换更快。

Option Compare Database 
Option Explicit 

Private objProvider As Object   ' Late Bound object variable for MD5 Provider 
Private objEncoder As Object   ' Late Bound object variable for Text Encoder 
Private strArrHex(255) As String  ' Hexadecimal lookup table array 

Public Enum hashServiceProviders 
    MD5 
    SHA1 
    SHA256 
    SHA384 
    SHA512 
End Enum 

Private Sub Class_Initialize() 
    Const C_HEX = "ABCDEF" 
    Dim intIdx As Integer    ' Our Array Index Iteration variable 

    ' Instantiate our two .NET class objects 
    Set objEncoder = CreateObject("System.Text.UTF8Encoding") 
    Set objProvider = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider") 

    ' Initialize our Lookup Table (array) 
    For intIdx = 0 To 255 
    ' A byte is represented within two hexadecimal digits. 
    ' When divided by 16, the whole number is the first hex character 
    '      the remainder is the second hex character 
    ' Populate our Lookup table (array) 
    strArrHex(intIdx) = Mid(C_HEX, (intIdx \ 16) + 1, 1) & Mid(C_HEX, (intIdx Mod 16) + 1, 1) 
    Next 

End Sub 

Private Sub Class_Terminate() 
    ' Explicity remove the references to our objects so Access can free memory 
    Set objProvider = Nothing 
    Set objEncoder = Nothing 
End Sub 

Public Property Let Provider(NewProvider As hashServiceProviders) 

    ' Switch our Cryptographic hash provider 
    Select Case NewProvider 
    Case MD5: 
     Set objProvider = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider") 
    Case SHA1: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA1CryptoServiceProvider") 
    Case SHA256: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA256Managed") 
    Case SHA384: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA384Managed") 
    Case SHA512: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA512Managed") 
    Case Else: 
     Err.Raise vbObjectError + 2029, "CryptoHash::Provider", "Invalid Provider Specified" 
    End Select 

End Property 

' Converts an array of bytes into a hexadecimal string 
Private Function Hash_BytesToHex(bytArr() As Byte) As String 
    Dim lngArrayUBound As Long   ' The Upper Bound limit of our byte array 
    Dim intIdx As Long     ' Our Array Index Iteration variable 

    ' Not sure if VBA re-evaluates the loop terminator with every iteration or not 
    ' When speed matters, I usually put it in its own variable just to be safe 
    lngArrayUBound = UBound(bytArr) 

    ' For each element in our byte array, add a character to the return value 
    For intIdx = 0 To lngArrayUBound 
    Hash_BytesToHex = Hash_BytesToHex & strArrHex(bytArr(intIdx)) 
    Next 
End Function 

' Computes a Hash on the supplied string 
Public Function Compute(SourceString As String) As String 
    Dim BytArrData() As Byte   ' Byte Array produced from our SourceString 
    Dim BytArrHash() As Byte   ' Byte Array returned from our MD5 Provider 

    ' Note: 
    ' Because some languages (including VBA) do not support method overloading, 
    ' the COM system uses "name mangling" in order to allow the proper method 
    ' to be called. This name mangling appends a number at the end of the function. 
    ' You can check the MSDN documentation to see how many overloaded variations exist 

    ' Convert our Source String into an array of bytes. 
    BytArrData = objEncoder.GetBytes_4(SourceString) 

    ' Compute the MD5 hash and store in an array of bytes 
    BytArrHash = objProvider.ComputeHash_2(BytArrData) 

    ' Convert our Bytes into a hexadecimal representation 
    Compute = Hash_BytesToHex(BytArrHash) 

    ' Free up our dynamic array memory 
    Erase BytArrData 
    Erase BytArrHash 

End Function 
+0

我不知道这是否适合在Code Review网站上询问,但我想知道如果您愿意分享您的解决方案的完整源代码,请在此处描述。如果不是这个答案,那么也许作为一个外部链接。我非常有兴趣使用Git为Access VBA项目实施源代码控制,这将是一个巨大的帮助。我们没有管理员权限来尝试使用RubberDuck,所以我仍然试图构建自己的解决方案。 – BarrettNashville

+1

我不介意分享。我没有分享版本系统的完整代码,因为它与问题无关。我已将它放到我的Google云端硬盘上。该代码完全评论,所以它应该是自我解释。 https://drive.google.com/file/d/0B_uoYoBD3vqMMVpuM0tyWEFmMEk如果您最终使用它,只需在信用到期时给予贷记。 Thx为upvotes。 – DHW

+0

太棒了!谢谢。奇迹般有效。代码很好的评论和AutoExec宏是不言自明的。很酷!我很好奇你为什么在用户打开数据库时将版本检查放在AutoExec中,而不是保存数据库或关闭时?我确信有一个正当的理由,我只是习惯于在做了一堆更改之后,在退出之前做代码签入,而不是当我第一次进入代码时。 – BarrettNashville