|
![]() |
|
Registriert seit: 29. Nov 2010 3.072 Beiträge Delphi 2010 Enterprise |
#1
Die Exclusivsperre und die Meldung "zu viele Nutzer" könnte auch die Folge von fehlerhaften oder falsch bewerteten Zugriffsversuchen sein.
Muster: Jeder Zugriff der 3 Nutzer wird (auf Dateiebene) nicht sauber beendet, jeder weitere Zugriff ist dann ggF. eine neue Verbindung. Ich würde mal schauen, ob ich auf Seite des Dateiservers, der die Access Datei und die Lockdatei hostet, einen genauen Überblick über die (offenen) Dateihandles bekommen kann. Falls es immer mehr werden, stimmt ja die Meldung "zu viele Nutzer".
Gruß, Jo
|
![]() |
Registriert seit: 3. Sep 2004 441 Beiträge Delphi 10.4 Sydney |
#2
Schon mal danke für die Rückmeldungen!
![]() Es ist immer sinnvoll, wenn alle Programme, die auf eine Datenbank zugreifen, den gleichen und vollständigen Connectionstring nutzen und nicht einen, der mal so gerade eben (hoffentlich) den Minimalanforderungen gerecht wird.
Delphi-Quellcode:
Beim Betrachten der TAdoConnection in diesem neuen Form ist mir wieder in den Sinn gekommen, dass ja viele Attribute direkt am Delphi-Objekt geändert werden können (z.b. db.Mode := cmShareDenyNone; ). Ob dies jetzt Vorrang vor dem 'Mode=Share Deny None;' aus dem ConnectionString hat, sich evtl. sogar wiederspricht oder ignoriert wird: Ich habe es vorsichtshalber nochmal explizit reingeschrieben, im notfall gilt hier: "Doppelt hält besser", da die Werte ja an beiden Stellen identisch sind.
db.Close;
db.Mode := cmShareDenyNone; db.KeepConnection := HAL_Registry_GetBool('KeepConnection', True); db.LoginPrompt := False; db.ConnectionString := 'Provider=Microsoft.Jet.OLEDB.4.0;' + 'User ID=Admin;' + 'Data Source=' + _FileName + ';' + 'Mode=Share Deny None;' + 'Jet OLEDB:System database="";' + 'Jet OLEDB:Registry Path="";' + 'Jet OLEDB:Database Password="";' + 'Jet OLEDB:Engine Type=5;' + 'Jet OLEDB:Database Locking Mode=1;' + 'Jet OLEDB:Global Partial Bulk Ops=2;' + 'Jet OLEDB:Global Bulk Transactions=1;' + 'Jet OLEDB:New Database Password="";' + 'Jet OLEDB:Create System Database=False;' + 'Jet OLEDB:Encrypt Database=False;' + 'Jet OLEDB:Don''t Copy Locale on Compact=False;' + 'Jet OLEDB:Compact Without Replica Repair=False;' + 'Jet OLEDB:SFP=False;' + 'Persist Security Info=False;'; // if db.CursorLocation <> clUseServer then begin // db.CursorLocation := clUseServer; // end; c1 := db.ConnectionString; db.Open; c2 := db.ConnectionString; Die Idee mit dem "Minimalismus" war und ist auch die Angst vor Exceptions beim Verbindungsaufbau. Falls mein Programm jetzt unter einer anderen Windows-Version eingesetzt wird, und ich ein Attribut mitgebe, was dort vielleicht nicht bekannt ist oder unterstützt wird: Jetzt geht hier gar nichts mehr (Verbindungsaufbau aller Datenbanken schlägt fehl), weil ein -von mir eh nicht "gewolltes"- übergebens Attribut nicht bekannt ist. Wahrscheinlich passiert gar nichts, und die Sorge ist unbegründet. Z.B. verwende ich seit Jahren 'Persist Security Info=False;' - der Assistent baut das Attribut auch beim Verbindungsaufbau nicht mit ein - zu stören scheint es aber auch nicht. Was mir hier noch Sorgen macht, ist db.CursorLocation. Ich habe in der OnlineHilfe und im Internet schon einiges drüber gelesen, weiß aber nicht, "ob" und in "wie weit" das für mich relevant ist. Nehmen wir mal an, alle Abfragen bei allen Tabellen und Kunden liefern i.d.R. nicht mehr als 20 Datensätze zurück. Hat CursorLocation jetzt eine Auswirkung auf irgendwelche Datensatz- oder Tabellensperren? Auf die Geschwindigkeit? In der OH steht u.a. "Viele Server unterstützen außerdem nur Vorwärts-Cursor, bei denen der Satzzeiger in der Ergebnismenge nicht in Richtung Tabellenanfang bewegt werden kann." --> Unerwartetes Verhalten / Probleme beim Ausführen einfacher Operationen wie AdoQuery.First etc.? Hört sich gefährlich an... Generell würde ich den Wert ja "unverändert" auf clUseClient lassen. -Spricht was dagegen? -TAdoQuery hat auch die Eigenschaft "CursorLocation". Hier ebenfalls clUseClient? ![]() Ich würde mal schauen, ob ich auf Seite des Dateiservers, der die Access Datei und die Lockdatei hostet, einen genauen Überblick über die (offenen) Dateihandles bekommen kann
-Erzeugt nur jede neue TAdoConnection.Open ein zusätzliches Handle auf die .mdb-Datei oder auch schon z.B. jedes TAdoQuery mit .Open? Bei der großen Umstellung (Link siehe Oben) wurde auch flächig der Zugriff der AdoQuerys überarbeitet. In der Tat habe ich schon bei vielen Kunden auf den FileServern gesehen, das es *schäm* sehr, sehr viele offene Handles auf die Datenbankdatei gegeben hat. Aus anderen Beiträgen hier im Forum habe ich entnommen, dass es nicht langt, ein AdoQuery einfach nur freizugeben (mit FreeAndNil), nein es MUSS explizit mit "Close" geschlossen werden. Bestätigung, Ja, Nein, andere Meinung? Beispielhaft sieht mein Zugriff auf die Datenbank immer so oder ähnlich aus:
Delphi-Quellcode:
-try..finally wäre natürlich innerhalb von try..except feiner, und so würde ich das heute auch machen. Aber so muss q auch ordentlich freigegeben werden: q wird ja am Anfang mit NIL initialisiert, und im schlimmsten Fall wird FreeAndNil(Nil) aufgerufen - es passiert nichts schlimmes. Im Falle einer Exception wird nun das wirklich erzeute q ordnungsgemäß freigegeben, da das Programm nach except..end weiterläuft, und diese Zeile somit in jedem Fall aufgerufen wird. Daran sollte es -wenn auch "ungewöhnlich" im vergleich zu einem eingebetteten try..finally- also nicht liegen, dass unnötige FileHandles auf dem Server offen bleiben.
function TRecurrencePattern.SaveToDatabase(db: TAdoQuery): Boolean;
var i: integer; q: TADOQuery; begin Result := False; q := NIL; try if not assigned(db) then begin Log('TRecurrencePattern.SaveToDatabase: db unassigned.'); Exit; end; q := TADOQuery.Create(Self); q.Connection := db; CloseQueryAndClearSQLText(q); // diese vier Zeilen sind in eigenen Prozeduren mit dortiger Fehlerbehandlung, nur zum Verständnis hier direkt q.SQL.Add('SELECT * FROM T_SerienMuster WHERE refTermin=:ID'); q.Parameters.ParamByName('ID').Value := _refTermin; q.Open; q.First; if q.Eof then q.Insert else q.Edit; q.FieldByName('refTermin').AsInteger := TerminID; q.FieldByName('blRecurrsJAN').AsBoolean := blRecurrsJanuar; q.FieldByName('blRecurrsFEB').AsBoolean := blRecurrsFebruar; q.FieldByName('blRecurrsMAR').AsBoolean := blRecurrsMaerz; q.FieldByName('blRecurrsAPR').AsBoolean := blRecurrsApril; q.FieldByName('blRecurrsMAY').AsBoolean := blRecurrsMai; // usw. q.Post; Result := True; except on E: Exception do begin Log('TRecurrencePattern.SaveToDatabase', M, E.Message); end; end; FreeAndNil(q); -Diese Prozedur ist wieder ein Paradebeispiel für (meine) schlampige Arbeit: nach dem q.Post fehlt das q.Close. Meine frühere Erwartung an TAdoQuery waren schlicht und ergreifend: Wenn ich es freigebe, wird es schon selbst für eine ordentliche Beendigung sorgen (.Close), und nicht einfach "das Messer in der Sau stecken lassen". Wenn ich das richtig verstehe, ist/könnte aber genau das sein, und ich muss dem TAdoQuery noch explizit hinterherräumen. Wenn man's weiß ist ja gut - wenn man mit einer falschen Erwartung dran geht... naja - woher soll man's wissen. Auch hier spielt wieder die "Angst" vor einer Exception eine große Rolle. Wenn ich das Programm jetzt wie folgt um schreiben würde:
Delphi-Quellcode:
Für FreeAndNil sollte es zunächst keinen Unterschied machen. Wenn es aber nun -aus welchen Gründen auch immer- vorkommen *könnte* dass die Erzeugung von q fehlschlägt, bzw. dieses im Laufe der Prozedur auf einen ungültigen Arbeitsspeicherbereich zeigt (jaja, Paranoia etc., aber spielen wir das Spiel mal zu Ende, bitte!) bzw. eine Exception zwischendrin ausgelöst wird, und finally nun angesteuert wird: q.Close würde/könnte hier eine *zusätzliche* Exception schmeißen (weil q ja ungültig oder der Close-Befehl "einfach so" eine Exception wirft), und dann wird FreeAndNil(q) ja gar nicht aufgerufen --> wirkliches MemoryLeak.
function TRecurrencePattern.SaveToDatabase(db: TAdoQuery): Boolean;
var i: integer; q: TADOQuery; begin Result := False; q := NIL; try try if not assigned(db) then begin Log('TRecurrencePattern.SaveToDatabase: db unassigned.'); Exit; end; q := TADOQuery.Create(Self); q.Connection := db; CloseQueryAndClearSQLText(q); // diese vier Zeilen sind in eigenen Prozeduren mit dortiger Fehlerbehandlung, nur zum Verständnis hier direkt q.SQL.Add('SELECT * FROM T_SerienMuster WHERE refTermin=:ID'); q.Parameters.ParamByName('ID').Value := _refTermin; q.Open; q.First; if q.Eof then q.Insert else q.Edit; q.FieldByName('refTermin').AsInteger := TerminID; q.FieldByName('blRecurrsJAN').AsBoolean := blRecurrsJanuar; q.FieldByName('blRecurrsFEB').AsBoolean := blRecurrsFebruar; q.FieldByName('blRecurrsMAR').AsBoolean := blRecurrsMaerz; q.FieldByName('blRecurrsAPR').AsBoolean := blRecurrsApril; q.FieldByName('blRecurrsMAY').AsBoolean := blRecurrsMai; // usw. q.Post; Result := True; finally q.Close; FreeAndNil(q); end; except on E: Exception do begin Log('TRecurrencePattern.SaveToDatabase', M, E.Message); end; end; -Wie setzt man sowas korrekt um? Wie habt Ihr das gelöst? -Ist
Delphi-Quellcode:
genau richtig, falsch, übertrieben, ...?
finally
try q.close; finally FreeAndNil(q); end; end; Danke im vorraus!
Delphi 10.4 32-Bit auf Windows 10 Pro 64-Bit, ehem. Delphi 2010 32-Bit auf Windows 10 Pro 64-Bit
|
![]() |
Registriert seit: 27. Nov 2017 2.592 Beiträge Delphi 7 Professional |
#3
Hilfe, soviele Fragen und so richtig kann ich das alles nicht beantworten
![]() Hier erstmal ein Stück Code aus 'nem Programm, das Ado nutzt und auf diverse Datenbanken zugreifen können sollen muss ![]()
Delphi-Quellcode:
Abhängig vom Datenbanktyp unterscheiden sich die Werte für CursorLocation und CursorType. Ohne diese Unterscheidung gibt es garantiert Fehler (zumindest auf meinen Systemen).
// Hier den Datenbanktypen abfragen und die Werte für Cursor ... setzen.
// CursorLocation bei SQLite muss wohl clUseClient sein, // bei Access aber clUseServer // Hier noch testen, was bei SQLite am sinnvollsten ist. if fDBIsSQLite then begin // ctUnspecified, ctOpenForwardOnly, ctKeyset, ctDynamic, ctStatic // Diese Variante spart Arbeitsspeicher und ist schnell. // Aber es gibt keine RecNo. // Änderungen von Datensätzen nicht möglich. // con.CursorLocation := clUseServer; // Dann gibt es keine RecNo :-( // qrySQL.CursorType := ctKeyset; // qrySQL.CursorType := ctStatic; // Benötigt viel Arbeitsspeicher, deutlich mehr, als die Datenbankgröße. // Ist sehr langsam. qrySQL.CursorLocation := clUseClient; qrySQL.CursorType := ctDynamic; // qrySQL.CursorType := ctStatic; // qrySQL.CursorType := ctKeyset; dbckOK.ValueChecked := 'T'; dbckOK.ValueUnchecked := 'F'; end else if fDBIsAccess then begin qrySQL.CursorLocation := clUseServer; qrySQL.CursorType := ctStatic; dbckOK.ValueChecked := 'Wahr'; dbckOK.ValueUnchecked := 'Falsch'; end else if fDBIsFirebird then begin qrySQL.CursorLocation := clUseClient; qrySQL.CursorType := ctDynamic; dbckOK.ValueChecked := '1'; dbckOK.ValueUnchecked := '0'; end else begin // Ansonsten? qrySQL.CursorLocation := clUseClient; qrySQL.CursorType := ctDynamic; dbckOK.ValueChecked := '1'; dbckOK.ValueUnchecked := '0'; end; Da das Ganze schon ein paar Jahre alt ist, weiß ich nicht mehr so genau, wie ich zu welchen Werten kam, aber seit dem scheint es rund zu laufen. Mein Vorgehen wäre von daher zuerst mal mit CursorLocation und CursorType zu experimentieren, bis Dein Testprogramm, das Du zweimal gestartet hast, keinen Fehler mehr wirft oder Du garantiert alle Kombinationen der beiden Eigenschaften "durchhast" und weißt, dass sie alle zu Fehlern führen. Mit dieser Funktion baue ich mir aus meinen Programmen heraus den Connectionstring auf (sofern wahlweise auf unterschiedliche Datenbanken zugegriffen werden soll).
Delphi-Quellcode:
Was kommt bei Deinem Testprogramm dabei raus? Stimmt das mit dem von Dir selbst erstellten Connectionstring überein oder mit dem vom Assistenten generierten? Bei meinen System stimmt es eher mit Deinem (im vorherigen Post) selbst erstellten Connectionstring überein.
function fnGetDataSourceString(Handle : THandle;
ADOConnection : TADOConnection; sConnection : String = '') : Boolean; begin ADOConnection.Connected := False; ADOConnection.ConnectionString := PromptDataSource(handle,sConnection); ADOConnection.Connected := True; Result := ADOConnection.Connected; end; Hier ein Beispiel:
Code:
Querys werden bei mir nach Gebrauch sofort per Close geschlossen. Allerdings erstelle ich sie nicht dynamisch, sondern hab' sie gewöhnlich auf 'nem TDataModule, auch wenn es da durchaus mal eine ganze Reihe für unterschiedliche Zwecke geben kann.
Provider=Microsoft.Jet.OLEDB.4.0;User ID=Admin;Data Source=C:\Support\TipVerwaltung.mdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database="";Jet OLEDB:Registry Path="";Jet OLEDB:Database Password="";Jet OLEDB:Engine Type=5;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:New Database Password="";Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False
Zur Reihenfolge: Weise bitte zuerst den Connectionstring zu und ändere erst dann (soweit erforderlich) die Eigenschaften der ADOConnection. Ich gehe mal davon aus, dass die von Dir gesetzten Werte beim Setzen des Connetionstring "überschrieben" werden. Machst Du die Änderungen nach der Zuweisung des Connectionstrings, werden "seine" Werte durch Deine überschrieben. Damit wärst Du dann quasi der Gewinner. So, wie Du es momentan zu machen scheinst, gewinnt jedoch der Connectionstring. Als erste Idee (ohne Garantie für irgendwas):
Delphi-Quellcode:
Damit scheinen bei mir mehrere Instanzen eines Programmes problemlos mit einer Accessdatenbank arbeiten zu können.
db.Close;
db.ConnectionString := 'Provider=Microsoft.Jet.OLEDB.4.0;' + 'User ID=Admin;' + 'Data Source=' + _FileName + ';' + 'Mode=Share Deny None;' + 'Jet OLEDB:System database="";' + 'Jet OLEDB:Registry Path="";' + 'Jet OLEDB:Database Password="";' + 'Jet OLEDB:Engine Type=5;' + 'Jet OLEDB:Database Locking Mode=1;' + 'Jet OLEDB:Global Partial Bulk Ops=2;' + 'Jet OLEDB:Global Bulk Transactions=1;' + 'Jet OLEDB:New Database Password="";' + 'Jet OLEDB:Create System Database=False;' + 'Jet OLEDB:Encrypt Database=False;' + 'Jet OLEDB:Don''t Copy Locale on Compact=False;' + 'Jet OLEDB:Compact Without Replica Repair=False;' + 'Jet OLEDB:SFP=False;' + 'Persist Security Info=False;'; db.KeepConnection := True; // HAL_Registry_GetBool('KeepConnection', True); db.LoginPrompt := False; db.CursorLocation := clUseServer; db.CursorType := ctStatic; db.IsolationLevel := ilReadCommitted; db.Mode := cmReadWrite; c1 := db.ConnectionString; db.Open; c2 := db.ConnectionString; |
![]() |
Registriert seit: 3. Sep 2004 441 Beiträge Delphi 10.4 Sydney |
#4
Vielen Dank für die detaillierte Antwort.
Ja, bei mir kommt auch der lange ConnectionString raus, in sofern verwende ich den jetzt auch. Ich habe all meine Quelltextdatei überarbeitet: -Es wird nun häufiger eine über Parameter übergebene TAdoConnection verwendet, anstatt selbst (temporär) eine neue zu Erstellen -Überall wo eine temporäre AdoConnection erstellt wird, gebe ich paranoia-mäßig alles mit try..finally frei
Delphi-Quellcode:
Was mich mit deinem Post jetzt aufgeschreckt hat:
finally
CloseQueryAndClearSQLText(q); end; finally db.Close; end; finally FreeAndNil(q); FreeAndNil(db); end; Bisher dachte ich, dass cmReadWrite letztendlich doch "Einzel-Benutzung" bedeutet, da das Wort "Share" fehlt. Ich verwende bis dato (imo problemlos?) immer cmShareDenyNone - "Alle Teilen, nichts verbieten". So wurde das glaube ich auch überall empfohlen und in den Beispielen verwendet. Ich bin eben fast vom Hocker gefallen, als ich in der OH nachgeschlagen habe:
Code:
cmShareDeny... : Der Benutzer kann alles, ausser Read-Lesen, Write-Schreiben, None-Nichts --> Der Benutzer kann alles, mit Ausnahme von nichts - ohne Ausnahme, siehe auch:
cmReadWrite - Die Verbindung verfügt über Lese-/Schreibrechte.
cmShareDenyRead - Die Verbindung kann von anderen Benutzern nicht mit Leseberechtigung geöffnet werden. cmShareDenyWrite - Die Verbindung kann von anderen Benutzern nicht mit Schreibberechtigung geöffnet werden. cmShareDenyNone - Die Verbindung kann mit keiner Art von Zugriffsrecht von anderen Benutzern geöffnet werden. ![]() ![]() nein, MSAccess ist auch für einen Mehrbenutzermodus vorgesehen
der Verbindungsstring wird normalerweise mit den Zugangsberechtigungen "share deny none' also jeder kann gleichzeitig zugreifen s.a. Connection.Mode cmShareDenyNone ![]() Hier bräuchte ich jetzt bitte echt schnell nochmal Rückmeldung: Was Unterscheidet cmShareDenyNone von cmReadWrite? Ist cmReadWrite wirklich für Mehrbenutzer-Zugriff für Access-Datenbanken über Netzwerk geeignet? Können dabei (z.B. nach einem Update) [einmalig] Tabellen angepasst werden (Spalten hinzufügen, ...), oder schlagen irgendwelche Befehle fehl?
Delphi 10.4 32-Bit auf Windows 10 Pro 64-Bit, ehem. Delphi 2010 32-Bit auf Windows 10 Pro 64-Bit
Geändert von berens ( 8. Okt 2020 um 11:19 Uhr) |
![]() |
Registriert seit: 27. Nov 2017 2.592 Beiträge Delphi 7 Professional |
#5
Ehrlich gesagt: Keine Ahnung.
Habe seinerzeit rumprobiert, bis ich zu dem Schluss kam, dass die oben von mir gepostete Variante (für meine Begriffe) ordentlich funktioniert. Die Formulierung
Code:
klingt für mich so: Die Anderen dürfen nix.
cmShareDenyNone - Die Verbindung kann mit keiner Art von Zugriffsrecht von anderen Benutzern geöffnet werden.
Aber: Man kann die Formulierung durchaus auch anders interpretieren. Für mich klingt das so: Egal mit welchen Rechten es die anderen versuchen, sie werden scheitern. Aber man kann es auch so interpretieren: Egal welche Rechte die Anderen nutzen, sie können zugreifen. Bei dieser Art von Hilfe bin ich immer hilflos, denn die Aussage entspricht in etwa einem "Entschiedenen sowoh als auch" (oder so ähnlich). Viele AdoConnection = viele Handles, viele Querys = viele Handles. Was ich nicht weiß, inwieweit die Handles beim Freigeben der AdoConnections bzw. der Querys sofort "verschwinden". Von daher ist mein Vorgehen immer so: Alles, was ich brauche, wird bereits in der Entwicklungsumgebung auf's Formular ... gepappt. Dabei gilt: Von allem so wenig wie möglich. Mehrere AdoConnetions nur, wenn auch mehrere Datenbanken genutzt werden. Pro DB nur eine Connection. Querys: Eine für Abfragen, eine für Insert, Update, Delete ... (also den Rest) (ggfls. pro DB). Pro DBGrid, DBCtrlGrid, DataSource (o. ä.) (sofern welche zum Einsatz kommen) eine Query. Connections verbinden sich beim Programmstart mit der Datenbank und beenden die Verbindung beim Programmende. Auch hier gilt: So wenig wie möglich. Ob dieses Vorgehen nun den Regeln der Kunst entspricht, mit irgendwelchen Vorgehensmodellen, ... konform geht, keine Ahnung. Fahre mit diesem Vorgehen seit fast 2 Jahrzehnten gut und solange läuft auch schon ein Teil meiner Software ohne Mucken. Geändert von Delphi.Narium ( 8. Okt 2020 um 11:42 Uhr) Grund: Schreibfehler |
![]() |
Registriert seit: 9. Apr 2006 1.684 Beiträge Delphi 5 Professional |
#6
Vielleicht lohnt sich ein Blick auf die
![]()
Sorry, wenn der Beitrag nicht (sonderlich) hilfreich gewesen sein sollte. Grüße Dalai |
||||||||||||||||||
![]() |
Ansicht |
![]() |
![]() |
![]() |
ForumregelnEs ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.
BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus. Trackbacks are an
Pingbacks are an
Refbacks are aus
|
|
Nützliche Links |
Heutige Beiträge |
Sitemap |
Suchen |
Code-Library |
Wer ist online |
Alle Foren als gelesen markieren |
Gehe zu... |
LinkBack |
![]() |
![]() |