多层数据库开发四:BDE会话期

简介:                                               第四章 BDE会话期  不管是单层、两层还是多层的数据库应用程序,一般都要用到BDE(BorlandDatabase Engine)。
                                              第四章 BDE会话期
  不管是单层、两层还是多层的数据库应用程序,一般都要用到BDE(BorlandDatabase Engine)。Delphi 4用TSession来管理BDE会话期对象,用TSessionList来管理和操纵一个应用程序中所有的BDE会话期对象。
  一般来说,并不需要显式地把TSession构件放到窗体或数据模块上,因为数据库应用程序在每次启动时会自动创建一个默认的BDE会话期对象叫Session。但如果开发的是一个多线程的数据库应用程序,就要显式地用到TSession构件,因为几个线程有可能要同时连接数据库,应当避免每次连接数据库时都要创建应用程序的另一个实例。
4.1 TSession
  TSession构件能够对一个应用程序内的一组TDatabase构件提供全局控制。当创建数据库应用程序(包括应用服务器)时,应用程序会自动创建一个默认的BDE会话期对象叫Session。在应用程序中加入新的TDatabase构件和新的数据集构件时,这些构件会自动地处于默认的BDE会话期对象即Session的控制之下。此外,BDE会话期对象还能够提供访问Paradox表的口令,指定网络控制文件所在的目录,控制数据库的连接。
  除了默认的BDE会话期对象外,有些应用程序需要用到另外的TSession构件。例如,如果应用程序要并行查询同一个数据库,此时,每一个查询都必须有单独的BDE会话期。多线程的数据库应用程序也需要有多个BDE会话期。
  您既可以在设计期加入TSession构件,也可以在运行期动态地创建TSession构件。
4.1.1 默认的BDE会话期对象
  所有的数据库应用程序都会自动包含一个默认的BDE会话期对象叫Session,它的SessionName属性是Default。默认的BDE会话期对象能够对所有的TDatabase构件提供全局控制,不管这些TDatabase构件是在设计期显式地加到窗体或数据模块上的还是在运行期动态创建的。注意:默认的BDE会话期对象在设计期是不存在的,它只存在于运行期。
  无论是在设计期还是在运行期加入TDatabase构件时,加入的TDatabase构件都会自动处于默认的BDE会话期对象管理之下。当然,如果您显式地使用了多个TSession构件,您也可以设置TDatabase构件的SessionName属性指定其中一个TSession构件。
  TSession最重要的属性是KeepConnections,如果这个属性设为True,即使当前没有打开数据集,也保持与数据库的连接。这样,下次打开数据集时,就不必再登录数据库。
  注意:应用程序千万不要试图删除默认的BDE会话期对象。
4.1.2 创建另外的BDE会话期对象
  有些情况下需要用到另外的BDE会话期对象。在设计期,您可以把一个或多个TSession构件加到窗体或数据模块上,然后在对象观察器中设置它们的属性,建立事件句柄,调用它们的方法。您也可以在运行期动态地创建BDE会话期对象。
  要在运行期动态地创建BDE会话期对象,可以参照下列步骤:
  第一步是声明一个TSession类型的变量。
  第二步是调用TSession的Create创建一个TSession的对象实例。Create会自动把Databases数组清为空,把KeepConnections属性设为True,同时还把新创建的BDE会话期对象加到由Sessions管理的BDE会话期对象的列表中。
  第三步是设置SessionName属性指定此BDE会话期对象的名称,注意不能与其他BDE会话期名称相同。TDatabase和数据集构件都是通过名称来区分BDE会话期对象。
  第四步是激活此BDE会话期对象,然后设置有关属性。
  下面的程序示例创建了一个BDE会话期对象,最后又删掉了这个BDE会话期对象。
var SecondSession: TSession;
Begin
SecondSession := TSession.Create;
With SecondSession Do
Try
SessionName := 'SecondSession';
KeepConnections := False;
Open;
...
Finally
SecondSession.Free;
End;
End;
  实际上,也可以调用TSessionList的OpenSession函数来创建BDE会话期对象,这个函数需要传递一个SessionName参数,指定要创建的BDE会话期对象的名称。如果与SessionName参数匹配的BDE会话期对象已存在,这个函数就激活它。程序示例如下:
Var MySession : TSession;
MySession := Sessions.OpenSession('MySession');
...
MySession.Free;
4.1.3 给BDE会话期对象命名
  TSession的SessionName属性用于给BDE会话期对象命名。对于默认的BDE会话期对象来说,它的SessionName属性是“Default”。
  在同一个应用程序内,BDE会话期对象的名称不能有重复。如果把TSession的AutoSessionName属性设为True,Delphi 4将自动为每个BDE会话期对象指定一个相异的名称,这样就不用担心重名。如果AutoSessionName属性设为False,应用程序就必须设置SessionName属性给每个BDE会话期对象指定一个相异的名称。在AutoSessionName属性设为True的情况下,不能直接修改SessionName属性的值。
  不过,使用AutoSessionName属性有许多限制。例如,如果窗体或数据模块上有多个TSession构件,就不能把AutoSessionName属性设为True。如果窗体或数据模块上已经有一个TSession构件,并且它的AutoSessionName属性设为True,就不能再把另一个TSession构件加到窗体或数据模块上。
  TDatabase构件和数据集构件都有一个SessionName属性,用于指定一个BDE会话期对象的名称。如果它们的SessionName属性为空,表示使用默认的BDE会话期对象。
  下面这个例子首先调用TSessionList的OpenSession创建一个BDE会话期对象,然后设置Database1的SessionName属性指定刚创建的BDE会话期对象。
var
IBSession: TSession;
...
Begin
IBSession := Sessions.OpenSession('InterBaseSession');
Database1.SessionName := 'InterBaseSession';
End;
4.1.4 激活BDE会话期对象
  如果TSession的Active属性返回True,表示BDE会话期对象处于活动状态。如果把这个属性设为True,将激活BDE会话期对象,并触发OnStartup事件。
  激活了BDE会话期对象后,就可以调用OpenDatabase函数来连接数据库。
  把Active属性设为True,相当于调用Open。把Active属性设为False,相当于调用Close。
  对于默认的BDE会话期对象即Session,最好不要把它的Active属性设为False。
  当BDE会话期对象被激活时将触发OnStartup事件,这样就有机会设置NetFileDir、PrivateDir和ConfigMode等属性,不过,NetFileDir和PrivateDir属性只在访问Paradox表才是有效的。ConfigMode属性用于设置哪些BDE别名是可见的。
4.1.5 KeepConnections属性
  如果KeepConnections属性设为True,即使当前没有打开任何数据集,也保持与数据库的连接。如果这个属性设为False,当所有的数据集都关闭时,就断开与数据库的连接。
  这个属性是针对动态生成的临时的TDatabase构件而言的,如果显式地使用了TDatabase构件,将以它的KeepConnections属性为准。
  如果一个应用程序需要频繁地打开和关闭所有的数据集,特别是当这些数据集是在一个远程数服务器上时,最好把KeepConnections属性设为True,这样可以避免再次登录到远程服务器。否则,应用程序不得不重新登录。
  不过,即使在KeepConnections属性设为True的情况下,仍然可以调用DropConnections函数把空的连接断开。所谓空的连接,是指当前并没有打开任何数据集,但由于KeepConnections属性设为True,仍然保持在连接状态。
4.1.6 打开和断开连接
  要打开一个数据库的连接,调用OpenDatabase函数。这个函数需要传递一个DatabaseName参数,用于指定要打开的数据库的名称,可以设为BDE别名或TDatabase构件的名称。对于Paradox或dBASE,DatabaseName参数也可以设为表所在的路径。
  下面的程序示例试图打开别名为DBDEMOS的数据库:
var
DBDemosDatabase: TDatabase;
Begin
DBDemosDatabase := Session.OpenDatabase('DBDEMOS');
...
End;
  OpenDatabase会首先自动激活BDE会话期(如果还没有激活的话),然后判断DatabaseName参数是否与BDE会话期对象所管理的TDatabase构件的名称匹配,如果没有找到匹配的数据库,OpenDatabase就会自动创建一个临时的TDatabase构件。最后,OpenDatabase调用TDatabase的Open连接数据库。
  OpenDatabase实际上是使一个内部的计数加1,只要这个计数大于0,数据库就处于连接状态。
  可以调用CloseDatabase函数来关闭一个数据库。与OpenDatabase一样,CloseDatabase也需要传递一个DatabaseName参数来指定要关闭的数据库,例如:
  Session.CloseDatabase(DBDemosDatabase);
  CloseDatabase实际上是使一个内部的引用计数减1,当引用计数减到0时,数据库才被关闭。
  如果DatabaseName参数指定的数据库是由一个临时的TDatabase构件管理的,而且KeepConnections属性设为False,当数据库被关闭的同时,相应的TDatabase构件也被删除。
  在KeepConnections属性设为False的情况下,对于那些临时创建的TDatabase构件来说,如果当前没有打开任何数据集,数据库将自动关闭,TDatabase构件将自动删除。对于那些显式加到窗体或数据模块上的TDatabase构件来说,需要调用Close才能关闭数据库。
  如果要一次关闭所有的数据库,可以把BDE会话期对象的Active属性设为False,或者调用Close函数。把Active属性设为False,会自动调用Close,而Close会关闭所有的数据库,删除所有临时创建的TDatabase构件,然后依次调用那些显式使用的TDatabase的Close,最后,把BDE会话期的句柄设为NIL。
  在打开和关闭数据库之前,可能需要调用FindDatabase函数来查找一个特定的数据库是否存在。FindDatabase函数需要传递一个DatabaseName参数,用于指定要查找的数据库,可以设为BDE别名或者TDatabase构件的名称。对于Paradox或dBASE表来说,可以设为表所在的路径。如果找到的话,FindDatabase函数就返回一个TDatabase构件,否则,就返回NIL。下面这个例子试图搜索别名为DBDEMOS的数据库:
var
DB: TDatabase;
Begin
DB := Session.FindDatabase('DBDEMOS');
If (DB = nil) then DB:=Session.OpenDatabase('DBDEMOS');
If Assigned(DB) and DB.Active then
 Begin
  DB.StartTransaction;
  ...
 End;
End;
4.2 检索有关BDE会话期的信息
  TSession提供了许多方法用于检索有关BDE会话期的信息如别名的参数等,下面简单介绍这些方法。
.GetAliasDriverName返回一个别名的驱动程序;
.GetAliasNames返回所有BDE别名的列表;
.GetAliasParams返回一个别名的参数的列表;
.GetConfigParams返回配置文件中的特定信息;
.GetDatabaseNames返回所有BDE别名的列表(含占用的别名);
.GetDriverNames返回已安装的驱动程序的列表;
.GetDriverParams返回一个驱动程序的参数;
.GetStoredProcNames返回一个数据库中的存储过程名;
.GetTableNames返回一个数据库中的表格名。
  上述方法中,除了GetAliasDriverName返回一个字符串外,其他方法都是返回一个列表。下面的例子试图检索所有的BDE别名:
  var List: TStringList;
Begin
List := TStringList.Create;
Try
Session.GetDatabaseNames(List);
...
Finally
List.Free;
End;
End;
4.3 管理BDE别名
  对于BDE会话期对象来说,BDE别名特别重要,许多方法都需要传递一个数据库的别名作为参数。TSession提供了管理BDE别名的功能。
4.3.1 指定别名的可见性
  ConfigMode属性用于设置哪些BDE别名对BDE会话期是可见的。ConfigMode属性是一个集合,默认值是[cmAll],相当于[cfmVirtual,cfmPersistent],表示所有的别名对BDE会话期都是可见的,包括BDE配置文件中定义的别名、BDE会话期创建的专用别名。
  使用ConfigMode属性的主要目的是限制别名的可见性。例如,可以把ConfigMode属性设为[cfmVirtual],表示只能看到本BDE会话期创建的别名,不能看到其他BDE会话期创建的别名,也不能看到BDE配置文件中定义的永久别名。
  在BDE会话期创建的别名只存在于内存中,默认情况下,只对本BDE会话期是可见的,其他BDE会话期即使是在同一个应用程序内都无法看到这些别名。
  如果要使BDE会话期创建的别名能够被所有的BDE会话期甚至其他应用程序看到,首先要调用SaveConfigFile把别名保存到BDE配置文件中,这样,其他BDE会话期或其他应用程序才可以使用这个别名。当然,ConfigMode 属性需要设为[cmAll]。
4.3.2 创建、修改和删除别名
  要创建一个SQL服务器的别名,可以调用AddAlias函数。要创建一个本地数据库如Paradox、dBASE或ASCII文本的别名,可以调用AddStandardAlias函数。
  AddAlias需要传递三个参数,其中,Name参数用于指定名称,Driver参数用于指定SQL Links驱动程序,List参数用于指定连接参数。程序示例如下:
var
AliasParams: TStringList;
Begin
AliasParams := TStringList.Create;
Try
With AliasParams Do
Begin
Add('OPEN MODE=READ');
Add('USER NAME=TOMSTOPPARD');
Add('SERVER NAME=ANIMALS:/CATS/PEDIGREE.GDB');
End;
Session.AddAlias('CATS', 'INTRBASE', AliasParams);
Finally
AliasParams.Free;
End;
End;
  与AddAlias不同的是,AddStandardAlias用于为Paradox、dBASE或文本创建别名,不需要连接参数,只需要指定一个路径和默认的驱动程序。程序示例如下:
  AddStandardAlias('MYDBDEMOS', 'C:/TESTING/DEMOS/', 'Paradox');
  要说明的是,调用AddAlias或AddStandardAlias函数创建的别名只存在于内存中,要把别名永久地保存到BDE配置文件中,请调用SaveConfigFile函数。
  创建了一个别名后,就可以调用ModifyAlias来修改别名的参数。ModifyAlias需要传递两个参数,一个是要修改的别名,还有一个是要修改的参数的列表。
  下面这个例子把CATS别名的OPEN MODE参数设为READ/WRITE:
var
List: TStringList;
Begin
List := TStringList.Create;
With List Do
Begin
Clear;
Add('OPEN MODE=READ/WRITE');
End;
Session.ModifyAlias('CATS', List);List.Free;
...
End;
  虽然CATS别名的参数有几个,但传递给ModifyAlias的列表中只需包含要修改的参数。
  要删除一个BDE会话期创建的别名,可以调用DeleteAlias函数。DeleteAlias只需要传递一个参数,即要删除的别名。
  注意:调用DeleteAlias函数仅仅是从内存中把一个别名删掉,如果要删除的别名已经永久地保存到BDE配置文件中,调用了DeleteAlias函数后还需要调用SaveConfigFile函数。
4.4 遍历所有的TDatabase构件
  这一节要介绍TSession的两个属性:Databases和DatabaseCount,用这两个属性可以遍历由一个BDE会话期对象管理的所有的TDatabase构件。
  Databases属性是一个数组,它的每一个元素是一个处于活动状态的TDatabase构件,这些TDatabase构件都处于BDE会话期对象的管理之下。
  DatabaseCount属性是一个整数,表明Databases数组中元素的个数。随着数据库的打开和关闭,DatabaseCount属性会自动变化。例如,在KeepConnections属性设为False并且没有显式使用TDatabase构件的情况下,每打开一个数据库,DatabaseCount属性就会加1,每关闭一个数据库,DatabaseCount属性就会减1。
  DatabaseCount属性一般要与Databases属性配合使用。例如,下面的代码把所有TDatabase构件的KeepConnection属性设为True:
var
MaxDbCount: Integer;
Begin
With Session Do
If (DatabaseCount > 0) then
For MaxDbCount := 0 to (DatabaseCount - 1) Do
Databases[MaxDbCount].KeepConnections:= True;
End;
4.5 访问Paradox表
  TSession的NetFileDir属性和PrivateDir属性只适用于Paradox表。其中,NetFileDir属性用于指定Paradox网络控制文件即PDOXUSRS.NET所在的目录,凡是需要共享Paradox表的应用程序必须指定这个文件所在的目录。PrivateDir属性用于指定Paradox表的私有目录,一些临时文件就存放在私有目录中。
  Delphi 4会自动从BDE配置文件中检索网络控制文件的位置,并把它赋给NetFileDir属性。也可以设置这个属性,指定另一个合法的网络路径。程序示例如下:
  Session.NetFileDir := ExtractFilePath(Application.EXEName);
  注意:只能在当前还没有打开任何Paradox表的情况下修改NetFileDir属性。
  如果PrivateDir属性为空,BDE会自动把当前目录作为私有目录。如果要运行的应用程序在一个远程的文件服务器上,为了避免老是读写文件服务器从而影响速度,最好把PrivateDir属性设为本地的某一个驱动器。
  注意:不能在设计期设置PrivateDir属性,否则,会出现“Directory Busy”的错误。另外,不要把PrivateDir属性设为一个驱动器的根目录,最好是子目录。程序示例如下:
  Session.PrivateDir := 'C:/TEMP';
4.6 口 令
  有的Paradox表和dBASE表是被口令保护的,访问这些表时需要提供口令。TSession提供了若干个方法和一个事件用于管理口令。
4.6.1 AddPassword
  TSession的AddPassword函数一般在访问需要输入口令的Paradox或dBase表之前调用,用于提供口令。AddPassword唯一的参数就是口令。程序示例如下:
var
Passwrd: String;
Begin
Passwrd := InputBox('Enter password', 'Password:', '');
Session.AddPassword(Passwrd);
Try
 Table1.Open
Except
  ShowMessage('Could not open table!');
Application.Terminate;
End;
End;
  上面这个例子中调用InputBox函数让用户输入口令,也可以调用PasswordDialog函数,或者用TEdit构件做一个编辑框,把PasswordChar属性设为星号。
  如果用PasswordDialog函数的话,需要传递BDE会话期对象作为参数,程序示例如下:
Procedure TForm1.Button1Click(Sender: TObject);
Begin
If PasswordDialog(Session) then
 Table1.Open
Else
 ShowMessage('No password given, could not open table!');
End;
  上述程序将打开一个“Enter password”对话框,如图4.1所示。
  图4.1 输入口令
  对话框上的“Add”按钮相当于调用AddPassword函数,“Remove”按钮相当于调用RemovePassword函数,“Remove All”按钮相当于RemoveAllPasswords函数。
  注意:要在程序中调用PasswordDialog函数,必须引用DBPWDlg单元。
如果您没有调用AddPassword或PasswordDialog函数来提供口令,当访问有口令保护的Paradox表和dBase表时,就会自动弹出如图4.1所示的对话框,让用户输入口令。
4.6.2 RemovePassword和RemoveAllPasswords
  TSession的RemovePassword用于删除一个先前用AddPassword输入的口令。RemovePassword只需要传递一个参数,即要删除的口令。程序示例如下:
  Session.RemovePassword('1234');
  TSession的RemoveAllPasswords函数用于删除先前所有输入的口令,程序示例如下:
  Session.RemoveAllPasswords;
4.6.3 OnPassword和GetPassword
  当程序试图打开一个受口令保护的Paradox表时将触发该事件,应当在处理这个事件的句柄中调用AddPassWord函数输入一个口令,然后把Continue参数设为True。
  调用GetPassword函数也会触发OnPassword事件。下面这个例子动态地把一个方法作为处理OnPassword事件的句柄:
Procedure TForm1. FormCreate(Sender: TObject);
Begin
Session.OnPassword := Password;
End;
  Password又调用InputBox函数打开一个输入框让用户输入口令,如果用户输入了口令的话,就把Continue参数设为True。
Procedure TForm1.Password(Sender: TObject;
var Continue: Boolean);
var Passwrd: String;
Begin
Passwrd := InputBox('Enter password', 'Password:', '');
Continue := (Passwrd > '');
Session.AddPassword(Passwrd);
End;
  如果用户输入的口令是错误的,则仍然不能打开Paradox表,因此,凡是要打开一个Paradox表的代码必须能处理异常。
  Procedure TForm1.OpenTableBtnClick(Sender: TObject);
const CRLF = #13 + #10;
Begin
Try
Table1.Open; {将触发OnPassword事件}
Except
On E:Exception Do
Begin
ShowMessage('Error!'+CRLF+E.Message+CRLF);
Application.Terminate;
End;
End;
End;
4.7 管理多个BDE会话期对象
  如果要创建一个多线程的数据库应用程序,就需要用多个TSession构件,而且必须在设计期显式地加到窗体或数据模块上,还要保证它们的SessionName属性是相异的。
  Delphi 4用TSessionList来管理和操纵一个应用程序中所有的BDE会话期对象,并且已自动声明了TSessionList的对象示例Sessions。
  如果要动态地创建一个新的BDE会话期对象,这就要用到TSessionList的OpenSession函数。这个函数只需要传递一个参数,即要创建的BDE会话期的名称。程序示例如下:
  Sessions.OpenSession('RunTimeSession' + IntToStr(Sessions.Count + 1));
  上述代码能保证BDE会话期的名称不会与已有的BDE会话期重复。
  TSessionList定义了一些属性和方法用来操纵BDE会话期对象,这里简单介绍一下:
.Count 返回BDE会话期对象的个数,包括活动的和非活动的;
.FindSession 查找一个指定的BDE会话期对象,如果没有找到,就返回NIL;
.GetSessionNames 返回所有BDE会话期对象的SessionName属性组成的列表;
.List 通过这个属性可以按名称访问一个BDE会话期对象;
.OpenSession 动态地创建一个BDE会话期对象;
.Sessions 通过这个属性可以按序号访问一个BDE会话期对象。
  在多线程的数据库应用程序中,在打开一个数据库之前,首先要检查这个数据库是否已经被其他线程打开。怎么检查呢?用TSessionList的Count属性和Sessions属性遍历所有的BDE会话期对象,逐个检查每个BDE会话期对象的Databases属性中是否包含要打开的数据库,如果有的话,说明这个数据库已经被某个线程打开,也就是说,不能再在这个BDE会话期内打开数据库,您得换下一个再进行检查。
  如果所有的BDE会话期对象都在使用这个数据库,就必须创建一个新的BDE会话期对象,然后再打开数据库。
相关文章
|
3天前
|
SQL 存储 关系型数据库
数据库开发之图形化工具以及表操作的详细解析
数据库开发之图形化工具以及表操作的详细解析
19 0
|
3月前
|
SQL 关系型数据库 MySQL
Python 数据库访问与ORM框架——打造高效开发利器
Python 作为一门广泛使用的编程语言,其在数据库访问方面也有着较为成熟的解决方案,其中ORM框架更是成为了开发者们的首选。本文将介绍 Python 中数据库访问和 ORM 框架的基本概念,以及如何使用 SQLAlchemy 这一优秀的 ORM 框架进行开发。
|
3月前
|
关系型数据库 API 数据库
Python数据库访问与ORM框架:加速开发、提升效率
在现代软件开发中,数据库是不可或缺的组成部分。本文介绍了Python中数据库访问的重要性,并探讨了ORM框架(例如SQLAlchemy)如何帮助程序员加速开发、提升效率。通过使用ORM框架,开发人员可以轻松地将Python对象映射到数据库表,并且可以通过简洁的API进行数据库操作。此外,本文还讨论了ORM框架在处理复杂查询、维护数据一致性和实现数据库迁移方面的优势。
|
3月前
|
Cloud Native 关系型数据库 分布式数据库
|
3月前
|
存储 关系型数据库 MySQL
Linux C/C++ 开发(学习笔记八):Mysql数据库图片存储
Linux C/C++ 开发(学习笔记八):Mysql数据库图片存储
49 0
|
3月前
|
关系型数据库 MySQL 数据库
Linux C/C++ 开发(学习笔记七):Mysql数据库C/C++编程实现 插入/读取/删除
Linux C/C++ 开发(学习笔记七):Mysql数据库C/C++编程实现 插入/读取/删除
49 0
|
3月前
|
SQL 数据库 C++
C++ Qt开发:Charts与数据库组件联动
Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍`Charts`组件与`QSql`数据库组件的常用方法及灵活运用。在之前的文章中详细介绍了关于`QCharts`绘图组件的使用方式,本章将继续延续这个知识点,通过使用`QSql`数据库模块动态的读取某一个时间节点上的数据,当用户点击查询数据时则动态的输出该事件节点的所有数据,并将数据绘制到图形组件内,实现动态查询图形的功能。
34 0
C++ Qt开发:Charts与数据库组件联动
|
3月前
|
数据库连接 数据库
kettle开发篇-数据库查询
kettle开发篇-数据库查询
43 0
|
3天前
|
SQL 存储 关系型数据库
数据库开发之mysql前言以及详细解析
数据库开发之mysql前言以及详细解析
14 0
|
3月前
|
NoSQL 关系型数据库 MySQL
基于Python和mysql开发的智慧校园答题考试系统(源码+数据库+程序配置说明书+程序使用说明书)
基于Python和mysql开发的智慧校园答题考试系统(源码+数据库+程序配置说明书+程序使用说明书)

热门文章

最新文章