A VFP-SQL Server Application From The Begining
A VFP-SQL Server Application From The Begining
A VFP-SQL Server Application From The Begining
Frederico Tomazetti
This article is the first of a series in which I will try to show you how to develop a Visual FoxPro system using
the SQL Server database. This article series is targeted to VFP developers who use free tables or tables in a VFP
DBC and who are trying to learn new development methodologies.
To this task I will use Erwin 4.0 as a modeling tool, SQL Server 2000 and Visual FoxPro 7.0.
Here I will make some comments about documentation, data modeling and the use of Erwin 4.0. In this first
part we will not concentrate in process and class modeling (UML), but just to model a small database with
customer, product and sales order maintenance. It will be a study-focused example for you to understand the
process as a whole.
Documentation
This topic is the weak point of most developers, because as we have all the process in our minds, we leave
documentation in a secondary place and, you can be sure, we are always hurt by this.
There are several arguments I could use to try convince you about documenting your system right, among them:
easier project comprehension by yourself or by another developer, process clarity, etc.
But I confess that the argument that convinced me about the need of documenting was: "If you don't document
your project you will lose time, and mainly, money".
Let me explain: When you develop a project for a customer and you don't detail what you'll do, you don't have
any control about what you already did and what's pending, and you'll never finish this project. Your customer
will always find a "little thing missing". Certainly, after everything is working, you have already heard
something like this:
Where is the module that integrates that with the accounting and payment systems?
If you did a proper documentation (and it is signed by your customer) it is enough to show them that this
module wasn't included in the project and if they want to build it, it would be another project, with its own
negotiation over values, timeframes, etc.
Have you seen how you lose time and money if you don't document your working process? I hope that this
argument works for you also.
The data modeling
When I was studying some theories about the modeling process with another developer he told me something
that maybe most of us feel: "I would need to learn something I don't know to make something I already know
how to do".
Actually, data modeling has the goal of creating the database with its tables, fields and relationships, concepts
that we all already know pretty well by practice. So, why should I do data modeling, why I don't just open SQL
Server and create the tables I need for once?
If the argument about documentation convinced you, you already have an answer for this question, but in the
case you are not convinced yet, try this: with the data model I can create the same data structure in another
database, like Oracle, MySQL, SyBase, etc. That means I won't need to start creating tables if my customer
decides some day to switch platforms or if I want to take advantage of the same database for another customer.
The modeling process
If you read a book about data modeling you will see that everything, or almost everything written there you
already do on your daily work. The modeling can be divided in three big parts:
Conceptual model:
In this phase a general gathering is made about the customer needs. Here you shouldn't worry about how you'll
solve it in your database, nor the tools to use. Just concentrate in understanding the process you'll have to
automate, talk with the future users, learn the working of each company's (customer) division, take notes and
follow the production processes. After that you'll have a prototype of what needs to be developed. The main
error in this phase is to start thinking about the tables that you'll create, and how many "IFS" or "CASES" the
system will have. Forget about this for now.
Logical model:
Now things start to have a more logical shape for us developers. Here we will specify which entities will be part
of our data model. For our example those entities will be: CLIENTE (customer), PRODUTO (product),
PEDIDO (order) and ITEM (it's a good practice to name always entities in singular) and what relationships exist
between them: "CUSTOMER puts ORDER" and "PRODUCT forms ORDER", or if you prefer "CUSTOMER
buys PRODUCT trough ORDER". In this model we will already define the main attributes of those entities, for
instance: CUSTOMER has: Code, name, address and phone. PRODUCT has Code, name, price and barcode,
etc. Here you detail how things happen; don't worry about how data will be stored, as theoretically you don't
even know yet in which database would be used. Leave the "IFS" and "CASES" for later.
Physical model:
Here you will finally define what entities and relationships will be transformed into tables, what fields will be
considered keys, the size and type of each field, what fields participate in the tables' relationships, and so on.
Here we are almost "at home". You learned something you didn't know to come to something you already
knew.
You can be asking yourself: Do I have to make all this separately? The answer: no. Erwin will make a good part
of the job, as after defining the logical model, the physical will be automatically generated, but from the
conceptual model you can't escape, as they have not invented yet an application that substitutes the human skills
for analyzing real world situations.
The Erwin 4.0 tool
It is not in the scope of this article to show all the features of this tool, as this would surely take a lot of text
pages. We will just work with the needed for our database to be modeled. But anyway, there is a tutorial that
explains very well the workings of Erwin. When opening Erwin, go to the HELP menu, TUTORIAL option...
ErWin Tutorial
Let's do a quick "tour" trough this tool to identify some basic items:
Look at the details circled in red
When we click NEW, a dialog opens asking about the major details about the model we will go create. Select
the logical/physical model and the SQL Server 2000 database, click Ok, and you'll open a totally blank work
area.
These are basically the tools we will need now
The "A" button is used to create our entities; the "B" set is used to define our relationships.
In this case the possible relationships are, respectively: 1 -> N with identified relationship, N -> N and 1 -> N
with unidentified relationship. Notice that the figure is formed by a line and a dot in one of the ends; the dot
means the "N" side of the relationship, and in the case of an N -> N relationship there is a dot in each end.
When we see this in practice, it would be easier to understand.
The "C" combobox indicates that we are working in the logical model. Later we will switch this combo to work
with the physical data model.
Click on the "Entity" tool and click
everywhere in the work area
Type the name of the entity and
press Enter
Here we have a detail important
enough
Notice that "attribute_name" is located in the entity as if it was a subtitle. That means that this attribute will
indicate our future table primary key.
It is good to point that we don't know yet "where" we will go to store our data; we're just defining "how" they
will be stored. We wouldn't worry about the field name, type (character, numeric, date, etc) or size. We can use
composite names without any problem.
Now we have to press "TAB", and thus we will go to the lower part of the entity
where we will place the remaining attributes. When you press "ENTER" again
you will go to place another attribute that will make part of the primary key of the
future table (you'll have in this case a composite key).
As this is not our case now, let's press "TAB".
Now let's enter the remaining attributes, always pressing "ENTER" to move to
the next line.
We can now complete our model with the remaining entities.
See how it will look
Look at the "Attribute_name"
These will be our entities (don't hesitate: save your file!)
The entities' relationships
After defining our entities and their attributes we will relate each one with the others, following our system's
logic.
Let's define that:
A CLIENTE
(customer) can put at
least a minimum of
zero (0) PEDIDOS
(orders) and several
PEDIDOS as
maximum.
A PEDIDO (order)
will have as a
minimum zero (0)
ITEM and at maximum
several ITEM.
An ITEM should have
at least 1 PRODUTO
(product) but a
PRODUTO can
participate in several
ITEM of several
PEDIDOS.
Notice that it is not needed to have a PEDIDO (order) for you to be able to enter a CLIENTE (customer), but
for you to put a PEDIDO, you need at least a PRODUTO entered.
Let's relate CLIENTE and PEDIDO. Click the tool for unidentified relationship (the one with the dashed line
and a final dot); click the CLIENTE entity and then the PEDIDO entity. See how it ends:
Look at the icon and the created relationship
Get another relationship with dashed lines
Let's relate PRODUTO with
ITEM.
We will use the same option:
Notice that the filled dot at the end
of the line always indicates the
"N" side of the relationship and
the empty dot indicates a non
dependency on the "1" side.
Most surely you already figured
that the other icon will use a
continuous line and that it
represents a mandatory
relationship.
And that way we will relate now
PRODUTO with ITEM:
Look that now the line is continuous and it also have an indication on the "N" side
Final considerations about the
Logical Model
Notice that when creating
relationships, the key field of
entity "1" is brought to the "N"
entity, and when the relation is
unidentified, this field is placed as
a normal attribute, but when we
use an identified relation
(PRODUTO X ITEM) the key on
side "1" will make part of the other entity identifier.
Notice also that the ITEM entity suffered and alteration on its format, now that its corners are rounded,
indicating that it belongs to an "N -> N" relationship between PEDIDO and PRODUTO (many orders have
many products).
Our logical model is created. In the next article we'll transform this model into the physical one, and we'll talk
about domains, database creation and reverse engineering.
A VFP/SQL SERVER application from start to end - Part II
Frederico Tomazetti
In the first article, we developed a small logical model, using the modelling tool ErWin 4.0, and I explained
about documentation and the process of data modelling.
We will now transform this logical model into a physical model, and generate a database in SQL Server. We
will define "how" our data is saved, what data types we will use for each field in our tables, which
relationships will have cascading constraints, etc.
Domains
This is a resource that will help us a lot maintaining our
database, since, through the definition of domains, we can
create customized data types, and, when we change a domain,
all related fields will inherit this change. For example: we detect
that there are several fields, in different tables, that store
monetary values. In this case, we can create a domain called
MonetaryValue, and assign it to type "Money", with 10 digits,
including 2 decimals. We will attribute this domain to all fields
that need to save monetary values, and if, in the future, it
becomes necessary to change these fields to work with 3 digits,
we only have to change our domain, and all fields that depend
on it will inherit this information.
That's all; now, you don't need to review your entire database,
for tables that have this field, to make this change one field at a
time.
Those who are accustomed to work with VFP tables will note
that the range of options for data types becomes much larger;
see below, in the two figures, the differences of data types
between native VFP and SQL Server:
Figure 2: Data types used by SQL Server
Figure 1: Native VFP Data Types
The Physical Model
Passing from the logical model to the physical model is fairly simple; you only have to change the
LOGICAL combobox to PHYSICAL, or use the shortcut keys Ctrl+downarrow. To go back to the logical
model, change the combobox, or use Ctrl-uparrow.
Figure 3: View of our physical model
Note that the modelling tool defined our data type with a template type. We will now define our domain and
assign this domain to the fields; we will also rename the fields, so that there is a logical relationship between
the fieldname and the name of the table.
There are several ways to do this, and I believe that every one will prefer one method or the other.
Personally, I will define the fieldnames in the manner which I find most practical, but you can use your own
way without any problem.
Analyzing the data which will be saved, we note that all tables have an identifier, CLIENT and PRODUCT
need character data type, and we will also need numerical and date fields.
We will create the following domains:
Domain Value
Identifier INT
String01 VarChar(60)
String02 VarChar(30)
Quantity Number(10,1)
MonetaryValue Money(10,2)
BarCode01 VarChar(13)
Date DateTime
Let's continue...
Make sure that sorting is hierarchically (Sort -> Hierarchialy).
Click on New ...
Figure 5: Data of the new Domain
Figure 6: As the next step, define the data type
Two points are of interest here:
The type VarChar stores only the data that is typed by the user, that is, you define the size as 60 characters,
and this will be the maximum field size. But if the user types 10 characters, only 10 characters are stored,
which saves us 50 characters.
Figure 8: All the domains are defined
If we extrapolate this to a database that has hundreds of tables and millions of records, the space saving can be
considerable.
The CHAR type is equivalent to the CHARACTER type in Visual FoxPro. This type doesn't save unused space,
that is, if you define char(60), all records of your table will have 60 spaces, even if the user only types 10
characters.
The other interesting point is the possibility of allowing, or not, null values in the fields. Since we will use this
domain to define the client and product name, we should not allow null data. Therefore, select the
corresponding option as NOT NULL, as shown in the figure.
Using these resources, define all the mentioned data types. In figure 8, you can see how our data types are
defined.
If this process isn't yet clear for you, investigate a little more about domains; they are a very useful resource,
and one that makes life for the developer easier. You will find, at the end of this article, a link to download the
modelling file that was used as an example for this article.
Let's go one step further...
We will define the names of the table fields. This can be done already in the logical model; I didn't do this, so
that the idea of a Logical Model remains well separated from the physical model (LOGICAL = what to save.
PHYSICAL = how to save it). At this point, these concepts should already be well defined, so that things start
to look more homogeneous.
Since we are in this process, let's define the domains for each field. See how it is done:
Figure 9: Double-click on the CLIENT table
Figure 11: Click on the GENERAL tab and select the domain IDENTIFIER
Continue defining the remaining fields as follows:
Field Name Domain
Client_Name CliName String01
Client_Telephone CliTelephone String02
Client_Adress CliAdress String01
The table CLIENT will look like this:
Define the remaining tables to match the following configuration:
Creating the Database
To create our database directly from the modelling tools, you have to have a conection to SQL Server. If you
want to create a file with the commands to create the database (a script) and then connect to the server and
create the database, this is also possible. Let's investigate this second option. Although it is a little more work,
you will have a better overview of the entire process:
If you have a conection with the database server, you will have, in your computer, a tool called QUERY
ANALYZER. Execute this application. It will ask for the name of the server and a password. Provide this
information, using, if possible, the SQL Server administrator account, and execute the following command:
Figure 14: After writing the command, type F5.
Now, let's go back to ErWin, and generate the script file to create our database. Go to the TOOLS menu and select the
option Forward Engineer/Schema generation ...
Figure 15: Click on Preview ...
Figure 16: On the next form, click on the button to save
I saved the file as UTMAG.SQL. It is attached to this article, together with the updated modelling file.
We now have to execute the script through the Query Analyzer. Go back to this tool, and open the file that you
just created. The Query Analyzer will ask you to save the command that you used previously to create the
database. It isn't necessary to save this command.
An important detail, before you execute the script UTMAG.SQL, is to make sure that the database that you just
created is in use. For this purpose, there is a combobox that shows which database is in use; normally, the
MASTER database is open, as a templated. Change this combobox to our database (UTMAG).
Figure 17: The file is open, check the detail in the combobox, and press F5
Below, you can see if there was any error executing the script commands.
Reverse Engineering
The process of reverse engineering consists in importing, into our modelling tool, an existing database. This
assists us in analyzing existing structures, and in correcting our own structure. Observe the reverse engineering
options in ErWin, mainly the options of comparing an existing database and the current model. The next
screenshot shows the comparison between the database we just created and our model. Since they are exactly
equal, there isn't much to do at this point.
In the next article, we will create a table with SQL Server, and, through this tool, import it into our modelling
tool. We will learn a little about the tool Enterprise Manager, and establish the conection between Visual
FoxPro and SQL Server.
In the second article, we created the domains, the physical model and the database. Now, we will create a
process that will help us learn a little about the Enterprise Manager, and about reverse engineering, between
SQL Server and Erwin. We will create a table in SQL Server and transfer it into Erwin.
We could do the opposite, that is, create a new entity in Erwin, and transfer it into our existing database. I will
show the first situation, since it is the most common one, when you take an existing database to analyze and
maintain it.
When I design a database, usually I create a table specifically to control the sequences of primary keys. I use a
unique sequence for all tables; I don't like to use the auto-numbering feature of SQL Server. Please note that
this is a personal preference, and I don't wish to claim that this is the best, or the worst, way to work. The
advantage that I see in this system is that there will never be a repeated value in a primary key, and as we know,
the primary key should not be shown to the user of the system.
When the user asks me to have a code for a client, or one to number an order, I create a counter specifically for
that table, using the auto-numbering system, but this is only for the user of the system; the real keys which
relate the tables are sequences, and not available for the end-user. This is safer.
The Enterprise Manager
Figure 1: This is the Enterprise Manager
Note our tables in folder "Tablas" (tables).
We will create a table called COUNTER that has a field called UniqueId of type "Identifier". This is quite
simple: click on folder tables, and then on the icon "NEW".
We will create a stored procedure that does the counting "plus one" for our table. This could be done through
VFP code, but we can also assign this function to the database, that is, start applying a little of the
"client/server" way of working.
Figure 5: The process is similar to creating the table:
Click on the folder Stored Procedure and click on the icon "NEW". In the dialog box, write the code below:
CREATE PROCEDURE ObtainNewId
AS
DECLARE @nnId int
BEGIN TRANSACTION
SELECT @nnId = UniqueID
FROM counter (HOLDLOCK)
IF NOT (@@rowcount = 1)
BEGIN
INSERT INTO counter (UniqueID) values (1)
select @nnId = 1
END
UPDATE counter
SET UniqueId = @nnId + 1
COMMIT TRANSACTION
SELECT @nnId as UniqueId
GO
Figure 6: Check the syntax clicking on "Check syntax", and click OK to confirm.
To verify whether the counter is really working, open the query analyzer and execute this command in our
database:
EXEC ObtainNewId
Transferring the changes to our model
See, now, how our screen, that compares the database with the model (last screenshot from the previous article)
looks now:
To transfer our changes to the model, click on the line that shows a difference, and click on the "Import" button.
Note that a yellow arrow will indicate what will be imported.Click on the "Next" button and ...
Figure 8: Click on "Start Import" ...
Figure 9:
Here is
the
procedur
e
for the
counter
and...
Figure 10: ... the
table COUNTER
Figure 11: Observe the window of the UDL file, and indicate the conection type.
Talking with Visual FoxPro
If you have always worked
only with Visual FoxPro,
what you saw here is quite
new, since VFP has its own
database that doesn't require
this sort of stuff.
However, we are working
with another database, much
more robust than the VFP
database, and the techniques
studied here will be useful to
work with any other database.
For Visual FoxPro to be able
to "talk" with SQL Server,
several methods exist, such as
ODBC/remote view,
OLEDB/ADO, and others.
For this article, I will use
OLEDB/ADO.
The connection between the
application and the database is
done through a connection string. To obtain this string
quickly, there is a little "trick" that consists in creating
a TXT file in any directory of your disk, renaming this
file with a UDL extension, and executing the file.
That's all, now confirm the data and edit the UDL file
with Windows Notepad, where you will find your
conection string ready for use. Finally, open Visual
FoxPro and create a PRG with the following code:
LOCAL lcStringCon as string, oCon as Custom
lcStringCon = "Provider=SQLOLEDB.1;"
+;
"Password=utmag;"
+;
"Persist Security Info=True;"
+;
"User ID=ftomazetti;"
+;
"Initial Catalog=UTMAG;"
+;
"Data Source=servidor"
oCon = CREATEOBJECT("adodb.connection")
oCon.open(lcStringCon)
Obviously, you must use the connection string
generated by your UDL file, which has the correct
password for your database.
You created an object called oCon that has a connection with the database.
There are other ways to connect with a database, which don't require hard-coding the password. For instance,
you can create a form where the user types the password, or you can use the connection which comes with
Windows; in this case, all machines involved have to have Windows 2000 (Server or Professional) or Windows
NT installed.
The integrated connection is, in my opinion, the best way to work, although it has the inconvenience that it
doesn't work with Windows 98 or Windows ME.
In the next article, we will see some data manipulation functions with OLEDB/ADO, and we will create our
connection objects and the access to the database.
In this article things can seem a bit confusing, because I will focus in some basic concepts about access to SQL
Server using OLEDB/ADO. To that purpose, I will show some functions and comment on every one of them.
These functions will be used later in the article sequence, being the basis for the upcoming "middle tier"
component of a Client/Server system.
Beginning
To access a database different from the VFP native one, we need a connection string, as we saw at the end of
the third article, as we generally need to configure the server name, database name, login type, etc.
To that purpose we will use an INI file that is just a text file (made with the note pad) that has some parameters,
and the routine bellow to read them.
The file UTMAG.INI has these lines:
[UTMAG]
servidor=SRVUTMAG
banco=UTMAG
autolog=S
The code bellow will be on the system's main PRG. This code creates properties for _SCREEN that are kept
visible throughout the system, avoiding public variable creation.
This way we can switch the server or database name without the need to recompile our system; it's just a matter
of editing the INI file.
The "Autolog" option will determine later on if we will use the Windows integrated login. This option is
available if you're using Windows NT (Windows 2000) on the server (2000 server) as well as on terminals
(2000 professional) as this version has a system login integrated with other applications. If you have Windows
95, 98 or Millennium, you will have to set "AUTOLOG=N" and during the system initialization the user will be
asked to enter his login name and password for SQL Server. We will see this later on.
This is the code to read data from UTMAG.INI:
_Screen.AddProperty("servidor",'')
_Screen.AddProperty("banco",'')
_Screen.AddProperty("integrado",'')
_Screen.AddProperty("usuario",'')
* Search for the data on UTMAG.INI - parameters
* 1 - Sections on square brackets [ARQUIVO]
* 2 - Variable into this section
* 3 - default value if not found
* 4 - variable that will receive the value (passed by reference)
* 5 - character length that the variable will receive
* 6 - .ini file
Declare Integer GetPrivateProfileString In Win32API As GetPrivStr ;
STRING cSection ,;
STRING cKey ,;
STRING cDefault ,;
STRING @cBuffer ,;
INTEGER nBufferSize ,;
STRING cINIFile
lcServidor = Space(20)
lcBanco = Space(20)
lcIntegrado = Space(10)
GetPrivStr('dt_sistemas', 'servidor' ,'vazio', @lcServidor , 20, 'outros\dt.ini')
GetPrivStr('dt_sistemas', 'banco' ,'vazio', @lcBanco , 20, 'outros\dt.ini')
GetPrivStr('dt_sistemas', 'autolog' ,'vazio', @lcIntegrado , 10, 'outros\dt.ini')
_Screen.servidor = Left(lcServidor, Len(Alltrim(lcServidor)) - 1)
_Screen.banco = Left(lcBanco, Len(Alltrim(lcBanco)) - 1)
_Screen.integrado= Left(lcIntegrado, Len(Alltrim(lcIntegrado)) - 1)
The small code snippet below stores the currently logged user name. This information will be used later to
confirm integrated login.
lcNome = Sys(0)
lnPosicao = At('#',lcNome) + 2
lnTamanho =Len(lcNome) - At('#',lcNome) -1
_Screen.usuario = Substr(lcNome,lnPosicao,lnTamanho)
Access to the database and business rules
The code bellow will create our data access class, which will considerably grow during the system
development. I will show just the basics to avoid complicating things by now, because when you start
transforming your system to Client/Server you will obviously use your existing structure.
Define Class UtmagDados As Relation OlePublic
lors = Null
loConn = Null
cMensError = ''
nNativeError = 0
Protected cStringSql,;
cServerName,;
cDataBaseName,;
cIntegrado ,;
cUserID,;
cPwd,cConnectionString,;
cCampos,cValor,cCamposSimples,cSqlWhere
Procedure Init (
pServidor As String, ;
pBanco As String, ;
pIntegrado As String, ;
pUsuario As String, ;
pSenha As String )
With This
.cIntegrado = Alltrim(pIntegrado)
.DefineAtributoConexao(pServidor, pBanco, pUsuario, pSenha)
.cStringSql = ""
.cConnectionString = ""
.loConn = Null
.lors = Null
Endwith
Endproc
Procedure Error
Lparameters nError, cMethod, nLine
If This.loConn.Errors.Count>0
This.cMensError = "Numero do Error: " + ;
Transform(This.loConn.Errors.Item(0).NativeError) + Chr(13) + ;
"Descricao Error: "+This.loConn.Errors.Item(0).Description + Chr(13) +;
"Origem do Error: "+This.loConn.Errors.Item(0).Source
Else
This.cMensError = "Numero do Error: "+Transform(nError) + Chr(13) +;
"Mensagem do Error: "+Message( ) + Chr(13) +;
"Linha do Error: "+Transform(nLine) + Chr(13) +;
"Metodo: "+cMethod
Endif
Endproc
Procedure Destroy
With This
If Vartype(.loConn) = "O"
.Desconectar()
Endif
Endwith
Endproc
*************************************************************
* Procedure: DefineAtributoConexao
* Objetivo: Initializes the attributes to connect to the database
* That data actually comes from a public object having
* the data to connect to the DB
*************************************************************
Procedure DefineAtributoConexao ( ;
pServidor As String, ;
pBanco As String, ;
pUsuario As String, ;
pSenha As String )
With This
.cServerName = Alltrim(pServidor)
.cDataBaseName = Alltrim(pBanco)
.cUserID = Alltrim(pUsuario)
.cPwd = Alltrim(pSenha)
Endwith
Endproc
The procedure bellow makes the connection with the database. Notice the condition: IF .cIntegrado = 'S' - Here
we will use or not the integrated Windows login.
*************************************************************
* Procedure: Conectar
* Objetivo: Makes the connection with the database trough ADO
*************************************************************
Procedure Conectar (pConnectionString As String ) As Boolean
Local llOK
With This
If Type("pConnectionString") = "C"
*-- The connection string came as a parameter
.cConnectionString = pConnectionString
Else
If .cIntegrado = 'S'
* The connection string comes from Windows 2000 login
.cConnectionString = 'Provider=SQLOLEDB.1' +;
';Integrated Security=SSPI' +;
';Persist Security Info=False' +;
';Initial Catalog=' + .cDataBaseName +;
';Data Source=' + .cServerName
Else
* string para conexo de segurana mista
* exige usurio digitar login e senha
.cConnectionString = 'Provider=SQLOLEDB.1' +;
';Data Source=' + .cServerName +;
';trusted_connection=false;' +;
';Initial Catalog=' + .cDataBaseName +;
';User ID=' + .cUserID +;
';PassWord=' + .cPwd
Endif
Endif
.loConn = Createobject("adodb.connection")
If Vartype(.loConn) = "O"
.loConn.Open(.cConnectionString)
If .loConn.State = 1 && Conexao Aberta
llOK = .T.
Else
llOK = .F.
Endif
Endif
Endwith
Return llOK
Endproc
This procedure disconnects the system from the database:
*************************************************************
* Procedure: Desconectar
* Objetivo: Closes the ADO connection to the database
*************************************************************
Procedure Desconectar
With This
If Vartype(.lors) = "O"
If .lors.State # 0
.lors.Close()
Endif
Endif
If Vartype(.loConn) = "O"
If .loConn.State # 0
.loConn.Close()
Endif
Endif
.loConn = Null
.lors = Null
Endwith
Endproc
The following two procedures create and close the database connection, as it is not needed to work permanently
connected. In this way we can maximize database access. Imagine a terminal that's used just every 30 minutes.
There is no need to have a permanent connection between this terminal and the database.
It is an economic issue, as your customer purchase an X amount of database access licenses, and you can only
use this amount. This way we can multiply the access level with the same number of licenses.
*************************************************************
* Procedure: CriaConexao
* Objetivo: Creates a public connection that would be shared
* by all forms.
**************************************************************
Procedure CriaConexao
_Screen.omanipuladados = Createobject("ManipulaDados", _Screen.servidor,;
_screen.banco,;
_screen.integrado,;
_screen.usuario,;
_screen.senha)
*-- Estabeleca a conexao com o BD
_Screen.omanipuladados.conectar()
Endproc
*************************************************************
* Procedure: FechaConexao
* Objetivo: Closes a public connection
**************************************************************
Procedure FechaConexao
If Vartype(_Screen.omanipuladados) = "O"
_Screen.omanipuladados.Desconectar()
_Screen.omanipuladados = Null
Endif
Endproc
The following three procedures will be used for transactions. Who already used a DBC on Fox would be
already familiar with transactions (BEGIN TRANSACTION, END TRANSACTION, ROLLBACK, etc).
*************************************************************
* Procedure: IniciarTransacao
* Objetivo: Opens a transaction trough ADO
*************************************************************
Procedure IniciarTransacao
This.loConn.BeginTrans
Endproc
*************************************************************
* Procedure: EncerrarTransacao
* Objetivo: Commits a transaction trough ADO
*************************************************************
Procedure EncerrarTransacao
This.loConn.CommitTrans
Endproc
*************************************************************
* Procedure: AbortarTransacao
* Objetivo: Rolls back a transaction trough ADO
*************************************************************
Procedure AbortarTransacao
This.loConn.RollBackTrans
Endproc
These las two procedures will be used to execute a Stored Procedure that returns a sequential counter from our
CONTADOR table on the database.
*************************************************************
* Procedure: ObterNovoContador
* Objetivo: Returns a unique counter according to a stored procedure
*************************************************************
Procedure ObterNovoContador As Integer
*-- Obtem o ID_UNICO do Sistema
Return This.executarSP("obternovooid",0,"",1,"@nRetorno")
Endproc
*************************************************************
* Procedure: ExecutarSP
* Objetivo: Function to execute database stored procedures
* Parametros: pNomeDaSP - Store procedure name to execute
* pNroParEnt - input parameter counter
* pParEntrada - output parameters (beginning with @ and comma-separated)
* pNroParSai - output parameter counter - can be just 1 or 0
* pParSaida - output parameter beginning with @
*************************************************************
Procedure executarSP As Custom
Parameters pNomeDaSP, pNroParEnt, pParEntrada, pNroParSai, pParSaida
* declarao de variveis locais
Local loADOCmd, loADOParam && objetos
Local adInteger, adCurrency, adDate ,; && uso do ADO
adBoolean, adChar, adNumeric, adVarChar, AdParamInput ,;
adParamOutPut, adCmdStoredProc, AdExecuteNoRecords
Local lnTamanhoString, laEntradas, laSaidas, lnElementoMatriz, ;
lcGuardarString && uso interno da funo
Local lnRetorno,i,llConectou
* valores utilizado pelo ADO
adInteger = 3
adCurrency = 6
adDate = 7
adBoolean = 11
adChar = 129
adNumeric = 131
adVarChar = 200
AdParamInput = 1
adParamOutPut = 2
adCmdStoredProc = 4
AdExecuteNoRecords = 128
If Vartype(This.loConn) # "O"
*-- Se nao houver conexao estabelece a conexao
If !This.Conectar()
Return .F.
Else
llConectou = .T.
Endif
Endif
loADOCmd = Createobject("ADODB.Command")
loADOCmd.ActiveConnection = This.cConnectionString
loADOCmd.CommandText = pNomeDaSP
loADOCmd.CommandType = adCmdStoredProc
* criar parametros de entrada
If pNroParEnt > 0 && monta um array com os parametros de entrada
lnTamanhoString = Len(pParEntrada)
Dimension laEntradas(pNroParEnt)
lcGuardarString = ''
lnElementoMatriz = 1
For i = 1 To lnTamanhoString
If Substr(pParEntrada,i,1) = ','
laEntradas(lnElementoMatriz) = lcGuardarString
lnElementoMatriz = lnElementoMatriz + 1
lcGuardarString = ''
Else
lcGuardarString = lcGuardarString + Substr(pParEntrada,i,1)
Endif
Next
laEntradas(lnElementoMatriz) = lcGuardarString
For i = 1 To pNroParEnt
loADOParam = loADOCmd.CreateParameter(laEntradas(i), adVarChar, AdParamInput)
loADOCmd.Parameters.Append(loADOParam)
Next
Endif
* criar parametros de sada (retorno da stored procedure)
If pNroParSai > 0
lnTamanhoString = Len(pParSaida) && monta um array com os parametros de saida
Dimension laSaidas( pNroParSai)
lcGuardarString = ''
lnElementoMatriz = 1
For i = 1 To lnTamanhoString
If Substr(pParSaida,i,1) = ','
laSaidas(lnElementoMatriz) = lcGuardarString
lnElementoMatriz = lnElementoMatriz + 1
lcGuardarString = ''
Else
lcGuardarString = lcGuardarString + Substr(pParSaida,i,1)
Endif
Next
laSaidas(lnElementoMatriz) = lcGuardarString
For i = 1 To pNroParSai
loADOParam = loADOCmd.CreateParameter(pParSaida, adInteger, adParamOutPut)
loADOCmd.Parameters.Append(loADOParam)
Next
Endif
loADOCmd.Execute(,,AdExecuteNoRecords)
lnRetorno = loADOCmd.Parameters(pParSaida).Value
loADOCmd = Null
If llConectou
This.Desconectar()
Endif
If pNroParSai = 1
Return lnRetorno
Else
Return -1
Endif
Endproc
EndDefine
Things may seem a bit out of place at this moment, but these procedures (and some less relevant ones that we
will see on our work sequence) are the ones that make all the data access and manipulation work.
It is fundamental to learn ADO. For that I recommend the articles on MSDN on the subject and a book that
taught me a lot called "Dominando SQL SERVER 2000 - A Biblia".
Another book that can really help, specially for the fifth part of this article, is "Desenvolvendo solues XML
com VISUAL FOXPRO" from our colleague Fbio Vazquez.
(Note: Both are Portuguese editions.)
Finally I wish to thank Breno Viana, a fellow UT member and coworker of mine, as many of the things in the
functions presented come from him.
The fifth part of this article is dedicated to the study of ADO within Visual FoxPro. The functions I will now
present are a continuation of those presented in part IV. They are functions of vital importance for the future of
the project; a failure at this point will endanger the entire system.
When I started the project "A VFP/SQL Server application from start to end", VFP 8.0 was not yet officialy
launched (it was in the beta version), therefore, the functions presented here were developed, tested, and are
being used, in version 7.0.
We know that in version 8.0 there are ready-made functions that make life easier for the developer who wants to
work with Visual FoxPro and SQL Server.
Executing Transact/Sql Commands
The function below executes a SQL statement in the database.
The main point is the parameters received by the function. The first "pStringSql" is the string that will be
executed, something like: "Select * from clientes".
The second parameter (pCursorLocation) indicates on which side the cursor will be created, the client or the
server. This has a great relevance on the performance of your system and the traffic it causes on the network.
We can't claim that the client-side cursor is better than the server-side cursor, or the other way round; rather,
this depends on the structure of our system, and on whether the user user has an overloaded server, and on
whether the client machines have fast or slow processors. That is, external factors will influence this type of
decision.
In my case, I use client-side cursors when the amount of data returned isn't very great, and server-side cursor for
a major amount of data, or when executing an INSERT, UPDATE or DELETE on a table.
The third parameter (pCursorType) indicates how the cursor is opened, and has 4 options (values from 0 to 3).
The most common option is the value zero (default), which defines the cursor as being opened "forward only".
If you use option 3, for instance, you can only read data. Apparently, option 2 (adOpenDynamic) would be the
best, but this consumes processing power, and we only need to read data sequentially.
The fourth parameter (pLockType) indicates the type of locking that ADO executes on the record or table of the
database. This process is practically the same as the one used natively in Visual FoxPro tables.
Normally I use the standard locking option (1 - adLockReadOnly), since it is safer than the others.
*************************************************************
* Procedure: Execute
* Purpose: Execute a SQL command in the database, through ADO
* Parmetros: pStringSQL - string that will be executed in the database
* pCursorLocation - Cursor location
* 2 = adUseServer (Default)
* 3 = adUseClient
* pCursorType - How the cursor will be opened
* 0 = adOpenForwardOnly (Default)
* 1 = adOpenKeyset
* 2 = adOpenDynamic
* 3 = adOpenStatic
* pLocktype - Locking type
* 1 = adLockReadOnly (Default)
* 2 = adLockPessimistic
* 3 = adLockOptimistic
* 4 = adLockBatchOptimistic
* Return value: True or False
*************************************************************
Procedure Executar (pStringSql As String,;
pCursorLocation As Integer,;
pCursorType As Integer,;
pLocktype As Integer) As Boolean
This part of the function verifies data typing and, in case some parameter wasn't specified, it assumes a default
value for it.
With This
If Vartype(pStringSql)<> "C"
Return .F.
Endif
If Vartype(.loConn) # "O"
*-- If there is no conection, establish the conection
If !.Conectar()
Return .F.
Endif
Endif
*-- RS location type
If Vartype(pCursorLocation) <> "N"
pCursorLocation = 2
Else
If !(pCursorLocation >= 2 And pCursorLocation <= 3)
pCursorLocation = 2
Endif
Endif
*-- Cursor type
If Vartype(pCursorType) <> "N"
pCursorType = 0
Else
If !(pCursorType >= 0 And pCursorType <= 3)
pCursorType = 0
Endif
Endif
*-- Locking type
If Vartype(pLocktype) <> "N"
pLocktype = 1
Else
If !(pLocktype >= 1 And pLocktype <= 4)
pLocktype = 1
Endif
Endif
If Vartype(.lors) # "O"
*-- Only create the RS object if it doesn't exist
.lors = Createobject("adodb.recordset")
Else
If .lors.State = 1
.lors.Close
Endif
Endif
At this point, the parameters feed the ADO connection and the object is created with the data that results from
the query.
With .lors
.CursorLocation = pCursorLocation
.CursorType = pCursorType
.LockType = pLocktype
.Open(pStringSql,This.loConn)
Endwith
ENDWITH
Here we have the error handling, in case there is some problem with the conection, or with the string to be
executed.
If This.loConn.Errors.Count>0
This.nNativeError = This.loConn.Errors.Item(0).NativeError
This.cMensError = "Error number: "+;
Transform(This.loConn.Errors.Item(0).NativeError) + Chr(13) +;
"Error description: "+This.loConn.Errors.Item(0).Description + Chr(13) +;
"Origin of Error: "+This.loConn.Errors.Item(0).Source
Endif
Return Iif(This.loConn.Errors.Count>0,.F.,.T.)
Endproc
Saving the RecordSet
This little function saves the RecordSet in an XML file or in an ADO structure. We will use 100% XML files,
the ADO structure was used only for compatibility reasons.
*************************************************************
* Procedure: SalvarRS (SaveRS)
* Purpose: Save the RecordSet in an XML file or ADO structure
* Parameters: pTipoDado (Data Type): 1 - save as XML / 0 - Save in ADO format
* pNome (pName) - name of the temporary file
*************************************************************
Procedure SalvarRS (pTipoADO As Integer,pNome As String) As Boolean
Local llOK
If Vartype(pNome) <> "C"
pNome = "temp\" + "ADO.TXT"
Endif
If File(pNome)
Delete File &pNome
Endif
With This
If Vartype(.lors) = "O"
If .lors.State = 1
.lors.Save (pNome,pTipoADO)
llOK = .T.
Endif
Endif
Endwith
Return llOK
Endproc
Creating a local cursor
To create a local Visual FoxPro cursor with the data we received from SQL Server, we have to use XML. See
the function below, which uses the two functions explained above.
The function receives a string (pSql), a name for a temporary file - local cursor (pArqTmp), the cursor type
(pTipoCursor) and whether this cursor will receive a blank record after being created (very useful for data entry
through grids).
Here we should emphasize that it is necessary to have service pack 1 of Visual FoxPro 7 (available as a
download from the Microsoft site), since this service pack corrects a problem with the XMLTOCURSOR
command.
*************************************************************
* Procedure: CriaCursor (create cursor)
* Purpose: Create a local VFP cursor through an ADO RecordSet
* Parameters: pSql = SQL command to execute
* pArqTMP = Name of TMP file created
* pTipoCursor = Flag that identifies the type of cursor created
* (.T. = 512 , .F. = 512 + 8192)
* pInsereRegistro = Insert a blanc record after creating the cursor
*************************************************************
parameters pSql,;
pArqTMP,;
pTipoCursor,;
pInsereRegistro
Local llRetorno
Local lcXML
If _Screen.omanipuladados.Executar(pSql)
lcXML = "temp\"+Sys(2015)+".xml"
If _Screen.omanipuladados.SalvarRS(1,lcXML)
Xmltocursor(lcXML,pArqTMP,Iif(!pTipoCursor,512 + 8192,512))
Delete File &lcXML
If !pInsereRegistro
If Reccount() = 0
Append Blank
Endif
Endif
llRetorno = .T.
Endif
Else
=RotinaDeErro (_Screen.omanipuladados.cMensError + Chr(13)+Chr(13) ;
+"Failure calling view!" ,'', _Screen.omanipuladados.nNativeError, .T.)
Endif
Return llRetorno
In this sixth and last article we will create the forms to save data, create local cursors to query, etc.
Since this moment, you should be able to adapt the data access and data query classes that you already have
built and in use from many time ago. To this purpose, I'll create the forms in the most simple way I can. I will
avoid using a form class and I will leave the method code for doing queries and everything else highly visible.
To keep all as didactic as possible, I didn't placed complex data validation rules. Take a look at the code inside
each form and use the ideas there to the classes the you are using already in your daily work.
You will need to build the example with the code included in the previous articles, as the forms provided in this
one makes reference to the previously presentes functions.
Some important issues that you have to keep in mind when adapting your own classes which are accessing free
tables today, using VFP native data, are:
1. You are developing an application which will be able to work with huge data volumes.
2. Many users will be able to access, update or insert data, and they will be able to do this trough this VFP-
built application or trough any other application written with .NET, VB, ASP, etc.
3. Don't use methods that bring all the data in a table, mainly if this table have the chance to have
thousands of records. That means that you will have to avoid the infamous "NEXT", "PREVIOUS",
"FIRST" and "LAST" buttons, as they work accessing data all the time.
4. You should have to get accustomed to use transactions instead of LOCK/UNLOCK.
5. Create views on SQL Server to make easier report creation, as views are SQL instructions that work as
tables, but are a lot faster than executing a complex SQL sentence trough ADO.
6. When the need arise to create a code block with INSERTs, UPDATEs or DELETEs for several records
and different tables (for example, inserting a sales order and all its items), try to concatenate the strings
with all the commands and send everything just once to the database, so you execute several related
commands in a single database access.
Bellow are some of the functions used inside the forms. These routines are fundamental for the process, so we
will analyze them mor carefully:
Properties used in the forms
THISFORM.ACAO - Receives 1 for insert and 2 for update.
THISFORM.ID_UNICO - Receives the record ID to process.
The object _SCREEN.OMANIPULADADOS is instantiated when executing the application and have the
ADO objects used to manipulate data.
Code to insert or update data
Quite simple. Given the action parameter, (THISFORM.ACAO) we send an insert or an update and execute the
string at the database.
LOCAL lcString as String
SET TEXTMERGE on
WITH _screen.omanipuladados
IF thisform.acao = 1 && insert
thisform.id_unico = .ObterNovoContador()
TEXT TO lcString noshow
INSERT INTO PRODUTO (ProdID,
ProdNome,
ProdPrecoUnit,
ProdCodBarra)
values ( <<TRANSFORM(thisform.id_unico)>>,
<<thisform.txtnome.Value>>,
<<thisform.txtpreco.Value>>,
<<thisform.txtBarras.Value>>)
ENDTEXT
ELSE && alterao
TEXT TO lcString noshow
UPDATE PRODUTO
set ProdNome = <<thisform.txtnome.Value>>,
ProdPrecoUnit = <<thisform.txtpreco.Value>>,
ProdCodBarra = <<thisform.txtBarras.Value>>
where CliID = <<TRANSFORM(thisform.id_unico)>>
ENDTEXT
ENDIF
* Execute the string on the database
.IniciarTransacao()
.executar(lcString,3)
IF .lors.errorcount = 0
.EncerrarTransacao
ELSE
.AbortarTransacao
ENDIF
ENDWITH
Code to search for data (look at the product maintenance form)
In this routine we have a SELECT that searches for the product data. After executing that command we check if
the recordset has any records. If it is zero, we tell that to the user and return FALSE.
As the user is performing a search, we take the chance to switch the AO property to 2 and then we get the
data from the recordset and load it to the form fields.
LOCAL lcBusca as String. lcString as String
lcBusca = Inputbox("Name:","Search")
IF EMPTY(lcBusca)
RETURN .f.
ENDIF
lcBusca = ALLTRIM(lcBusca) + "%"
SET TEXTMERGE on
TEXT TO lcString noshow
SELECT ProdID, ProdNome, ProdPrecoUnit, ProdCodBarra
from PRODUTO
where ProdNome like <<lcBusca>>
endtext
SET TEXTMERGE off
WITH _screen.omanipuladados
.executar(lcString,3)
IF .lors.recordcount <= 0
MESSAGEBOX("Nenhum registro encontrado")
RETURN .f.
ENDIF
thisform.acao = 2 && alterar
thisform.id_unico = .lors.fields("ProdID").value
thisform.txtnome.Value = .lors.fields("ProdNome").value
thisform.txtpreco.Value = .lors.fields("ProdPrecoUnit").value
thisform.txtBarras.Value = .lors.fields("ProdCodBarra").value
ENDWITH
The one-to-many form
After the form's Init we create a local cursor that would help us with the sales order items (the MANY side).
Inserting data in a one to many relatioship is performed after all the sales order items are entered (on a grid). We
do that with an insert for the PEDIDO table and then, in a SCAN...ENDSCAN we place the item table's
INSERTs into the same variable. Notice that there is a blank line after and before each insert command; this is
needed to leave a separation between the commands at execution time.
Consider that the string is executed just at the end of the process. This way, if any problem arises during the
data updating process (look at the line IF LORS.ERRORCOUNT = 0), the transaction will be rolled-back. If we
save the data as the user is entering it, we will face the risk of leaving an open transaction for too much time,
beside significatively increasing netwrok traffic.
* Important - The lcString variable will receive several concatenations.
* That's why there is a blank space after and before
* each insert or update command (See TEXT / ENDTEXT)
LOCAL lcString as String
SET TEXTMERGE on
WITH _screen.omanipuladados
IF thisform.acao = 1 && inclusao
thisform.id_unico = .ObterNovoContador()
TEXT TO lcString noshow
INSERT INTO PEDIDO ( PedID,
PedData,
PedTotal,
CliID)
values ( <<TRANSFORM(thisform.id_unico)>>,
<<thisform.txtdata.Value>>,
<<thisform.txttotal.Value>>,
<<thisform.id_cliente>>)
ENDTEXT
* Itens do pedido
SELECT itens
GO top
SCAN
lnContador = .ObterNovoContador()
TEXT TO lcString NOSHOW additive
INSERT INTO ITEM (ItemID,
ItemQuant,
ItemValVenda,
PedID,
ProdID)
values (<<TRANSFORM(lnContador)>>,
<<TRANSFORM(itens.Quantidade)>>,
<<TRANSFORM(itens.valUnitario)>>,
<<TRANSFORM(thisform.id_unico)>>,
<<TRANSFORM(itens.IdProduto)>>)
ENDTEXT
ENDSCAN
ELSE && update
SELECT itens
GO top
SCAN
TEXT TO lcString NOSHOW additive
UPDATE ITEM SET ItemQuant = <<TRANSFORM(itens.quantidade)>>,
ItemValVenda = <<TRANSFORM(itens.valunitario)>>
where PedID = TRANSFORM(thisform.id_unico)
and ProdID = TRANSFORM(itens.IdProduto)
ENDTEXT
ENDSCAN
ENDIF
* Execute the string on the database
.IniciarTransacao()
.executar(lcString,3)
IF .lors.errorcount = 0
.EncerrarTransacao
ELSE
.AbortarTransacao
ENDIF
ENDWITH
Function to search sales order data
I left this function incomplete on purpose, so if you analyze it deeper, you'll find that it returns the first sales
order from a customer. In this case you will need to work with the return value of the first select so you give the
user the chance to select which order to update.
This routine is a bit long and place it now in this function could affect the idea of sowing a basic operation for
search and save data.
After that we have a SELECT statement that will search for the data on the database and trough a DO...WHILE
loop over the recordset, we load them on a supporting cursor for the grid.
LOCAL lcBusca as String. lcString as String, lnIdCliente
* Buscar o cliente
lcBusca = Inputbox("Customer name:","Search")
IF EMPTY(lcBusca)
RETURN .f.
ENDIF
lcBusca = ALLTRIM(lcBusca) + "%"
SET TEXTMERGE on
TEXT TO lcString noshow
SELECT CliID
from CLIENTE
where CliNome like <<lcBusca>>
endtext
SET TEXTMERGE off
WITH _screen.omanipuladados
.executar(lcString,3)
IF .lors.recordcount <= 0
MESSAGEBOX("No records found")
RETURN .f.
ENDIF
* buscar o pedido
.lors.movefirst && move the recordset to the begining
lnIdCliente = .lors.fields("CliID").value
SET TEXTMERGE on
TEXT TO lcString noshow
SELECT PE.PedID, PE.PedData, PE.PedTotal,IT.ItemQuant, IT.ItemValVenda,
PRO.ProdNome
FROM PEDIDO PE, ITEM IT, PRODUTO PRO
WHERE PE.PedID = IT.PedID
and PRO.ProdID = IT.ProdID
and PE.CliID = <<TRANSFORM(lnIdCliente)>>
ENDTEXT
.executar(lcString,3)
IF .lors.recordcount <= 0
MESSAGEBOX("No records found")
RETURN .f.
ENDIF
.lors.movefirst && move recordset to the begining
thisform.id_unico = .lors.fields("PedID").value
thisform.txtdata.Value = .lors.fields("PedData").value
thisform.txttotal.Value = .lors.fields("PedTotal").value
* Feed the grid
SELECT itens
ZAP
DO WHILE !.lors.eof()
INSERT INTO itens (idProduto, NomeProduto, Quantidade, ValUnitario);
values (.lors.fields("ProdID").value,;
.lors.fields("ProdNome").value,;
.lors.fields("ItemQuant").value,;
.lors.fields("ItemValVenda").value)
.lors.movenext
ENDDO
thisform.gridProdutos.Refresh
ENDWITH
I hope you have now an overview about how it works an application developed with Visual FoxPro accssing
SQL Server trough an OLEDB/ADO connection. I'd like to receive some feedback from you who followed this
article series. I'd like to know what do you achieved based on it and, what you didn't.
It was really a pleasure to write this series, and I'm now thinking on another topic for an upcoming one, so any
suggestion is welcome. Thanks to all of you.