{ 单元名: ScAutoUpdateUnit 作者: 张引 时间: 2003/12/29 作用: 升级ACCESS数据库文件 限制: 1.只能添加字段,修改字段类型,大小(字符串类型),不能删除字段 2.只支持如下字段: ftString, ftSmallint, ftInteger, ftBoolean, ftSingle, ftDouble, ftCurrency, ftDateTime, ftMemo, ftOLEObject 注意: 必须设置TScUpdater.CurrentFileVer,以实现根据版本号判断自动升级。 } unit ScAutoUpdateUnit; interface uses SysUtils, DB, ADODB, Classes, ScTablesUnit{, ScFileArchiver, ScFileArchiverConsts}; const MaxFieldCount = 512; PrimaryKey = 'PrimaryKey'; type PFieldDefs = ^TFieldDefs; TFieldDefs = array [0..MaxFieldCount - 1] of TScFieldDef; PTableDef = ^TTableDef; // 表定义结构 TTableDef = record // 表名 TableName: string; // 字段数 FieldCount: Integer; // 字段结构数组 FieldDefs: PFieldDefs; // 是否需要重新创建 Recreate: Boolean; // 重新创建主键 RecreatePrimaryKey: Boolean; end; TSQLType = (stAlter, stCreate, stReCreate); TUpdateEventType = (uetAddFields, uetKeys, uetAfterUpdate); TUpdateEvent = procedure (ATableName: string; AEventType: TUpdateEventType; ASQLType: TSQLType; AConnection: TADOConnection); TUpdatedEvent = procedure (AFileVersion, ACurVersion: string; AConnection: TADOConnection; ASucceed: Boolean); EScUpdater = class(Exception); TScUpdater = class(TObject) private FTableDefList: TList; FFileName: string; FConnection: TADOConnection; FFileVer: string; FQuery: TADOQuery; FForceUpdate: Boolean; FForceCheck: Boolean; FCurFileVersion: string; FOnUpdateData: TUpdateEvent; FOnUpdated: TUpdatedEvent; function GetCurFileVersion: string; // 返回True:表存在,返回False: 表不存在 function CheckTable(ATableName: string): Boolean; procedure GenerateSQL(ATableDef: PTableDef; ASQLType: TSQLType; ASQLList: TStrings); procedure InternalExcuteSQL(ASQL: string; AHideException: Boolean = False; AOpen: Boolean = False); function ExcuteUpdateSQL(ASQLList: TStrings): Boolean; procedure SetForceUpdate(const Value: Boolean); procedure SetForceCheck(const Value: Boolean); // 字符串是否是事件 // 事件字符串格式:"事件名 表名 操作类型" function CheckEvent(AText: string): Boolean; public constructor Create; destructor Destroy; override; // 打开(AFileVers:打开文件的版本,程序中最新的文件版本号) procedure Open(AFileName: string; AConnection: TADOConnection; AFileVer, ACurrentFileVer: string); // 关闭 procedure Close; // 执行升级操作 function ExcuteUpdate: Boolean; // 执行其他SQL语句(如建立、删除索引) procedure ExcuteSQL(ASQL: string); // 添加表定义 function AddTableDef(ATableName: string; AFieldDefs: PFieldDefs; AFieldCount: Integer; AReCreate, ARecreatePK: Boolean): Integer; // 文件是否需要升级(根据版本号判断) function FileNeedUpdate: Boolean; // 强制进行表和字段的升级检查 property ForceCheck: Boolean read FForceCheck write SetForceCheck; // 强制升级所有表和字段 property ForceUpdate: Boolean read FForceUpdate write SetForceUpdate; // 最新文件版本 property CurrentFileVer: string read GetCurFileVersion; // 事件 // 重建关键字之前 property OnUpdateData: TUpdateEvent read FOnUpdateData write FOnUpdateData; // 完成升级后 property OnUpdated: TUpdatedEvent read FOnUpdated write FOnUpdated; end; const SQLTypeStrs: array [TSQLType] of string = ('Modify', 'Create', 'ReCreate'); SOnUpdateData = 'OnUpdateData'; implementation uses ScUtils; function StrToSQLType(ASQLType: string): TSQLType; var I: TSQLType; begin Result := stAlter; for I := Low(SQLTypeStrs) to High(SQLTypeStrs) do begin if SameText(ASQLType, SQLTypeStrs[I]) then begin Result := I; Break; end; end; end; { TScUpdater } function TScUpdater.AddTableDef(ATableName: string; AFieldDefs: PFieldDefs; AFieldCount: Integer; AReCreate, ARecreatePK: Boolean): Integer; var pRec: PTableDef; begin New(pRec); pRec^.TableName := ATableName; pRec^.FieldCount := AFieldCount; pRec^.FieldDefs := AFieldDefs; pRec^.Recreate := AReCreate; pRec^.RecreatePrimaryKey := ARecreatePK; Result := FTableDefList.Add(pRec); end; function TScUpdater.CheckEvent(AText: string): Boolean; var strText, strEvent, strTableName, strEventType, strSQLType: string; SQLType: TSQLType; iPos: Integer; begin Result := False; strText := AText; // 检查事件名称 iPos := Pos(' ', strText); if iPos > 0 then begin strEvent := Copy(strText, 1, iPos - 1); Delete(strText, 1, iPos); if SameText(strEvent, SOnUpdateData) then begin // 事件类型 iPos := Pos(' ', strText); strEventType := Copy(strText, 1, iPos - 1); Delete(strText, 1, iPos); // 表名 iPos := Pos(' ', strText); strTableName := Copy(strText, 1, iPos - 1); Delete(strText, 1, iPos); // 操作类型 strSQLType := strText; SQLType := StrToSQLType(strSQLType); Result := True; if Assigned(FOnUpdateData) then FOnUpdateData(strTableName, TUpdateEventType(StrToInt(strEventType)), SQLType, FConnection); end; end; end; function TScUpdater.CheckTable(ATableName: string): Boolean; var I: Integer; Names: TStringList; begin Names := TStringList.Create; try FConnection.GetTableNames(Names); if Names.IndexOf(ATableName) < 0 then Result := False else Result := True; finally Names.Free; end; end; procedure TScUpdater.Close; begin FQuery.Close; end; constructor TScUpdater.Create; begin FForceUpdate := False; FForceCheck := False; FTableDefList := TList.Create; FQuery := TADOQuery.Create(nil); end; destructor TScUpdater.Destroy; begin Close; FQuery.Free; ClearPointerList(FTableDefList); FTableDefList.Free; inherited; end; procedure TScUpdater.ExcuteSQL(ASQL: string); begin InternalExcuteSQL(ASQL); end; function TScUpdater.ExcuteUpdate: Boolean; var I: Integer; pRec: PTableDef; SQLs: TStringList; SQLType: TSQLType; bHasError: Boolean; sError: string; begin Result := False; bHasError := False; sError := ''; if FileNeedUpdate then begin SQLs := TStringList.Create; try for I := 0 to FTableDefList.Count - 1 do begin pRec := PTableDef(FTableDefList[I]); if CheckTable(pRec^.TableName) then begin if pRec^.Recreate then SQLType := stReCreate else SQLType := stAlter; end else SQLType := stCreate; GenerateSQL(pRec, SQLType, SQLs); if SQLs.Count > 0 then if not ExcuteUpdateSQL(SQLs) then begin bHasError := True; sError := sError + #13#10 + Format('Update operation [%s] on table [%s] can not be executed!', [SQLTypeStrs[SQLType], pRec^.TableName]); end; end; finally SQLs.Free; end; if bHasError then MessageWarning(0, '升级文件时发生错误,无法完成升级。'#13#10'错误信息:' + sError) else Result := True; if Assigned(FOnUpdated) then FOnUpdated(FFileVer, CurrentFileVer, FConnection, Result); end; end; function TScUpdater.ExcuteUpdateSQL(ASQLList: TStrings): Boolean; var I: Integer; HideExcption: Boolean; begin Result := False; try for I := 0 to ASQLList.Count - 1 do begin if not CheckEvent(ASQLList[I]) then begin HideExcption := ASQLList.Objects[I] <> nil; if HideExcption then HideExcption := Boolean(Integer(ASQLList.Objects[I])); InternalExcuteSQL(ASQLList[I], HideExcption); end; end; Result := True; except end; end; function TScUpdater.FileNeedUpdate: Boolean; begin if GetCurFileVersion = '' then raise EScUpdater.Create('必须设置TScUpdater.CurrentFileVer!'); Result := (ScCompareFileVer(FFileVer, GetCurFileVersion) < 0) or FForceCheck; end; function SameFieldType(AFieldType: TFieldType; AScFieldType: TScMDBFieldType): Boolean; begin Result := False; case AScFieldType of ftString: Result := (AFieldType = DB.ftWideString) or (AFieldType = DB.ftString); ftByte: Result := AFieldType = DB.ftWord; ftSmallint: Result := AFieldType = DB.ftSmallint; ftInteger: Result := AFieldType = DB.ftInteger; ftBoolean: Result := AFieldType = DB.ftBoolean; ftSingle: Result := AFieldType = DB.ftFloat; ftDouble: Result := AFieldType = DB.ftFloat; ftCurrency: Result := (AFieldType = DB.ftCurrency) or (AFieldType = DB.ftBCD); ftDateTime: Result := AFieldType = DB.ftDateTime; ftMemo: Result := AFieldType = DB.ftMemo; ftOLEObject: Result := AFieldType = DB.ftBlob; end; end; procedure TScUpdater.GenerateSQL(ATableDef: PTableDef; ASQLType: TSQLType; ASQLList: TStrings); var bHasKey: Boolean; function GenerateCreateSQL: string; var I: Integer; Def: TScFieldDef; strField, strFields, strKeyFields: string; begin Result := ''; if ATableDef^.FieldCount > 0 then begin // CREATE TABLE table1 Result := Format('CREATE TABLE %s ', [ATableDef^.TableName]); strFields := ''; strKeyFields := ''; for I := 0 to ATableDef^.FieldCount - 1 do begin Def := ATableDef^.FieldDefs[I]; // field1 type strField := Def.FieldName + ' ' + ScMDBFieldTypeName[Def.FieldType]; case Def.FieldType of ftString: // field1 type (size) strField := strField + Format(' (%d)', [Def.Size]); ftFMTBCD: // field1 type (size, Precision) strField := strField + Format(' (%d,%d)', [Def.Size, Def.Precision]); end; if Def.NotNull then // field1 type (size) NOT NULL strField := strField + ' ' + 'NOT NULL'; if Def.PrimaryKey then strKeyFields := strKeyFields + Def.FieldName + ', '; strFields := strFields + strField + ', '; end; if strKeyFields <> '' then begin Delete(strKeyFields, Length(strKeyFields) - 1, 2); // CONSTRAINT PrimaryKey PRIMARY KEY (field1, field2...) strKeyFields := Format('CONSTRAINT %s PRIMARY KEY (%s)', [PrimaryKey, strKeyFields]); end else Delete(strFields, Length(strFields) - 1, 2); // (field1 type (size) NOT NULL, field2 type (size) NOT NULL..., CONSTRAINT PrimaryKey PRIMARY KEY (field1, field2...)) strFields := Format('(%s)', [strFields + strKeyFields]); // CREATE TABLE table1 (field1 type (size) NOT NULL, field2 type (size) NOT NULL..., CONSTRAINT PrimaryKey PRIMARY KEY (field1, field2...)) Result := Result + strFields; end; end; type TAlterType = (atAddField, atAlterField, atDropField, atAddIndex, atDropIndex); function GenerateSingleFieldAlterSQL(ATableName: string; AFieldDef: TScFieldDef; AOp: TAlterType; var ANeedDefault: Boolean): string; begin Result := ''; ANeedDefault := False; case AOp of atAddField: begin Result := Format('ALTER TABLE %s ADD COLUMN %s %s', [ATableName, AFieldDef.FieldName, ScMDBFieldTypeName[AFieldDef.FieldType]]); case AFieldDef.FieldType of ftString: Result := Result + Format(' (%d)', [AFieldDef.Size]); ftFMTBCD: Result := Result + Format(' (%d,%d)', [AFieldDef.Size, AFieldDef.Precision]); end; if AFieldDef.NotNull then begin Result := Result + ' NOT NULL'; case AFieldDef.FieldType of ftByte, ftSmallint, ftInteger, ftBoolean, ftSingle, ftDouble, ftCurrency, ftDateTime, ftFMTBCD: ANeedDefault := True; end; end; end; atDropField: begin Result := Format('ALTER TABLE %s DROP COLUMN %s', [ATableName, AFieldDef.FieldName]); end; atAlterField: begin Result := Format('ALTER TABLE %s ALTER COLUMN %s %s', [ATableName, AFieldDef.FieldName, ScMDBFieldTypeName[AFieldDef.FieldType]]); case AFieldDef.FieldType of ftString: Result := Result + Format(' (%d)', [AFieldDef.Size]); ftFMTBCD: Result := Result + Format(' (%d,%d)', [AFieldDef.Size, AFieldDef.Precision]); end; if AFieldDef.NotNull then begin Result := Result + ' NOT NULL'; case AFieldDef.FieldType of ftByte, ftSmallint, ftInteger, ftBoolean, ftSingle, ftDouble, ftCurrency, ftDateTime, ftFMTBCD: ANeedDefault := True; end; end; end; end; end; function GenerateDefaultValueSQL(ATableName: string; AFieldDef: TScFieldDef): string; begin Result := ''; case AFieldDef.FieldType of ftByte, ftSmallint, ftInteger: Result := Format('UPDATE %s SET %s = %d WHERE IsNull(%s)', [ATableName, AFieldDef.FieldName, 0, AFieldDef.FieldName]); ftSingle, ftDouble, ftCurrency, ftFMTBCD: Result := Format('UPDATE %s SET %s = %f WHERE IsNull(%s)', [ATableName, AFieldDef.FieldName, 0.0, AFieldDef.FieldName]); ftBoolean: Result := Format('UPDATE %s SET %s = %s WHERE IsNull(%s)', [ATableName, AFieldDef.FieldName, 'FALSE', AFieldDef.FieldName]); ftDateTime: Result := Format('UPDATE %s SET %s = ''%s'' WHERE IsNull(%s)', [ATableName, AFieldDef.FieldName, '2000-1-1 12:00:00', AFieldDef.FieldName]); end; end; function GenerateSingleKeyAlterSQL(ATableName, AIndexName, AFieldNames: string; AOp: TAlterType): string; begin Result := ''; case AOp of atAddIndex: begin Result := Format('ALTER TABLE %s ADD CONSTRAINT %s Primary Key (%s)', [ATableName, AIndexName, AFieldNames]); end; atDropIndex: begin Result := Format('ALTER TABLE %s DROP CONSTRAINT %s', [ATableName, AIndexName]); end; end; end; procedure GenerateAlterSQL; var I, J: Integer; Field: TField; pDef: PScFieldDef; AddList, ModifyList: TList; KeyFields: string; bNeedDefaultValue: Boolean; begin InternalExcuteSQL(Format('SELECT * FROM %s WHERE 0=1', [ATableDef^.TableName]), False, True); AddList := TList.Create; ModifyList := TList.Create; try KeyFields := ''; for I := 0 to ATableDef^.FieldCount - 1 do begin pDef := @ATableDef^.FieldDefs^[I]; if (KeyFields <> '') and pDef^.PrimaryKey then KeyFields := KeyFields + ', '; if pDef^.PrimaryKey then KeyFields := KeyFields + pDef^.FieldName; AddList.Add(pDef); end; { if KeyFields <> '' then Delete(KeyFields, Length(KeyFields) - 1, 2);} for I := 0 to FQuery.Fields.Count - 1 do begin Field := FQuery.Fields[I]; for J := 0 to AddList.Count - 1 do begin pDef := PScFieldDef(AddList[J]); if SameText(Field.FieldName, pDef^.FieldName) then begin if FForceUpdate or pDef^.ForceUpdate then ModifyList.Add(pDef) else begin if not SameFieldType(Field.DataType, pDef^.FieldType) then ModifyList.Add(pDef) else if (Field.DataType in [ftWideString]) and (Field.Size <> pDef^.Size) then ModifyList.Add(pDef) // ADO读不到数据库中字段的Required,所以这句代码无用 (* else if Field.Required <> pDef^.NotNull then ModifyList.Add(pDef)*); end; AddList.Remove(pDef); Break; end; end; end; for I := 0 to ModifyList.Count - 1 do begin pDef := PScFieldDef(ModifyList[I]); ASQLList.Add(GenerateSingleFieldAlterSQL(ATableDef^.TableName, pDef^, atAlterField, bNeedDefaultValue)); if bNeedDefaultValue then ASQLList.Add(GenerateDefaultValueSQL(ATableDef^.TableName, pDef^)); end; for I := 0 to AddList.Count - 1 do begin pDef := PScFieldDef(AddList[I]); ASQLList.Add(GenerateSingleFieldAlterSQL(ATableDef^.TableName, pDef^, atAddField, bNeedDefaultValue)); if bNeedDefaultValue then ASQLList.Add(GenerateDefaultValueSQL(ATableDef^.TableName, pDef^)); end; if AddList.Count > 0 then // 添加事件 ASQLList.Add(Format('%s %d %s %s', [SOnUpdateData, Ord(uetAddFields), ATableDef.TableName, SQLTypeStrs[ASQLType]])); if ATableDef.RecreatePrimaryKey then begin if bHasKey then ASQLList.AddObject(GenerateSingleKeyAlterSQL(ATableDef^.TableName, PrimaryKey, KeyFields, atDropIndex), TObject(Integer(True))); // 添加事件 ASQLList.Add(Format('%s %d %s %s', [SOnUpdateData, Ord(uetKeys), ATableDef.TableName, SQLTypeStrs[ASQLType]])); ASQLList.Add(GenerateSingleKeyAlterSQL(ATableDef^.TableName, PrimaryKey, KeyFields, atAddIndex)); end; finally AddList.Free; ModifyList.Free; end; end; procedure CheckHasKey; var Table: TADOTable; I: Integer; begin bHasKey := False; Table := TADOTable.Create(nil); try Table.Connection := FConnection; Table.TableName := ATableDef^.TableName; Table.Open; for I := 0 to Table.FieldCount - 1 do if pfInKey in Table.Fields[I].ProviderFlags then begin bHasKey := True; Break; end; finally Table.Free; end; end; begin ASQLList.Clear; bHasKey := True; case ASQLType of stAlter: begin // zhangyin 2014-01-26虽然此方法可以避免调试时弹出异常,但是会占用比较长的时间 // (4500行清单的项目会将升级数据库时间加长一倍以上 1s->2.4s) // 所以还是屏蔽掉。 //CheckHasKey; GenerateAlterSQL; end; stCreate: ASQLList.Add(GenerateCreateSQL); stReCreate: begin ASQLList.Add(Format('DROP TABLE %s', [ATableDef^.TableName])); ASQLList.Add(GenerateCreateSQL); end; end; if ASQLList.Count > 0 then ASQLList.Add(Format('%s %d %s %s', [SOnUpdateData, Ord(uetAfterUpdate), ATableDef^.TableName, SQLTypeStrs[ASQLType]])); end; function TScUpdater.GetCurFileVersion: string; begin Result := FCurFileVersion; end; procedure TScUpdater.InternalExcuteSQL(ASQL: string; AHideException, AOpen: Boolean); begin FQuery.Close; FQuery.SQL.Clear; FQuery.SQL.Add(ASQL); try if AOpen then FQuery.Open else FQuery.ExecSQL; except if not AHideException then raise; end; end; procedure TScUpdater.Open(AFileName: string; AConnection: TADOConnection; AFileVer, ACurrentFileVer: string); begin FFileName := AFileName; FConnection := AConnection; FFileVer := AFileVer; if AFileVer = '' then FFileVer := '0.0.0.0'; FCurFileVersion := ACurrentFileVer; // 为空时,设一个很大的当前版本号,强制升级 if FCurFileVersion = '' then FCurFileVersion := '100000.0.0.0'; FQuery.Connection := AConnection; ClearPointerList(FTableDefList); end; procedure TScUpdater.SetForceCheck(const Value: Boolean); begin FForceCheck := Value; end; procedure TScUpdater.SetForceUpdate(const Value: Boolean); begin FForceUpdate := Value; end; end.