Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Venelina Jordanova
Introduction
Developing wide range of applications we always need to consider not only user interface, but also data model and data access. Designing data layer the developer has to choose the best database platform for the application, but he has always to keep in mind that the application which will proceed a few thousand records in single user mode in next years could expand to a large scale application, handling hundreds of thousands records. In such case you are often enforced to migrate application data toward more vigorous data storage. This often can lead to a need to fully rewrite applications data access. The approach used to access data is basic matter to avoid such fundamental application amendments, when data storage needs to be changed. In this session we will examine how CursorAdapter class can help developers to organize applications data access in such way, so future data storage changes (and respectively data source changes) to have minimal impact on it.
(Group DATA)
(Group DATA)
You can also use AfterCursorFill event to create necessary indexes. All of insert and update events receive parameters that give you information what happened in the data. cFldState parameter field and row state as you would receive from GETFLDSTATE(-1) function. To make easier work with new CursorAdapter class VFP 8 includes a new CursorAdapter Builder. It helps you quick to set CursorAdapters properties and also generates necessary code for you. In addition to this we also have a new DataEnvironment Builder that helps to set Data Source properties for the DataEnvironment. This is also the place, where you add CursorAdapter objects to DataEnvironment. This will be discussed later in this article.
Sample application
An important part of this article is a small sample application that is designated to show how CursorAdapter class can save your time and efforts when migrating data between different data storage platforms. The sample application consists of a form that combines presentation and business logic layers and a set of data access classes based on CursorAdapter class. Used database is well-known Northwind database, as far as it comes with Visual FoxPro 8 and MS SQL Server installations. The form presents orders and detail lines for particular order. It uses updateable cursors created from Orders and OrderDetails tables and a set of lookup cursors corresponding to Employee, Customers and Shippers tables. DataAccess is a class library that contains several CursorAdapter classes used to access applications data.
To run the application you need to access Northwind database for every different data source type that you want to use. For Native VFP data it is located in Northwind folder under _samples path. It also ships as a sample database with MS SQL Server and MS Access.
(Group DATA)
In the beginning our application will use VFP Native data. In the Data source type drop-down list we choose Native and then set the Database. For some reasons you may not want the Builder to generate code for the connection. Then connection setting will be used temporarily only in the builder. In the Data Access page Data fetching and Buffering mode properties have to be set and all other Data access properties are left blank. All of them will be further defined for every particular subclass.
10th
(Group DATA)
Figure 4. Data Access page is used to specify Select command and Cursor Schema along with Data fetching and Buffering mode.
The Select Command Builder (Figure 5) can help us easy and fast to create simple SELECT statements.
Figure 5. Select Command Builder provides visual interface for creating SQL SELECT statement
The necessary table has to be chosen from the drop-down list and you can move the fields that you need on the right side, where selected fields are listed. In the same way as defining views, you can also move the first available choice * that means that all fields from the source table will be selected. If you use Native data source type, you are also allowed to add additional tables by clicking the Add Tables button. Often this could be free tables that are not included in the database. Clicking on OK will build the appropriate SELECT statement. If you click on Build button for the Cursor Schema it is automatically generated using the specified Select command. In this moment the builder instantiates a cursor adapter and calls its CursorFill method to populate the cursor. In order to work this feature needs to have access to your data otherwise the cursor could not be created. You can also write the Cursor Schema yourself. Cursor adapter - powerful tool 2003 Venelina Jordanova (Group DATA) 10th European Visual FoxPro DevCon 2003 E-CAD 5
If we used the Select Command Builder, Tables property is already filled on the Auto-Update page (see Figure 6). Send updates option is set by default. If you want data not to be sent back to data source you can uncheck it. Auto-update checkbox determines whether CursorAdapter will automatically generate update statements. In fields grid we see fields mapping (also automatically generated) and we have to choose key fields (key symbol column) and fields that will be automatically updated (pencil symbol column).
Figure 6. Auto-Update page is used to specify how update statements will be generated.
Here we can also define conversion functions, which will be used for certain fields transformation before to send data to data source. You can also choose which values will be used for automatically WHERE clause generation. Finally we have to write code that performs additional actions according application needs. For example in the AfterCursorFill event we will write code to create necessary indexes. These indexes will be used later in forms to ensure proper data viewing in user interface forms or for building relationships between cursors. It is also possible here to perform some calculations or data conversions that cannot be performed at data storage side. An example for such calculation is a resultant field containing the number of overdue days for a bill (or an invoice). When you access Native data and your fields are of DATE data type this can be as simple expression as:
SELECT InvoiceId, InvoiceDate, iif(DATE() > DueDate, DATE() DueDate, 0) as OverdueDays from Invoice
When your fields are of DATETIME data type command will look like this:
SELECT InvoiceId, InvoiceDate, iif(DATE() > TTOD(DueDate), DATE() TTOD(DueDate), 0) as OverdueDays
10th
(Group DATA)
from Invoice
Finally if you access a SQL Database trough ODBC data source your select command:
SELECT InvoiceId, InvoiceDate, case when DATEDIFF(dd, DueDate, GETDATE()) > 0 then DATEDIFF(dd, DueDate, GETDATE()) else 0 end as OverdueDays from Invoice
To apply such functionality you will either need to write complex code to assign different values to SelectCmd depending of DataSourceType or you can perform data manipulation in AfterCursorFill event. In this case the SelectCmd will look in this way:
SELECT InvoiceId, InvoiceDate, DueDate, 0 as OverdueDays from Invoice
This will create an empty field where in AfterCursorFill you can store calculated number of days later using similar code:
CurrentDate = DATE() RELPACE ALL OverdueDays WITH IIF(DueDate > CurrentDate, DueDate - CurrentDate, 0)
Which one of the above approaches is better for your application depends on several factors. It depends on type and complexity of calculations that you need to perform, on amount of processed data rows. For some purpose you may prefer to write simple data conversions at the client side, but for heavy data processing you might prefer to use server side stored procedures. Concerning accessing different data sources you could prefer to processes data using common FoxPro code independent of data storage specifics. However, when complex online analyzing processing on large data is needed stored procedures that are optimized and cached at server side must always be considered.
3 4
(Group DATA)
If the ODBC driver or OLE DB Provider is unable to provide required functionality or if the functionality is disabled, settings 1, 2, and 3 for ConflictCheckType might fail. For example is SET NOCOUNT ON in SQL Server. In this case no information about affected records is available. If you set ConflictCheckType to 4 CursorAdapter uses ConflictCheckCmd property. Its value is appended to the commands specified by the UpdateCmd and DeleteCmd properties for checking update or delete conflicts. For example when working with Native data source you can write a function that checks conflicts using _tally system variable. If your data storage is SQL Server, you can use @@ROWCOUNT and @@ERROR system functions like this:
ConflictCheckCmd="IF @@ERROR=0 AND @@ROWCOUNT!=1 RAISERROR (' Update conflict!', 16, 1)"
When we present them in a drop-down list it is more suitable to have one common column EmployeeName that will be resultant of FirstName and LastName. To accomplish this, we will amend the generated select statement in that way:
select RTRIM(EMPLOYEES.FIRSTNAME) + " " + RTRIM(EMPLOYEES.LASTNAME) as EmployeeName, EMPLOYEES.EMPLOYEEID from EMPLOYEES
When we change the select command we have to press Build Schema button again the builder will automatically generate correct cursor schema. (Figure 8)
10th
(Group DATA)
It is very important when you plan to use different data sources to write SELECT statement compliant with all of them. In this particular case I used RTRIM function instead of TRIM or ALLTRIM functions, because it is a common for VFP and T-SQL and when we switch to SQL Server data source in the future this CursorAdapter class will remain useful without any changes in it. Unfortunately amending Select command in this way does will violate Select Command Builder and you cannot invoke it again and add new fields. So you can amend Select command when you will no longer need Select Command Builder help.
(Group DATA)
After choosing the class, a Builder form is invoked and you can change setting for the newly created object. In this way we create CursorAdapter objects for all data needed for the form and associated cursors are visible in the DataEnvironment Designer (Figure 10.)
Here, in the DataEnvironment we can also write code in CursorAdapter objects events to prepare forms specific indexes and other actions that related to the particular form. For this sample application I choose to load all data at once. In this case it is necessary in the forms Init method to establish a relationship between crsOrders cursor and crsOrderDetails cursor. In case you will need to proceed large amount of data, loading entire table content in cursors is not a good solution. In such case you could retrieve at whole only parent table (crsOrders) and retrieve child records (crsOrderDetails) for every parent record. You could even decide to retrieve Orders row by row and load off clients computer resources. For this type of data retrieval you should use parameterized queries. Parameters are passed in a way that you already know from pass-trough queries. Now after we have created all necessary CursorAdapter objects in the forms DataEnvironment, it is fast and easy to create controls on the form just by dragging fields onto it. On Figure 11 you can see how the form looks like after creating controls and adding a navigation bar as well as a few drop-down combo boxes that are designated to give users an easier way to fill customer, shipper and employee data. For same purpose in the grdOrderdDtails grid is also added a column colProductName.
10th
(Group DATA)
And finally we have to add code to fill OrderId field in crsOrderDetails, to ensure referential integrity when adding new detail records in the grid.
(Group DATA)
multi-user application where two users are entering orders simultaneously. They both start the application and retrieve existing data records. If the last used ID at this time is 2048, both of them will create records starting with 2049 in their cursors. What will happen if the first one entered two orders and then wants to save the data, but meanwhile the second user has already saved one order record? In crsOrders we will have two new records, numbered respectively 2049 and 2050. In scrOrderDetails will be also several records numbered with same OrderID values. After inserting these new records into data source, new ID values will be respectively 2050 and 2051. Therefore our first task afterwards should be to replace OrderId with 2050 in all crsOrderDetails records that till this moment were numbered 2049. Right in this moment we have mixed order details records for both new records, as far as all they contain OrderId value 2050. The only way to avoid this problem is to use ID values that could never come across real used values. Using negative numbers as ID values for newly added records can ensure that these values can never be mixed with IDs generated when data records are inserted into data source. Considering all mentioned above, in AfterInsert method we will write additional code to handle this. For this purpose I added a new property to the caOrders class that will hold the name of corresponding child alias, so the class itself not to require hard coded child cursors name. The code in AfterInsert method that will ensure referential integrity will look like this:
LPARAMETERS cFldState, lForce, cInsertCmd, lResult Local lnOldIDValue, lcFilter, lnNewId If lResult lnNewId = 0 DO case Case Upper(This.DataSourceType) == "NATIVE" * this code relies of fact that when working with * Native data source type, the table remains opened and * record pointer is on newly inserted record lnNewId = Orders.OrderId Case InList(Upper(This.DataSourceType), "ODBC", "ADO") If SQLExec(This.DataSource, ; "Select @@IDENTITY as NewIDValue", "crsNewID") > 0 lnNewId = crsNewID.NewIDValue USE in crsNewID EndIf Otherwise EndCase If lnNewId > 0 lnOldIDValue = Evaluate(Alltrim(This.Alias)+".OrderID") Select (This.cChildAlias) Replace all OrderId with lnNewId for OrderId = lnOldIDValue Select (This.Alias) Replace OrderId with lnNewId Else MessageBox("Error retreiving ID value", 0 + 16, "Error") endif endif
After making these final polishing, our sample application is up and running and its real life begins.
Application growing
After a couple of months flawlessly work, suddenly appears a need to upsize the database to SQL Server. There could be many different reasons: optimizing data processing or by security reasons. So, we need to migrate the data and upgrade our application to access SQL Server database. Thanks to our multi-tier application structure, this renovation will have minimal impact to our code. Fortunately we will need to make changes in data access classes only. Even more we will need to alter only our underlying base class caBseAccess.
(Group DATA)
In fact CursorAdapter Builder does our work and has written necessary code for assigning DataSource value. We can see this code in the Init method of the class.
This.DataSource = sqlstringconnect([DRIVER={SQL SERVER};] + ; [SERVER=JEI-Server;] + ; [Trusted Connection=ON;] + ; [DATABASE=NORTHWIND;])
In case we have designed tables structure considering future data migrating this step will be the only one necessary action to change data access from using native tables toward using ODBC data source.
Because fields names and type are same and we do not need to change CursorSchema of the class, but we need also to amend KeyFieldList and UpdatableFieldList properties. Figure 13 shows how this is done using the builder. You can also manually amend the code in the Init method of the class.
(Group DATA)
Figure 13. Setting caOrderDetails class according Northwind database on SQL Server
Migrating data from one type data storage to another must be thoroughly planed. All data types that dont quite match have to be considered. For example the logical data type does not have exactly correspondent data type among SQL Server data types, because Bit data type is presented as N(1) when retrieved into a VFP cursor. In order to have reusable code you can either use additional code to convert different data types or you have to consider using of data types that are identical in the set of data types for different data storages that you intend ever to use.
(Group DATA)
b)
Building a base set of underlying CursorAdapter classes gives you also the flexibility to change data source type programmatically and to develop applications that can access different data source type according customers requirements.
Summary
Being one of the most impressive enhancements in VFP 8, Cursor adapter gives the developers great opportunity to develop flexible reusable data classes. It is vigorous class that helps you easy and rapidly to develop data layer in your applications. Using CursorAdapter classes can significant cut down development resources and efforts when migrating data to different data storage.
(Group DATA)