foreign key ado syntax
|
|
Thread rating:  |
MP - 07 Aug 2008 03:55 GMT I have a db with a string field "dwgname" as many records will share a common value i'm considering "normalizing" that field to it's own table then instead of the string "dwgname" I'll store the foreign key of the "dwgNameTable"
I create fields via adox using the following(wrapped in a function in a class) Dim oField as Field .... Set oField = New cField: oField.Name = "fldDwgName": oField.DataType = adVarWChar
so if i store a key, that will just be a long? right? so i'd just change the datatype to adInteger???
or do I do something different with a field that is to receive foreign key??/ for example to create a primary key in the table i do... Set oField = New cField: oField.Name = "fldIDPrimary": oField.DataType = adInteger: oField.IsPrimary = True: oField.IsNullable = False: oField.AutoIncrement = True
so does the receiving field need special attributes? or is it just set up to receive a long value? Set oField = New cField: oField.Name = "fldDwgNameKey": oField.DataType = adInteger
and in the context of why its' good to normalize, I'm still storing duplicate values, it's just that the value is a long instead of a string and therefore saves memory???
you normalize to avoid duplicate data(i think...among other reasons i guess), but i'm still storing the same number of duplicate datas, just a different data type, right???
thanks mark
MP - 07 Aug 2008 04:31 GMT >I have a db with a string field "dwgname" > as many records will share a common value i'm considering "normalizing" > that field to it's own table > then instead of the string "dwgname" I'll store the foreign key of the > "dwgNameTable" found it Set oKey = New ADOX.key With oKey .Type = adKeyForeign
now to figure out how to tell it what table's id to use...
MP - 07 Aug 2008 05:20 GMT >>I have a db with a string field "dwgname" >> as many records will share a common value i'm considering "normalizing" [quoted text clipped - 8 lines] > > now to figure out how to tell it what table's id to use... duh! how about .RelatedTable
:-) I forgot I was using a cFields class to wrap the ado.Field object and give some other properties, sorry for the confusion
this might work, added foreign key option to my create table function in cDatabaseController cls it looks bad cause i have to loop through fields collection twice, since i have to add a field to a table, and append the table to the catalog before setting some properties of the field(key) but i don't know if there is a way around that duplication?
also i'm not sure if i should add a .DeleteRule adRICascade 1 Cascade changes.
adRINone 0 Default. No action is taken.
so if in the "foreign" table, if an entry were deleted(which I don't expect in my case) then any entries in the "main" table that have that "fk" would be deleted? if the setting were adRiCascade...is that right? and if adRiNone , they would remain but would contain 'invalid' data in that field, right?
Public Sub CreateTable(TableName As String, FieldObjectsCollection As Collection) On Error GoTo CreateTable_Error Dim ADOXtable As Table Dim oField As cField Dim oKey As ADOX.key Set ADOXtable = New ADOX.Table ADOXtable.Name = TableName
Set ADOXtable.ParentCatalog = moCatalog With ADOXtable 'Add Columns to the table For Each oField In FieldObjectsCollection .Columns.Append oField.Name, oField.DataType If oField.AutoIncrement Then .Columns(oField.Name).Properties("AutoIncrement") = True End If If oField.IsNullable Then .Columns(oField.Name).Properties("Nullable") = True End If Next oField End With 'table
'append tables to database before setting keys moCatalog.Tables.Append ADOXtable
For Each oField In FieldObjectsCollection 'could add boolean to ensure only one primary key, but if i try to add more, i expect i'd get an error here anyway? If oField.IsPrimary Then Set oKey = New ADOX.key With oKey .Name = "PrimaryKey" .Type = adKeyPrimary .RelatedTable = TableName .Columns.Append oField.Name End With ADOXtable.Keys.Append oKey End If
'can have multiple fks If oField.IsForeignKey Then Set oKey = New ADOX.key With oKey .Name = "fk_" & oField.RelatedTableName .Type = adKeyForeign .RelatedTable = oField.RelatedTableName .Columns.Append oField.Name End With
ADOXtable.Keys.Append oKey End If Next oField 'Clean up Set ADOXtable = Nothing msDbTableName = TableName
ExitHere: On Error GoTo 0 Exit Sub
CreateTable_Error: LogError ProcName End Sub
MP - 12 Aug 2008 13:17 GMT I'm still not getting it to work :-( per the help on ado on dev site <quote under the section on create table syntax on setting constraints: ...or if the field in the foreign table is the primary key, you only need name the table and the database engine references it by default:
CREATE TABLE Sales (SalesID INTEGER, ProductID INTEGER CONSTRAINT ForeignKeyRef REFERENCES Products, Item TEXT); end quote>
so with adox I'm trying to create a key (which is how i understand on e creates constraints if not using sql "Create Table" statement and rather building a table via adox.
here's what i'm trying LogEntry "Create foreign key" 'Create foreign key constraint on field in main table 'oField is instance of cField class holding name, datatype, and properties of a Field object to be created Set oKey = New ADOX.key With oKey .Name = "fk_" & oField.RelatedTableName .Type = adKeyForeign <------------------------- i assumed this was equiv to "CONSTRAINT ForeignKeyRef " .RelatedTable = oField.RelatedTableName <----- i assumed this was equiv to "REFERENCES TableName" .Columns.Append oField.Name End With
ADOXtable.Keys.Append oKey <--------------- this is the line throwing error
the error i'm getting: <-2147217903> <Microsoft JET Database Engine> (Column ID is invalid.)
anyone see what i'm doing wrong? thanks mark
Schmidt - 12 Aug 2008 18:18 GMT > I'm still not getting it to work :-( Don't know, why you are trying to tell the JET-engine, that a special Field in your Main-Table is a Foreign-Key-Field at all. That would only be necessary, if you want to use some special features of the DB-engine itself (e.g. cascaded deletes, which I read you don't need in your scenario) ... or if you want to use it inside the Access.exe-GUI, to let some special Access-GUI-Controls behave properly (e.g. in the Access-Grid-Control you would get automagic filling of a combo- box for your foreign-key-field, not showing the adInteger-Keys, but the "clear-text" instead in the DropDown.
But if this is for usage in VB6, then you normally don't need "extended definitions" for foreign-keys in your table-design. In my DB-Schemas I simply follow a Field- Naming-Convention: Each Table always gets an ID-Field as its first Field. If the ID-Field of some table is used inside other tables, I simply name it appropriately with SomeTableName_ID.
Simply put a normal Integer-Field into your Main-Table, and handle the ID-referencing of the associated table in your class-wrappers manually. Should only be a few lines of code. If you follow the above mentioned naming- convention, then you could even parse automatically for Fields with the Suffix '_ID', split the Foreign-Table-Name-Prefix from such a FieldName - and handle everything generically in your Object-Model.
BTW - why ADO (I assume with JET) ... since you are a user of dhRichClient.dll, why not using the builtin SQLite-Wrapper - you'd get some more "degrees of freedom" this way (smaller, faster, InMemory-DBs, definable triggers, e.g. for selfdefined cascaded deletes, which don't need special Table-Defines or Field-Attributes, to work properly).
Olaf
MP - 12 Aug 2008 19:16 GMT >> I'm still not getting it to work :-( > Don't know, why you are trying to tell the > JET-engine, that a special Field in your > Main-Table is a Foreign-Key-Field at all. because i'm a total newbie to databases :-) I just read the help files and these ngs to try to learn I saw the foreign key attribute and assumed I needed to use that.
> That would only be necessary, if you want > to use some special features of the DB-engine > itself (e.g. cascaded deletes, which I read you > don't need in your scenario) haven't figured out if or why I'd need the cascade function...pretty sure in this tiny app I don't and won't in the future.
... or if you want
> to use it inside the Access.exe-GUI no access to access :-)
> But if this is for usage in VB6, then you normally > don't need "extended definitions" for foreign-keys > in your table-design. great, one less thing to worry about :-)
> In my DB-Schemas I simply follow a Field- > Naming-Convention: [quoted text clipped - 23 lines] > which don't need special Table-Defines or > Field-Attributes, to work properly). well, I've watched with interest over the last year or so your conversations with rb smissert re sqlite I've saved all that for future reference for when I have time to learn how to use sqllite. at this point it's one more learning curve i've been putting off...since I just barely am getting the hang of ado/adox (creating tables, fields, getting values, inserting values etc)
but maybe you'll convince me it's not as hard as I fear
:-) and on the general topic of foreign keys...this whole issue came up just because I realize i'm not normalizing my single table database. its' probably not required at all for this tiny app I'm trying to build but I just didn't realize how hard it would be to learn how to do and from what I'd read on normalization I thought it was the "right" thing to do ;-)
I have a field that will contain duplicate strings (fldDwgName) so i figured it was better to have a filed that contained duplicate longs(FK_primarykey from 'foreign table') (fldDwgName_ID)
that's my (mis)understanding of the theory of why to "normalize" <g>
If I ever succeed in getting a foreignkey field created (either with ado/jet or sqllite) I don't even know how to use it exactly...
I think when making an entry I'll have to read the "foreign table", find the string value matching my current data, find the id for that string value, then put that id into the "fk_field" in the main table.... is that right?
seems like more work than what I'm doing now, just putting the string value in my main table, but I'm trying to learn the "right" way to do these things.
:-) any pointers appreciated mark
Schmidt - 14 Aug 2008 16:40 GMT > >> I'm still not getting it to work :-( > > Don't know, why you are trying to tell the [quoted text clipped - 6 lines] > I saw the foreign key attribute and assumed I > needed to use that. As said, only if you need special DB-Features, which are based on these "Extra-Definitions".
> > That would only be necessary, if you want > > to use some special features of the DB-engine [quoted text clipped - 4 lines] > cascade function...pretty sure in > this tiny app I don't and won't in the future. As said, in other DB-Systems (e.g. MS-SQL- Server or Postgres or even SQLite you can define appropriate Triggers for that purpose.
> > But if this is for usage in VB6, then you normally > > don't need "extended definitions" for foreign-keys [quoted text clipped - 14 lines] > but maybe you'll convince me it's not as hard as I fear > :-) To learn ADO, and how to work with Cnn-, Command- and Recordset-Objects is not a bad idea - it is a well established DB-abstraction-layer, but if you know ADO, you will have no problems with my SQLite-Wrapper, it is not completely compatible, but behaves very similar.
> and on the general topic of foreign keys...this whole > issue came up just because I realize i'm not normalizing > my single table database. its' probably not required at all > for this tiny app I'm trying to build but I just didn't realize > how hard it would be to learn how to do and from what I'd > read on normalization I thought it was the "right" thing to do ;-) Normalization is usually a good idea - it requires planning and makes some additional work - but it worth the effort regarding future maintenance and enhancements on the DB- Schema-Layout. Though sometimes it affects performance - but only on huge Databases - and if your Indexes are not placed properly. Finally (over-)normalized DBs behave a bit like "too overdesigned OOP-models" - so one has to find a good balance between "strict flatness" and "overboarding hierarchy-levels" - strictly following "the rules" can lead to the same mess like over-laziness.
> I have a field that will contain duplicate strings (fldDwgName) > so i figured it was better to have a filed that contained duplicate > longs(FK_primarykey from 'foreign table') (fldDwgName_ID) > > that's my (mis)understanding of the theory of why to "normalize" <g> If that is a misunderstanding, depends (as always ...<g>). E.g., if in your case for example a file will be renamed, then you would only have to reflect this name-change in your separated FileName-Table (under the same old ID). That's all - in your MainTable (which uses only the IDs), this new, changed Name would be automatically "reflected". If you want to avoid exactly that (e.g. if you want to store the FileName-Data in your Main-Table, for archive-reasons), then you should rethink about your Table-Splitting again (or implement another solution, which is based on the "normalized approach", but never allows renaming on once given FileName- IDs, but would require a new record with a new FileName-ID for Namechanges of already stored Files).
> If I ever succeed in getting a foreignkey field created > (either with ado/jet or sqllite) I don't even know how > to use it exactly... Maybe a better suited example is easier to understand..., e.g. two (normalized) tables 'Persons' and 'Addresses' (somewhat simplified). Persons with: ID, Name Addresses with: ID, ZipCode, Street, Persons_ID
Persons_ID is the (Integer) Foreign-Key-Field, pointing to table 'Persons' - and this allows us, to have more than one Address-Record for each Person (beside the fact, that tables which identify a Person or a "User" will also be of use in many more tables than only Addresses in a typical DB- Design).
Anyway, assuming you get a call from "Some Person", that this is now the third advertising-letter he receives from your side where his name was misspelled, you will only have to change the name in one record and in one single table -> 'Persons'.
Will try to put a small example together, based on the above, using the SQLite-Wrapper in dhRichClient (including a Trigger, which deletes automatically from table 'Addresses' - as soon as a Record from 'Persons' is deleted - just to demonstrate a "cascaded delete" the SQLite-way).
Olaf
MP - 14 Aug 2008 20:18 GMT >> >> I'm still not getting it to work :-( >> > Don't know, why you are trying to tell the [quoted text clipped - 8 lines] > As said, only if you need special DB-Features, > which are based on these "Extra-Definitions". thanks
>> well, I've watched with interest over the last year >> or so your conversations >> with rb smissert re sqlite >> I've saved all that for future reference for when I have >> time to learn how to use sqllite. well, I *thought* i'd saved them...but cant find now...will have to search the ng arcives to get that trove...<g>
> To learn ADO, and how to work with Cnn-, Command- > and Recordset-Objects is not a bad idea - it is a well > established DB-abstraction-layer, but if you know ADO, > you will have no problems with my SQLite-Wrapper, > it is not completely compatible, but behaves very similar. great, I've started reading your code in the dhSqllite demo project
Finally (over-)normalized DBs behave a
> bit like "too overdesigned OOP-models" - so one has to find > a good balance between "strict flatness" and "overboarding > hierarchy-levels" - strictly following "the rules" can lead to > the same mess like over-laziness. i'm thinking I don't realy need to do the splitting now this db is just being used as a temporary storing/ sorting tool
> If that is a misunderstanding, depends (as always ...<g>). > E.g., if in your case for example a file will be renamed, then > you would only have to reflect this name-change in your > separated FileName-Table (under the same old ID). makes sense, in my case that won't happen so I probably don't need to split
after storing/sorting/assigning id-code to entries, i write the new data (computed id-codes) back to the source dwgs:
Call ReadDatabaseIntoBlockDataObjects(mcolBlockDataObjects) 'which is basically Set oRs = oConn.Execute ( "Select * FROM " & sTableName & " ORDER BY fldDwgName") Call ReadRecordsetWriteBlockDataObjects(oRs, mcolBlockDataObjects)
then those objects put data back into the appropriate source files (.dwg files) For Each oBdo In mcolBlockDataObjects DwgFullName = oBdo.DwgName If Len(LastDwgFullName) > 0 Then 'not in first dwg, save, close lastdwg and open next dwg If Not DwgFullName = LastDwgFullName Then 'store name for next check LastDwgFullName = DwgFullName 'save current dwg Save 'close current dwg Close 'open next dwg in list Open DwgFullName Else 'already in that dwg End If Else 'in first dwg 'store name for next check LastDwgFullName = DwgFullName 'open first dwg in list Open DwgFullName End If 'in first dwg 'enter data into file ... Next oBdo
I see the point in your example of why to normailze, thanks
> Anyway, assuming you get a call from "Some Person", > that this is now the third advertising-letter he receives [quoted text clipped - 10 lines] > > Olaf appreciate the offer, Id be interested to see how to read and write to a record when using a FK eg, maybe the person is already in the Persons table or maybe not. I assume there has to be some way to put the first occurrance into the table and retrieve the index to use as FK, then with each successive entry, grab the idex for the existing person out of the table and use that as FK for the main table entry something like... fk_ID_Person = (Select fld_ID_Person From tblPersons Where Exists (Select * From tblPersons Where fldPersonName = "thisname") ).Value
then if "thisname" is not on list (Select fld_ID_Person From tblPersons Where Exists (Select ...)).Value either would error or return 0 is what I'm guessing??? and I'd have to make an entry for that person and get the last iD...???
is that close?
thanks mark
Schmidt - 14 Aug 2008 20:49 GMT > > To learn ADO, and how to work with Cnn-, Command- > > and Recordset-Objects is not a bad idea - it is a well [quoted text clipped - 3 lines] > > great, I've started reading your code in the dhSqllite demo project Yep, if you compare for example the two VB-Forms fTestNWindADO and fTestNwinddhSQLite, you will find both of them very similar code-wise.
> Finally (over-)normalized DBs behave a > > bit like "too overdesigned OOP-models" - so one has to find [quoted text clipped - 4 lines] > i'm thinking I don't realy need to do the splitting now > this db is just being used as a temporary storing/ sorting tool Especially if you want to achieve a good performance, then the "direct-saving" is probably better in your case (though that would need more memory on disk of course, dependent on how many duplicate filenames you have in your scenario, an Integer takes up only four Bytes in the DB).
> ... Id be interested to see how to read and write to a > record when using a FK - eg, maybe the person is already > in the Persons table or maybe not... Ok, will try to address that special case too with an enhanced example (based on the simple one I've just posted).
Olaf
Schmidt - 15 Aug 2008 18:57 GMT > > ... Id be interested to see how to read and write to a > > record when using a FK - eg, maybe the person is already > > in the Persons table or maybe not... > Ok, will try to address that special case too with > an enhanced example ... Here's the Download-Link: www.datenhaus.de/Downloads/ForeignKeyFileScan.zip
Have tried, to match a bit with your Filescan-Scenario. The Demo uses two Classes (one for ADO/JET and one for SQLite), which both implement an iFileScan- Interface, which was defined in an "abstract class" in the same project.
Tried my best, to implement both Classes with the fastest known methods (using Command-Objects for the inserts) - anyway, the ADO/JET approach is ca. factor 15 away from the SQLite-implementation.
Both versions currently operate directly against the DB, so in principle should produce consistent results even in multiuser-mode.
If you always create new DBs for your FileScans and/or are the only user working on this DB, then you could speedup both versions, if you "buffer" the FileName- ExistsInDB-check with a Collection or the SortedDictionary.
Olaf
Schmidt - 16 Aug 2008 08:51 GMT > www.datenhaus.de/Downloads/ForeignKeyFileScan.zip > ... > ... you could speedup both versions, if you "buffer" the FileName- > ExistsInDB-check with a Collection or the SortedDictionary. Just enhanced the small Demo-Code above with the suggested Dictionary-Lookup (which can be switched inside the Class-Implementations for ADO/JET and SQLite over a constant now, between Dict-Filename- Lookups and the slower Rs-FileName-Lookups) - and that change boosts both implementations. The SQLite-Version profites the most from this change and so you have now a factor 40-advantage in this scenario compared with ADO/JET.
The new code is downloadable under the same link.
Olaf
MP - 18 Aug 2008 16:55 GMT Fantastic, Thanks Olaf, will dl that and study I appreciate very much your taking time to help. Mark
>> www.datenhaus.de/Downloads/ForeignKeyFileScan.zip >> ... [quoted text clipped - 14 lines] > > Olaf MP - 18 Aug 2008 18:02 GMT Olaf, Just read through your sample. that is a thing of beauty! Thank you again! Mark
> Fantastic, > Thanks Olaf, will dl that and study [quoted text clipped - 19 lines] >> >> Olaf Schmidt - 14 Aug 2008 20:35 GMT [simple Persons, Addresses scenario, to demonstrate Foreign-Keys]
> Will try to put a small example together, based on > the above, using the SQLite-Wrapper in dhRichClient > (including a Trigger, which deletes automatically from > table 'Addresses' - as soon as a Record from 'Persons' > is deleted - just to demonstrate a "cascaded delete" the > SQLite-way). Ok, for those who are interested:
'***Into a Form (Project needs a reference to dhRichClient) Option Explicit
Private Cnn As cConnection
Private Sub Form_Load() Set Cnn = New cConnection Cnn.CreateNewDB 'without FileName opens a new InMemory-DB
CreateSchema 'here we define two tables, indexes and a trigger End Sub
Private Sub Form_Click() 'first two simple dumps, to check the content of our two tables DumpRsContent Cnn.OpenRecordset("Select * from Persons") DumpRsContent Cnn.OpenRecordset("Select * from Addresses")
'now a simple Join, to "integrate" the "cleartext" of our foreign table DumpRsContent Cnn.OpenRecordset("Select Addresses.*, Name From " & _ "Addresses Inner Join Persons " & _ " On Persons_ID = Persons.ID")
'and finally a demonstration, that the defined trigger works Cnn.Execute "Delete From Persons Where ID > 2" Debug.Print "After a cascaded Delete on Table Persons, let's check..." DumpRsContent Cnn.OpenRecordset("Select * from Addresses") End Sub
Private Sub DumpRsContent(Rs As cRecordset) Dim Fld As cField Debug.Print "Content of: "; Rs.SQL 'the header For Each Fld In Rs.Fields Debug.Print Fld.Name, Next Fld Debug.Print vbCrLf; String(128, "-")
'and the content Do Until Rs.EOF For Each Fld In Rs.Fields Debug.Print Fld.Value, Next Fld Debug.Print Rs.MoveNext Loop Debug.Print End Sub
Private Sub CreateSchema() 'Create a simple Persons-Table Cnn.Execute "Create Table Persons(ID Integer Primary Key," & _ " Name Text)" 'to speedup caseinsensitive "reverse search" per Name on Persons-Table Cnn.Execute "Create Index Idx_Persons_Name On " & _ "Persons(Name Collate NoCase)"
'now the Addresses-Table, which points to table Persons with Persons_ID Cnn.Execute "Create Table Addresses(ID Integer Primary Key," & _ " ZipCode Text," & _ " City Text," & _ " Street Text," & _ " Persons_ID Integer)" 'to speedup joining with the Persons-Table over Persons_ID Cnn.Execute "Create Index Idx_Addresses_Persons_ID On " & _ "Addresses(Persons_ID)"
'now we create a trigger for cascaded deletes Cnn.Execute "CREATE TRIGGER Delete_On_Addresses " & _ "BEFORE DELETE ON Persons " & _ "FOR EACH ROW BEGIN " & _ "DELETE from Addresses WHERE Persons_ID = OLD.ID; " & _ "END;"
'and finally we fill in some Demo-Data FillDemoData End Sub
Private Sub FillDemoData() Dim i&, j&, Rs As cRecordset 'fill in some Persons Set Rs = Cnn.OpenRecordset("Select * from Persons") For i = 1 To 4 Rs.AddNew Rs.Fields("Name").Value = "Person " & i Next i Rs.UpdateBatch
Set Rs = Cnn.OpenRecordset("Select * from Addresses") For i = 1 To 8 If (i Mod 2) Then j = j + 1 'ensure, each Person has a secondary residence ;-) End If Rs.AddNew Rs.Fields("ZipCode").Value = "Zip " & i Rs.Fields("City").Value = "City " & i Rs.Fields("Street").Value = "Street " & i Rs.Fields("Persons_ID").Value = j Next i Rs.UpdateBatch End Sub
Olaf
MP - 14 Aug 2008 20:54 GMT Thanks, will study that Mark
> [simple Persons, Addresses scenario, to demonstrate > Foreign-Keys] > > Olaf MP - 22 Aug 2008 02:37 GMT Hi Olaf,
> [simple Persons, Addresses scenario, to demonstrate > Foreign-Keys] [quoted text clipped - 4 lines] > > Private Cnn As cConnection Great, thanks for the sample
ok, now you got me all excited about learning sqlite and replacing my dependence on ado/adox/mdb
I have a project where i've been using some classes that wrap ado stuff so I wanted to make a drop in replacement with minimal re-writing so I could switch from ado to sqlite to test differences Like in your sample, where you use IFileScan interface...with an ado version and an sqlite version
I previously had cDatabaseController class with all the functions i needed to create dbs, tables, execute sql, retrieve recordsets etc...which wrapped ado/adox
Now I added IDatabaseController (based on your IFileScan and adding calls I had in cDatabaseController) changed cDatabaseController to Implement IDatabaseController Then I adapted your cFileScanSqllite.cls to cDatabaseControllerSqlite and had it also implement IDatabaseController so in theory I can use either one... here I have a couple questions...
I previously had a method .ExecuteSql(ssql as string)as Recordset <that called adodb.Connection.Execute (which returns a recordset or not depending on the sql statement)>
I notice your cConnection.Execute doesn't return a recordset... you'd use .OpenRecordset for that... but .OpenRecordset returns a cRecordset, not an adodb.Recordset (naturally <g>)
So I'm thinking how to rewrite all my client calls from other classes to cDatabaseController.ExecuteSql that expect a Recordset...
I guess I just need to rewrite all of that so it can either deal with a Recordset or a cRecordset interchangably (as long as all my calls to the oRs are shared by both Recordset and cRecordset) (or create an "adapter pattern" of some kind to interface with either a recordset or a cRecordset)
then redefine IDatabaseController.ExecuteSql to return an Object instead of an adodb.Recordset ???
then internally cDatabaseController.ExecuteSql can call IDatabaseController_ExecuteSql and cast the return to a Recordset and internally cDatabaseControllerSqlite.ExecuteSql can call .OpenRecordset and return a cRecordset
??? is that more or less the way it would be handled???
is this too confusing???
or is there any way to make an Adapter pattern that would not require rewriting all the calls to different implementations (ado vs sqlite) that provided different object types as returns ??? ...(thinking out loud here) <g> create a cMultiRecodsetObject give it a .Return object property dim oRs as cMultiRecodsetObject If type of oRs.Return is cRecordset then ElseIf type of oRs.Return is adodb.Recordset then ...???
thanks for any light you can shine on my confusion
:-) mark
Schmidt - 22 Aug 2008 12:41 GMT > I have a project where i've been using some classes that > wrap ado stuff so I wanted to make a drop in replacement > with minimal re-writing so I could switch from ado to sqlite > to test differences... I would define a small IRecordset-Interface, which includes the Rs-Methods and -Properties you want to use in your App.
.EOF, .BOF .MoveFirst, .MoveNext, .MoveLast .AddNew .UpdateBatch including some Properties/Methods, which allow you to work with the Field-Names and Field-Values, without exposing the Field(s)-Object, to keep things simple inside the IRecordset-Implementation (something in analogy to VBs Fonts, which you can either address with Font.Name, Font.Size or more directly one ObjLevel above with .FontName, .FontSize).
so your IRecordset should also contain: .FieldsCount() as Long .FieldValue(KeyOrIndex As Variant) As Variant .FieldName(KeyOrIndex As Variant) As Variant
this way you can avoid, exposing the Field- Object-stuff and writing additional IFields and IField-Interfaces + Wrappers.
Then I would split your combined Method (.ExecuteSQL ) in your current Database-Wrapper-Object into two Methods - one for the DDL-stuff and the Write-Direction against the DB, called .Execute (returning only a Long with the affected Rows-Count if you want) and another one for the Read-Direction, addressing the "Selects", returning an IRecordset (maybe called OpenRecordset, like in my SQLite-Wrapper). That'd make things more "obvious" in your Client-Code and adds only one Method.
Depending on ADOJet or SQLite, the OpenRecordset- Implementations would look something like this:
inside cADOJETDatabase (implementing IDataBase): Private mCnn as Connection 'the internal ADO-Cnn
Private Function IDataBase_OpenRecordset(SQL as String) as IRecordset Dim Rs as cADOJetRecordset 'instantiate the implementation of IRecordset Set Rs = New cADOJetRecordset
'now call a Friend-Method on cADOJetRecordset Rs.Initialize SQL, mCnn
Set OpenRecordset = Rs 'and finally cast and return End Function
inside the IRecordset-Implementation cADOJetRecordset: (additionaly to the Implementation of IRecordset, which should be trivial, the Friend-Method on the Default- Interface of this Class should be something like...) Private mRs as Recordset 'the internal ADO-Rs
Friend Sub Initialize(SQL as String, Cnn as Connection) Set mRs = New Recordset mRs.Open SQL, Cnn, adOpenStatic, adLockBatchOptimistic End Sub
And in analogy you would have to code the two SQLite- Implementations for IDatabase and IRecordset, using the appropriate internal Objects (cConnection and cRecordset of course).
HTH, and let me know, how you come along - and of course how the performance looks like in your real-world-app...
Beside that, the SQLite-Wrapper is able to give you a real ADO-Recordset as a copy from its internal SQLite-Recordset-Content as a disconnected ADO-Rs, containing all the Data of the SQLite-Recordset using the Method: Set ADORs = SQLiteRs.GetADORsFromContent
But this ADORs is then a real disconnected one, without any relation to the SQLite-DB or SQLite- Connection, so .UpdateBatch on this ADORs-Copy will of course not write its changes back into the SQLite-DB (you would have to use the SQLite- Command- or Cnn.Execute-Methods for the Write-Direction against the DB). And this Extra-Copy-Method will of course affect the Read-Performance of SQLite - basically this should result in ca. the same (slower) Read-Performance as if retrieving an ADO-Recordset directly from a JET-*.mdb, maybe in sum (including the Extra- Copy) some 10 or 20% slower whilst retrieving smaller Recordsets and ca. the same amount faster, if the Recordsets become larger.
So I would go with the additional IRecordset- Implementation for your App, but for a first try you could use this method, to avoid the Implementation- efforts for IRecordset, simply returning an ADO-Recordset from bot IDataBase-Implementations.
Olaf
MP - 23 Aug 2008 03:29 GMT >> I have a project where i've been using some classes that >> wrap ado stuff so I wanted to make a drop in replacement [quoted text clipped - 3 lines] > includes the Rs-Methods and -Properties you want > to use in your App. snip
> So I would go with the additional IRecordset- > Implementation for your App, but for a first try [quoted text clipped - 3 lines] > > Olaf Thanks Olaf that's great! Very understandable and I'm heading that way now. Thanks much for clearing the cobwebs out...thats exactly what I needed ...and your explanation is very clear on how to proceed. Thanks again
Mark
MP - 26 Aug 2008 19:25 GMT >> So I would go with the additional IRecordset- >> Implementation for your App Olaf, I'm in the process of converting (refactoring <g>) my client code to use IRecordset and IDbController
the only question here is the efficiency difference between For Each and For lIdx = 0 to oRs.FieldsCount -1 although I'm sure it's negligible especially in my small data sets...I was just wondering...
previously in client code using ado objects... <Dim oRs as adodb.Recordset, oField as Field> 'ors is a specially sorted recordset to get records in a particular order Do Until ors.EOF sTest = "" 'for each supposed to be better than index... For Each oField In ors.Fields 'compose pipe delim string for postprocessing sTest = sTest & "|" & oField.Value Next oField 'send trigger to "pseudo StateMachine"<g> for postprocessing StateMachine sTest ors.MoveNext Loop
'revise to use Interfaces which can be either ado or sqlite since cRecordset doesn't have a Fields collection, and if I use your suggestion of not exposing the field level interface <eg: in IRecordset: Public Property Get FieldValue(KeyOrIndex As Variant) As Variant Public Property Let FieldValue(KeyOrIndex As Variant, ByVal vFieldValue As Variant)
then I assume I need to rewrite the above something like thus: Dim oRs as IRecordset, lIdx as long Do Until ors.EOF sTest = "" For lIdx = 0 to oRs.FieldCount -1 sTest = sTest & "|" & oRs.FieldValue(lidx) Next lIdx
StateMachine sTest ors.MoveNext Loop
and what ever small inefficiency might be introduced by indexed loop would be more than offset by all the sqlite efficiency over the ado approach in any case (or so I assume)
besides in my small case, efficiency isn't a current concern, I'm just trying to get a functioning, maintainable, program I can "performance tune/optimize" if I ever get it all working in the field(office <g>)
ps not that it affects the above but fyi, inside stateMachine sub the strings pass through various tests and a number is calculated to apply to that record this gets stored in a cSortedDictionary(thanks to you know who! :-) ) Call moDStore.Add(sTest, mlMarkNumber) and that later gets written back to the database by parsing the pipe delim string to find the appropriate record(s) and enter the calculated number value
thanks again, (as always)
:-) mark
Schmidt - 26 Aug 2008 22:34 GMT > the only question here is the efficiency difference between > For Each and For lIdx = 0 to oRs.FieldsCount -1 [quoted text clipped - 7 lines] > sTest = sTest & "|" & oRs.FieldValue(lidx) > Next lIdx There'd be not much of a difference measurable IMO. For Each has the extra-overhead of initializing and jumping into the IEnumVariant-Implementation (whilst preparing for "For Each"), though once done will give you the next Field-Object a little bit faster than the direct indexing. Finally, especially if you don't enumerate huge Field-Lists, the gains and losses of both methods will probably balance.
For example you can achieve a larger gain, if you handle the String-Concats a bit more efficiently inside an additional Method on your IRecordset-interface: 'Implementaion-Example: Function GetJoinedFieldValues(Separator as String) as String Dim i as Long Static FArr() as String, FCount as Long With mRs.Fields If FCount <> .Count Then FCount = .Count if FCount = 0 Then Exit Function Redim Preserve FArr(0 To FCount - 1) End If
For i = 0 to FCount - 1 FArr(i) = .Item(i).Value Next i End With GetJoinedFieldValues = Join(FArr, Separator) End Function
This way you can spare the indirect method-calls through the IRecordset-Implementation-Layer for each single Field in this high-frequented loop - and also the somewhat more LOC for the faster StringArray-Concats are hidden to your application, which codewise can become now: Do Until oRs.EOF 'send trigger to "pseudo StateMachine"<g> for postprocessing StateMachine oRs.GetJoinedFieldValues("|") oRs.MoveNext Loop
At this occasion an advice regarding Null-Values inside the Fields - ADO-Fields which contain Nulls, will give you vbNull in the returned Variant-FieldValues, which do not "AutoCast" and require an extra-check IsNull(), before filling your String-Array-Idxposition. 'ADO If IsNull(.Item.Value) Then FArr(i) = "" Else FArr(i) = .Item(i).Value End If
In the SQLite-Wrapper I'm not using vbNull, but vbEmpty instead, to "mark" NullFields, returned by the Variant-FieldValues. These cast automatically to an empty String, so you don't need to code that explicitely as in the ADO-Implementation.
On the other hand, if you instead want to explicitely "show" Null-Values inside your StringConcat-return, you can use e.g.: 'SQLite If IsEmpty(.Item.Value) Then FArr(i) = "<NULL>" Else FArr(i) = .Item(i).Value End If
'ADO If IsNull(.Item.Value) Then FArr(i) = "<NULL>" Else FArr(i) = .Item(i).Value End If
Olaf
MP - 26 Aug 2008 23:39 GMT Thanks again, great advice all Thank you mark
>> the only question here is the efficiency difference between >> For Each and For lIdx = 0 to oRs.FieldsCount -1 [quoted text clipped - 83 lines] > > Olaf MP - 03 Sep 2008 01:50 GMT Hi Olaf, If I havent' worn out my welcome yet <g> in your sample you "set up" a command object when you create the table 'prepare a command-object for faster inserts of new FileContents Set CmdContents = Cnn.CreateCommand("Insert Into FileContents(ContentType, FileNames_ID) Values(?,?)") then later in Private Sub iFileScan_InsertFileContent(FileName As String, FileContent As String) 'that's the found content of a given File, so we check this in CmdContents.SetText 1, FileContent
you set the values (i'm guessing that's what .SetText is doing)
in my 'adaptation' of your sample to my previous code i'm creating the tables thus: Private Sub CreatePcmkDatabase(TableName As String) Cnn.Execute "Create Table DwgNames (ID Integer Primary Key," & _ " fldDwgName Text)" Cnn.Execute "Create Index Idx_DwgNames " & _ " On DwgNames(fldDwgName Collate NoCase)"
Cnn.Execute "Create Table " & TableName & " (ID Integer Primary Key," & _ " ContentType Text," & _ " DwgNameKey_ID Integer, " & _ " fldType Text, " & _ " fldCond Text, " & _ " fldCondVar Text, " & _ " fldCondVarDesc Text, " & _ " fldCondLbr Text, " & _ " fldFinLbr Text, " & _ " fldLengthMax Double, " & _ " fldLengthMit Double, " & _ " fldLengthAvg Double, " & _ " fldHandle Text, " & _ " fldPcmk Text, " & _ " fldMark Text, " & _ " fldOpp Text, " & _ " fldBlockName Text " & _ " fldSequence Text " & _ " fldLayoutName Text " & _ " fldCount Integer " & _ ")"
Cnn.Execute "Create Index Idx_DwgNames_ID " & _ " On " & TableName & "(DwgNameKey_ID)"
End Sub so as you see i'm not "pre-loading" a command object with the "template" you set up in your sample... Cnn.CreateCommand("Insert Into FileContents(ContentType, FileNames_ID) Values(?,?)")
should I be doing that???
i'm guessing that Values(?,?) is like a place holder for the values to be put into fields later,,, with one ? for each field to be filled in???
is that right?
previously(ado version) i had an 'insert' function that built the value string and concated it with the insert fieldlist string then sent it to moConnection.Execute (insertString)
now i've adapted to your sqlite version it looks like:
(ValueString is the built string...created in another function that puts the values in order of field names)
sInsertsql = "Insert Into " & EDW_PCMKS_TABLE & " (...list of field names...) VALUES " & ValueString
then i just do Set CmdContents = Cnn.CreateCommand(sInsertsql) CmdContents.Execute
is that ok? or should i be pre-building the command in the database creation routine like your sample?
thanks for all your help (still trying to get everything working to adapt to your sample...getting closer every week <g>) mark
Schmidt - 03 Sep 2008 19:25 GMT Your Table- and Index-Creation looks Ok for me.
[Regarding Command-Objects]
> so as you see i'm not "pre-loading" a command object > with the "template" you set up in your sample... Yep.
> Cnn.CreateCommand("Insert Into FileContents(ContentType, > FileNames_ID) Values(?,?)") > > should I be doing that??? If you want to work in the fastest possible way, then yes.
> i'm guessing that Values(?,?) is like a place holder for the > values to be put into fields later,,, with one ? for each field > to be filled in??? > is that right? Yes, the '?' are placeholders for the Field-Values.
A standard-insert-statement would expect the Values directly in the Insert-SQL, which you can throw at the DB using "concrete Values": Cnn.Execute "Insert Into FileContents(ContentType, FileNames_ID) Values('A', 1)")
This would immediately insert the String-Value A and the Long-Value 1 into the DB-Table.
But working with these explicite Insert-Statements over Cnn.Execute is not recommended, performancewise because the SQL-Interpreter will have to parse each statement independently (although if you are in a loop for multiple Record-inserts one usually only changes the values ) and typewise it is not recommended, because you will have to perform your own String-Concats, to place the FieldValue correctly inside that Statement-String using their correct "encoding" (Strings within '...', or ByteArray-Blobs would have to be Hex-Encoded, etc.).
That's why these Command-Objects exist, which only require the SQL-Interpreter to parse and prepare the given Command once, precreating Field-Structs for each '?', which will only receive your changed Values for each record - and that with TypeSafety and without String-Concats on your side. SQLite-Commandobjects are currently only for the Write-Direction, and after preparing such a command- Object you will only have to change the '?'-Parameters in correct (OneBased) order. CmdContents.SetText 1, FileContent the above will place a String- or Text-Value inside the CommandObject at '?'-Param-position 1. CmdContents.SetInt32 2, FileID and that will place a Long-Value appropriately at '?'-param-position 2. To beam this fully filled Record into the DB, you will only have to call CmdContents.Execute.
Then proceed with the next Record-Values on the very same (preparsed) CommandObject - all wrapped ideally inside a transaction.
> previously(ado version) i had an 'insert' function that > built the value string and concated it with the insert > fieldlist string then sent it to > moConnection.Execute (insertString) As said, this method will work in my wrapper in exaclty the same way too - but although it works, it is not recommended, neither in ADO nor in SQLite.
> now i've adapted to your sqlite version it looks like: > [quoted text clipped - 9 lines] > > is that ok? No, the command-object is wasted here - you can directly execute such a "complete" (with explicite Values) Insert- Statement with Cnn.Execute.
As said, for a given "insert-job" you can precreate a fixed statement for a given table using one Command- Object using the correct count of matching '?' in the Values-list. Then you can forget that "InsertString" completely and further use only the CommandObject, which is based on that string (same thing for ADO).
If you currently have a routine, which is building your concrete InsertValues-String, then you should pass now the Command-Object into that Routine, together with the needed Parameters.
e.g. your old version may be look like: Sub PlaceValuesIn (strInsertCommand, P1 as String, P2 as Long, ...) strInsertCommand = strInsertCommand & "'" & P1 & "', " strInsertCommand = strInsertCommand & P2 & ")" End Sub followed by Cnn.Execute strInsertCommand
...and should now become: Sub PlaceValuesIn (oCmd as cCommand, P1 as String, P2 as Long, ...) oCmd.SetText 1, P1 oCmd.SetInt32 2, P2 End Sub followed by oCmd.Execute
> or should i be pre-building the command in the database creation > routine like your sample? Why not, the Command-Objects can remain alive as long as your Connection-Object remains alive - you can also create them dynamically "as needed", but normally there are not as many different "Insert, or Update-Jobs" per Application - in your case I suspect, there are only 2-4 (mostly one Cmd-Obj for each different Table-Type).
And BTW, there's also of course Rs.UpdateBatch, working very similar in both, the SQLiteWrapper and ADO too (ADO will require the right Rs-Settings (adUseClient, adOpenStatic, adLockBatchOptimistic) and then there are no big differences in using that feature. Here you also have TypeChecking whilst updating or inserting new values into the Recordset-Fields. And as the method-name suggests, you can do more than one change in the Rs before calling UpdateBatch - e.g. 10 Deletes, followed by a few Fld.Updates (simply by using Rs.Fields(xxx).Value = NewValue), followed by 100 Inserts using (Rs.AddNew and placing new Values in the Rs.Fields-List.). Rs.UpdateBatch can take care of all three WriteDirection- Types and performes them all together (after each other) in one single UpdateBatch-Call. In both cases (ADO and my Wrapper), these Call is using an implicite Transaction, with AutoRollback in case of an error. Very comfortable that, but with less control of course - your decision (performancewise UpdateBatch is a slower than direct usage of the Command-Objects, because of more internal Overhead, although I use Command-Objects too inside the method-implementation, but keeping the state of the Rs-internal stuctures for the case of an eventual Rollback is costly).
So for the write direction there are three different ways (in both "DB-Acces-Wrappers") Cnn.BeginTrans Cnn.Execute "Some Direct Statement" Cnn.CommitTrans
oCmd-Create-and-Prepare Cnn.BeginTrans oCmd-FillValuesIn oCmd.Execute Cnn.CommitTrans
or simply Rs-Do-a-lot-of-changes Rs.UpdateBatch
HTH
Olaf
MP - 03 Sep 2008 20:19 GMT Fantastic Thanks for the very clear explanation... back to the drawing board
:-) mark
> Your Table- and Index-Creation looks Ok for me. MP - 04 Sep 2008 01:36 GMT > Your Table- and Index-Creation looks Ok for me. > >> should I be doing that??? > If you want to work in the fastest possible way, then yes. ok
just to confirm, given the table structure 'TABLE STRUCTURE ' bSuccess = Cnn.Execute("Create Table " & TableName & " (ID Integer Primary Key," & _ ' " DwgNameKey_ID Integer, " & _ ' " fldType Text, " & _ /etc etc
and given the command 'setup' thus Set CmdContents = Cnn.CreateCommand("Insert into " & TableName & "(DwgNameKey_ID, fldType, etc ....) Values(?,?,?, etc...)")
then ID is field 1 DwgNameKey_ID is field 2 fldType is field 3 is that right? (in whatever order the table is created in...)
then a command object fill in the values function would be like:
'don't put any thing for field 1 since it's primary key it's going to be automatically created right???? 'except i don't see any settings that tell it to be unique or autonumber...i'm just guessing that's a default in sqlite????
oCommand.SetInt32 , 2, mlDwgNameKey oCommand.SetText , 3, sType etc etc
is that right?
or is ID possibly field number 0 and that's how it would never get filled in manually by code since the field indexes start at 1???
thanks again mark
Schmidt - 04 Sep 2008 01:55 GMT > just to confirm, given the table structure > 'TABLE STRUCTURE [quoted text clipped - 13 lines] > is that right? > (in whatever order the table is created in...) No, it is not the FieldOrder of your CreateTable-statement which counts here, but the FieldOrder of the InsertStatement (the FieldNames you give within the first parentheses). And the count of the ?-Placeholders has to match not the FieldCount of your Table-Definition, but the FieldCount of your Command-Statement. So - DwgNameKey_ID is Index 1 , fldType is 2, etc.
> 'don't put any thing for field 1 since it's primary key it's > going to be automatically created right???? Right in principle, but we achieve that not by starting at index 2, but by leaving out the ID-Field in our Insert- Command-Description completely beforehand. As said, Index 1 points to DwgNameKey_ID,...
Olaf
MP - 04 Sep 2008 18:28 GMT >> just to confirm, given the table structure >> 'TABLE STRUCTURE [quoted text clipped - 29 lines] > > Olaf duh! of course... sorry for the dumb question
:-) mark
MP - 13 Sep 2008 02:43 GMT Hi Olaf, if you're still 'watching' :-)
...so i've gotten to where I can create a db, write (maybe) records, open existing db, read records....
to open I used the .Open method on the connection object... oConnection.Open dbName
how do i close the connection when i'm done? I don't see a oConnection.Close method do I just set oConnection = Nothing to close connection and release all objects???
for example, this was just to look at some temp dbs created during debugging....if they're empty i wanted to delete them Sub ReadDb(dbName as String, TableName as String, lRecords as Long) Set modb = New cDBControllerSQLite modb.Connect bSuccess = modb.OpenDB(dbName) If bSuccess Then LogEntry "Opened db" Else LogErr methodFailed, ProcName, "OpenDb" End If
modb.DumpSQLSelect "Select * From " & TableName, lRecords
'this was just to clean up some temp original dbs created during debug practise... If lRecords = 0 Then If MsgBox("Delete empty database?", vbYesNo Or vbDefaultButton2, "EMPTY DATABASE") = vbYes Then 'does this close db and release db file for deletion?? Set modb = Nothing Kill dbName End If End If End Sub
Schmidt - 13 Sep 2008 03:13 GMT > if you're still 'watching' :-) Apparently... ;-)
> ...so i've gotten to where I can create a db, write > (maybe) records, open existing db, read records.... As said, would be interested, to hear from you regarding some timing-results - or generally, how it compares with JET in your real App-scenario.
> to open I used the .Open method on the connection object... > oConnection.Open dbName Yep.
> how do i close the connection when i'm done? > I don't see a oConnection.Close method > do I just set oConnection = Nothing to close > connection and release all objects??? Yes, that was the intention in my wrapper (tried to avoid all these Close-Methods, that we see in ADO-Objects - no circular references in my Wrapper-Objects-Model, ergo not that much Code to write for cleanup). Look at the comments in my iFileScan-Demo, the SQLite-Implementation which wraps the Cnn and Rs-Objects does not need a Class_Terminate at all, since all the internal SQLite-Wrapper-Objects die automatically when they get Out-Of-Scope (when the implementing Parent-Class terminates).
BTW, just read your other post in m.p.v.g.d. regarding ADOX-based Table-Creation vs DDL-Executes - both methods don't really differ much in performance. If you look at the iFileScan-Sources, there I use Table- and Index-DDL for both versions (SQLite and ADO/Jet) - better to maintain IMO, because you can create/save your Schema-Definitions (all your DDL-Statements) also into your File of choice or inside another DB(-Table) or whereever. DDL is available on all DBs I know and does not differ all that much (mostly only on the Type-Def-Syntax), but in principle the Statements have the same structure and are easy to formulate after you become somewhat familiar with them.
Olaf
MP - 13 Sep 2008 05:13 GMT Thanks for the addn'l info
>> if you're still 'watching' :-) > Apparently... ;-) [quoted text clipped - 4 lines] > some timing-results - or generally, how it compares with > JET in your real App-scenario. I'm still struggling to get it inserted into the actual app. Due to my inexperience coding and limited time to work on this particular project it's taking a long time. I'm having to disentangle all the ways I was working before where it was tied to the ado based approach. Trying to get it so I can literally plug in the sql object and the ado object so I could time the comparison.
I have no doubt your sql version will be much faster than the ado version for that portion of the process
Getting the info from Acad takes a huge amount of time so I doubt if i'll see a big diff in overall time from the two for now (im building a dll that will be run in acad's process when it's compiled...then the time will be much faster) for working on uncompiled (referencing the vbp instead of the dll) it's wayyyy slow
I have an object that harvests and stores the information from an acad object from the acad documents. So I collect a few hundred of these objects each with it's own packet of information. The way I was doing it each object wrote it's info to the db one at a time(actually made the dbController object write it's info) your batch update idea would probably be better if I make a way to collect all the info into some form and then write it all at one time...not sure yet how to do that.
Then I either version one send that object to the database object(sql or ado) ... 'make the database object responsible to "read" a "datacontaining object"
OR... version two I send the database object to that object... 'make the "data containing object' responsible to put it's information into the database
I'm not sure which is the "correct" approach previously I was sending the db object to the "datacontaining" object (to get the info written to the db)
'assign responsibility to "DataContainingObject" to put it's own information in database 'in cBlockData.cls Public Function ToDatabase(oDatabaseController As cDatabaseController, _ bCancel As Boolean, _ bDebug As Boolean) As Boolean 'compose value string sInsertValues = EPADataValueString(mlDwgNameKey)
'send to db sInsertSql = "Insert Into " & oDatabaseController.TableName & " (fldType, fldCond, fldCondVar, fldCondVarDesc, fldCondLbr, fldFinLbr, fldLengthMax, fldLengthMit, fldLengthAvg, fldHandle, fldDwgNameKey, fldBlockName, fldSequence, fldLayoutName, fldCount) " sInsertSql = sInsertSql & "VALUES " & sInsertValues
ToDatabase = oDatabaseController.ExecuteSql(sInsertSql) End Function '-------------------------
so now i'm redesigning the whole project to work with the interfaces you suggested the above couples the BlockData object to a cDatabaseController object (ado wrapper)
now i'm rewriting everything to refereince and IDatabaseController etc... but i'm not sure if i want to couple the BlockData object to the database object or the other way around....
I could just create a reference in the database object to a "source of data" object to "read"
so anyway all that confusion and timeconsuming rewriting and debugging is why I don't have any progress to report as yet.
:-) Thanks for all your help and patience on my database education :-) I just wish there was some way I could repay all the kindness you've shown me over these years! mark
MP - 16 Sep 2008 20:17 GMT >> if you're still 'watching' :-) > Apparently... ;-) Hi Olaf, still contemplating how to modify my previous implementation to incorporate your sqlite wrapper. I was wondering if you'd have any insights to share how you'd go about it.... my scenario is this I have a collection of documents which are a collection of objects containing data in my previous ado approach I was getting a "Values ..." string from each object and passing that to an "Insert " statement on the connection object
basic pseudocode: in cDocuments.cls Sub ToDatabase(oDbController as IDBController) For Each oDoc in mcolDocuments oDoc.ToDatabase oDbController Next End Sub
in cDocument.cls Sub ToDatabase(oDbController as IDBController) For Each oBlockData in mcolDataObjects oBlockData .ToDatabase oDbController Next End Sub
in oBlockData.cls Sub ToDatabase(oDbController as IDBController) 'previous ado Insert string implementation 'collect values from internal variables and compose insert sql statement ValueString = " ( '" & sType _ & "', '" & sCond _ & "', '" & sCondVar _ & "', '" & sCondVarDesc _ ..etc ... & "')"
'---------------------------------- < > ----------------------------- 'that string gets passed to moDBController sql insert line like the following ' sSql = "INSERT INTO " & msTableName & _ 'list of field names in same order as list of values to be input '" (fldType, fldCond, fldCondVar, fldCondVarDesc, ...etc...,) 'VALUES " & ValueString '---------------------------------- < > ----------------------------- oDbController .Execute sSql End Sub
That was probaby the wrong way to do it from the start since it calls an execute for each record(instead of doing a batch...which i've never done yet)
now since the sqlite implementation takes a command object and uses .SetText etc I need to modify how i'm composing the info and probably want to collect them all into one group then do one .Execute (i presume)
but somehow i'd have to set it up so either ado or sqlite would work...so I could give you those timings you were curious about (me too) the sqlite version wants something like
...inside cBlockData.ToDatabase...or somewhere??? Each oBlockData object needs to do something like this...or somehow give it's collection of data to some object that passes it to IDbController 'new sqlite command object type implemetation... oCommand.SetInt32 1, mlDwgNameKey oCommand.SetText 2, sType oCommand.SetText 3, sCond oCommand.SetText 4, sCondVar oCommand.SetText 5, sCondVarDesc oCommand.SetText 6, sCondLbr oCommand.SetText 7, sFinLbr oCommand.SetDouble 8, dLengthMax oCommand.SetDouble 9, dLengthMit oCommand.SetDouble 10, dLengthAvg oCommand.SetText 11, msHandle oCommand.SetText 12, msName oCommand.SetText 13, sSequence oCommand.SetText 14, msLayoutName oCommand.SetInt32 15, lCount 'either execute once for each object ' oCommand.Execute '....or ' maybe I just collect all Command objects into a collection for that document 'moDocument.CommandsCollection.Add oCommand
'instead of executing once for each object 'then collect all documents command collections into the Documents.CommandCollection.... 'then somehow cSqliteImplementation.ExecuteCommandCollectionBatch (colAllCommandsAllDocs) ...or something like that????
but the ado version still uses Conn.Execute "Insert Into ....etc " so I need some kind of (adapter pattern???) object that can either build an insert string for ado, or fill in command objects for sqlite...
...is that more or less the direction I should be looking in???
thanks obviously still confused :-) mark
Schmidt - 17 Sep 2008 12:57 GMT > I have a collection of documents which are a collection of objects > containing data [quoted text clipped - 15 lines] > Next > End Sub First, you should wrap this DB-Inserts with a transaction - it is your decision, if you want to wrap the "whole story" oDBController.BeginTrans For Each oDoc in mcolDocuments oDoc.ToDatabase oDbController Next oDBController.CommitTrans
or if you want to have Transactions at the Document-Level: For Each oDoc in mcolDocuments oDBController.BeginTrans oDoc.ToDatabase oDbController oDBController.CommitTrans Next
Of course you should also place an appropriate oDBController.RollbackTrans in case of an error.
> in oBlockData.cls > Sub ToDatabase(oDbController as IDBController) [quoted text clipped - 73 lines] > obviously still confused :-) > mark MP - 17 Sep 2008 14:25 GMT >> I have a collection of documents which are a collection of objects >> containing data [quoted text clipped - 6 lines] > Next > oDBController.CommitTrans ah! that's what I was missing...I saw it but just didnt' click...
> Of course you should also place an appropriate > oDBController.RollbackTrans in case of an error. So the individual command.Executes (at the individual blockobject level) won't take place till the .CommitTrans? so there's just one write after all?
in that case there'd be no difference between (version 2 below) collecting all commands and running them at one time versus running each command each time a blockobject is encountered (version 1 below)
? is that right?
>> in oBlockData.cls >> Sub ToDatabase(oDbController as IDBController) >> 'new sqlite command object type implemetation... >> oCommand.SetInt32 1, mlDwgNameKey etc...
version 1
>> 'either execute once for each object >> ' oCommand.Execute >> '....or version 2
>> 'moDocument.CommandsCollection.Add oCommand 'and eventually...ForEach ocommand in collection ocommand.Execute
and then in BlockData.ToDatabase would I just do a TypeOf check on oDbController0 and have two branches of implementation for sqlite versus ado?
thanks again mark
Schmidt - 17 Sep 2008 16:33 GMT Sorry first, that post was send unintentionally and was not completed.
[Transactions]
> > Of course you should also place an appropriate > > oDBController.RollbackTrans in case of an error. > > So the individual command.Executes (at the individual > blockobject level) won't take place till the .CommitTrans? > so there's just one write after all? More or less, yes. Speaking for SQLite - all the single Executes are stored in a journal-file first (when a transaction is open) and in case of a commit, all the new pages in the journal are written into the real DB-File internally (in fast page-mode). Without "transaction-wrapping" SQLite performs much slower on the inserts, because then "direct-DB-Writes" will by default be done in secure-mode for each single Cmd.Execute (single-record-writes directly to disk, and not against the Write-Cache of the OS, as in contrast e.g. JET does by default).
... What was missing in the previous post is the following:
Regarding Command-Abstraction... Simply define an iCommand-Interface in the same way as you already done that for iRecordset.
To make it a bit easier, I've already coded that in this Demo: www.datenhaus.de/Downloads/DBController.zip This includes abstracting interfaces for iDBController iCmdInsert and iRecordset - and the appropriate implementations for ADO/JET and also for SQLite.
The Demo-Form then shows, how to use these interface-implementations in a generic way, writing some 5000 Records, wrapped in a transaction - and reading these Records back into an iRecordset, followed by a Rs-dump.
That should help you IMO, to bring something similar into your own App.
Olaf
MP - 17 Sep 2008 17:01 GMT > Sorry first, that post was send unintentionally > and was not completed. snip
> That should help you IMO, to bring something similar > into your own App. > > Olaf once again, thank you very much will study this and hope to make some progress soon mark
MP - 17 Sep 2008 23:12 GMT snip
> To make it a bit easier, I've already coded that in this > Demo: www.datenhaus.de/Downloads/DBController.zip [quoted text clipped - 12 lines] > > Olaf Man! You are nothing short of amazing!!!
:-)))))))))))))) You make it look so Simple!!!!!
:-)))))) Thank you so much!!!!!!! mark
Schmidt - 18 Sep 2008 18:51 GMT > You make it look so Simple!!!!! Nahh, it *is* simple if one becomes a bit familiar with the VB6 interfaces- and implementation-concept (and of course a bit of experience with the ADO- or SQLite-Command-Object-stuff ... agreed).
Just one small advise regarding the used cSortedDictionaries inside the Cmd-Object-Implementations - please make sure, that you switch the .StringCompareMode of all the dictionaries to TextCompare explicitely (after instantiation this defaults to BinaryCompare, which can cause some problems with the FieldNames, if these are not written exactly the same). So, sorry I simply forgot that in the example.
Olaf
MP - 18 Sep 2008 22:46 GMT >> You make it look so Simple!!!!! > Nahh, it *is* simple if one becomes a bit familiar with > the VB6 interfaces- and implementation-concept (and > of course a bit of experience with the ADO- or > SQLite-Command-Object-stuff ... agreed).
> Just one small advise regarding the used cSortedDictionaries > inside the Cmd-Object-Implementations - please make sure, [quoted text clipped - 3 lines] > the FieldNames, if these are not written exactly the same). > So, sorry I simply forgot that in the example. thanks, will keep that in mind...
I'm thinking of using your cleverly elegant FieldType-DescriptionPairs to send to both create table and commands
Private Function GetFieldTypes(oDB As iDBController)as cSortedDictionary Dim CmdFieldTypes As cSortedDictionary 'Create FieldType-DescriptionPairs for our InsertCommand and also creating schema? Set CmdFieldTypes = New cSortedDictionary
'list of fields and types CmdFieldTypes.Add "Txt", enmFieldTypes.fldText ...etc
Set GetFieldTypes = CmdFieldTypes End Function
Then ... Dim CmdFieldTypes As cSortedDictionary, Cmd As iCmdInsert Set CmdFieldTypes = GetFieldTypes Set Db = oDB.CreateSchema("dbName", CmdFieldTypes) Set Cmd = oDB.CreateInsertCmd("cmdName", CmdFieldTypes)
object to send to both create table and create cmdInsert using your elegant structure in
Private Sub CreateSchema(FieldList as cSortedDictionary)
Cnn.Execute "Create Table Test (ID Integer Primary Key," & _ 'parse Field list for name and types like you did in createCmd function... " Txt Text," & _ ...etc
End Sub
does that make sense?
Thanks again, Mark
Schmidt - 19 Sep 2008 00:50 GMT > I'm thinking of using your cleverly elegant FieldType- > DescriptionPairs to send to both create table and commands For commands it is not important, in what order the Field-Parameters sit in the Commands Insert-Statement, but remember that we have a *sorted* Dictionary here - so if you want to use it also for Table-Creation, then I would at least keep the ID-Column "out of the game" (what you probably want to do too) - but the order of the rest of your Table-Fields is then not determined. That's probably not such a big problem, but sometimes you want to do a simple "Select * from Table" and expect a certain Field-Order in this case. If you always want to give the FieldOrder explicitely in your Rs-Selects, then this sounds like a good idea.
I've just build this into a new version of the DBController- Demo (together with the TextCompareMode-fixes for the Dictionaries) - same link:. www.datenhaus.de/Downloads/DBController.zip
So, now you can create your Command-Objects simply by specifying the TableName - the relevant TypeInfo-Dictionaries are already internally stored (in the Controller) whilst creating the DB-Tables from appropriate FieldType-Pairs (each FieldType- Dictionary is saved in an additional Dict with the Tablename as Key).
Olaf
MP - 19 Sep 2008 03:13 GMT >> I'm thinking of using your cleverly elegant FieldType- >> DescriptionPairs to send to both create table and commands [quoted text clipped - 3 lines] > so if you want to use it also for Table-Creation, then > I would at least keep the ID-Column "out of the game" Right
> (what you probably want to do too) - but the order > of the rest of your Table-Fields is then not determined. not sure I get what you mean...I didn't think there was any particular ordering to fields except when you ask for a rs and then select whatever fields you want in whatever order you want?
> That's probably not such a big problem, but sometimes > you want to do a simple "Select * from Table" and expect > a certain Field-Order in this case. so that would come out in the order the table was created in?
> If you always want to give the FieldOrder explicitely > in your Rs-Selects, then this sounds like a good idea. I wasn't so much caring about the order, just that in my case I have a lot of fields and am always having to page back and forth to remember their exactnames/positions in the select statements etc just thought it would simplify my poor memory if I could build the 'structure' once then use it everywhere this is a very unusual(i'm sure) use of a database where I really only have a couple selects for a special purpose...not a general usage db where users are looking up varieties of info and adding/deleting records...it's just a one time use db to record/sort/identify/and label some parts read from a list of documents(acad dwgs)
so the idea of a fixed list of fields for both table creation and command creation is probably not a 'general use' concept, but seems handy in this case.
> I've just build this into a new version of the DBController- > Demo (together with the TextCompareMode-fixes for the [quoted text clipped - 10 lines] > > Olaf You are Too Much! as they say in the States :-) Thanks again mark
Schmidt - 19 Sep 2008 04:33 GMT [Dictionary-sorted Field-Order for Create Table Statements]
> not sure I get what you mean...I didn't think there was > any particular ordering to fields except when you ask for > a rs and then select whatever fields you want in whatever > order you want? Yep, if you explicitely give the FieldNames in a Select, then the 'Create Table order' is not important (this has also no performance-impact).
> > That's probably not such a big problem, but sometimes > > you want to do a simple "Select * from Table" and expect > > a certain Field-Order in this case. > > so that would come out in the order the table was created in? Yes, the * will give you that order. But that's also not a big problem, since you can always define a View with the needed Field-SubSet and Field-Order you want. And then simply perform the * - based Select against that ViewName.
> > If you always want to give the FieldOrder explicitely > > in your Rs-Selects, then this sounds like a good idea. [quoted text clipped - 4 lines] > just thought it would simplify my poor memory if I could build the > 'structure' once then use it everywhere As said, using Views you can predefine your Selects once and in your App later use a simple "Select * from ViewName ..." instead directly working with the tables, managing the different FieldLists explicitely in your Client-Code. Views can be created very easy: Cnn.Execute "Create View ViewName As " & _ "Select F1, F3, F7, F23... From Table" You can also predefine Where-Conditions and Joins inside the Views Select-part - very handy all that for more generic Client-Code (e.g. Reports which are based on View-Definitions - no need to change Client-Code, just to include an additional Column into the "Table-Printout" - a View-Redefinition with that additional Column should be enough).
> so the idea of a fixed list of fields for both table creation and > command creation is probably not a 'general use' concept, > but seems handy in this case. Yep.
Olaf
MP - 21 Sep 2008 02:12 GMT snip
>> so the idea of a fixed list of fields for both table creation and >> command creation is probably not a 'general use' concept, >> but seems handy in this case. > Yep. > > Olaf Thanks I'll be working this wkend to study and incorporate the new pattern. Really appreciate your taking all this time to "baby step" me through this process. You are really patient... I know I'm not too quick on the up take...it's very generous of you not to just blow me off for being so slow . thanks Mark
|
|
|