Delphi 3 - A Technical View of Delphi Enterprise and Entera - Part 3

By: Charles Calvert

Abstract: Delphi Enterprise uses Borland's middleware product called Entera to allow programmers to create scalable cross platform business solutions.

A Technical View of Delphi Enterprise and Entera
Part III
Charlie Calvert
Developer Relations Manager for Delphi and C++Builder

NOTE: The views and information expressed in this document represent those of its author(s) who is solely responsible for its content. Borland does not make or give any representation or warranty with respect such content.

Summary

In this paper you have seen a brief overview of the key facts you need to know to get up and running with Delphi Enterprise and Entera. Once you have mastered the materials in this paper then you can create your robust three tier applications.

Distributed computing is at the forefront of computer programming technologies at the time I write this article. It is a technology designed to allow you to create robust applications that can meet the needs of thousands, or even tens of thousands, of users. Over the next few years, most of our computing will probably move away from isolated single machines, and away from fat client applications. The technology that will make this change possible is distributed computing, and this paper has shown how you can begin using this technology in your own corporation, organization or business.

Appendix A: Sample SQL Server Scripts (Blob Types)

Listing 18: A Server script

select_industry:
SELECT 

        IND_CODE ,
        IND_NAME ,
        LONG_NAME
FROM 
        Industry;

delete_industry:
delete from
	Industry
where
	IND_CODE = $IND_CODE;


insert_industry:
insert into
Industry(
	IND_CODE ,
 	IND_NAME ,
 	LONG_NAME
       )
Values (
        $IND_CODE,
        '$IND_NAME',
        '$LONG_NAME'
       );

update_industry:
update
	industry
set
	IND_CODE = 	$IND_CODE,
	IND_NAME = 	'$IND_NAME',
 	LONG_NAME = 	'$LONG_NAME'
where                            
        IND_CODE = $IND_CODE;

Listing 19: This example shows how to work with blob fields other exotic types

[uuid(b0198e90-ef02-11d0-9313-0020aff5e6be), version(1.1)]
interface test
{

adhoc:
  ALTER SESSION SET NLS_DATE_FORMAT = 'MM/YY/DD HH24';

DbaseSelect:
  select NAME from ANIMALS;

IBGetNonBlob:
select KEYFLD, CHARFLD, DATEFLD, DECIMALFLD, DOUBLEFLD, FLOATFLD, 
          INTFLD,  LONGFLD, NUMERICFLD, SHORTFLD, VARCHARFLD, CHARVARFLD 
from ALLTYPE1;

IBUpdateNonBlob:
update ALLTYPE1 
set KEYFLD = $KEYFLD, CHARFLD = $CHARFLD
where KEYFLD = $OLD_KEYFLD and CHARFLD = '$OLD_CHARFLD';

IBDeleteNonBlob:
delete from ALLTYPE1 
where KEYFLD = $KEYFLD and CHARFLD = '$CHARFLD';

IBInsertNonBlob:
insert into ALLTYPE1(KEYFLD, CHARFLD) 
values ($KEYFLD, '$CHARFLD');

IBGetBlob:
select KEYFLD, [binary]MEMOFLD, [binary]GRAPHICFLD from ALLTYPE1;

ORGetNonBlob:
select KEYFLD, NUMBER82FLD, NUMBER104FLD, CHARFLD, 
       DATEFLD, VARCHARFLD, VARCHAR2FLD from ALLTYPE1;

ORGetNonBlob1:
select KEYFLD, NUMBER82FLD, NUMBER104FLD, CHARFLD, 
       DATEFLD, RAWFLD, VARCHARFLD, VARCHAR2FLD from ALLTYPE1;

ORGetLONGBlob:
select KEYFLD, [binary]LONGFLD from ALLTYPE1;

ORGetRAWBlob:
select KEYFLD, [binary]RAWFLD from ALLTYPE1;

ORGetVARCHAR2Blob:
select KEYFLD, [binary]VARCHAR2FLD from PBLOB;

ORGetAll:
select KEYFLD, NUMBER82FLD, NUMBER104FLD, CHARFLD, 
       DATEFLD, LONGFLD, RAWFLD, VARCHARFLD, VARCHAR2FLD from ALLTYPE1;

ORUpdateNonBlob:
update ALLTYPE1
  set KEYFLD = $KEYFLD, CHARFLD = '$CHARFLD' 
  where KEYFLD = $OLD_KEYFLD and CHARFLD = '$OLD_CHARFLD';

ORInsertNonBlob:
insert into ALLTYPE1(KEYFLD, CHARFLD) 
  values ($KEYFLD, '$CHARFLD');

ORDeleteNonBlob:
delete from ALLTYPE1
  where KEYFLD = $OLD_KEYFLD and CHARFLD = '$OLD_CHARFLD';

MSGetNonBlob:
select KEYFLD, BITFLD, CHARFLD, DATETIMEFLD, DECIMALFLD, 
       FLOATFLD, INTFLD, MONEYFLD, NUMERICFLD, REALFLD, 
       SMALLDATETIMEFLD, SMALLINTFLD, SMALLMONEYFLD,
       TINYINTFLD, VARCHARFLD, CHARVARFLD from ALLTYPE1;

MSUpdateNonBlob:
update ALLTYPE1
  set KEYFLD = $KEYFLD, CHARFLD = $CHARFLD, DATETIMEFLD = 
'$DATETIMEFLD'
  where KEYFLD = $OLD_KEYFLD and CHARFLD = '$OLD_CHARFLD' and 
DATETIMEFLD = '$OLD_DATETIMEFLD';

MSDeleteNonBlob:
delete from ALLTYPE1
  where KEYFLD = $OLD_KEYFLD and CHARFLD = '$OLD_CHARFLD' and FLOATFLD 
= $OLD_FLOATFLD and DATETIMEFLD = '$OLD_DATETIMEFLD';

MSInsertNonBlob:
insert into ALLTYPE1(KEYFLD, CHARFLD, FLOATFLD, DATETIMEFLD) 
  values ($KEYFLD, '$CHARFLD', $FLOATFLD, '$DATETIMEFLD');

MSGetTEXTBinary:
select KEYFLD, TEXTFLD from ALLTYPE1;

MSGetTEXTChar:
select KEYFLD, [char(100)]TEXTFLD from ALLTYPE1;

MSGetIMAGEBlob:
select KEYFLD, [binary]IMAGEFLD from ALLTYPE1;

MSGetBINARYBlob:
select KEYFLD, [binary]BINARYFLD from ALLTYPE1;

MSGetAllBlob:
select KEYFLD, [binary]IMAGEFLD, TEXTFLD from ALLTYPE1;

MSGetAll:
select KEYFLD, BITFLD, CHARFLD, DATETIMEFLD, DECIMALFLD, 
       FLOATFLD, INTFLD, MONEYFLD, NUMERICFLD, REALFLD, 
       SMALLDATETIMEFLD, SMALLINTFLD, SMALLMONEYFLD,
       TINYINTFLD, VARCHARFLD, CHARVARFLD, 
       BINARYFLD, IMAGEFLD, TEXTFLD, VARBINARYFLD from ALLTYPE1;
  

MSGetLotData:
select i, v from LotMoreData;

getstar:
select * from EDATA;

get_edata:
select [int]ID, STRFIELD, [double]FLOATFIELD from EDATA;

get_raw_edata:
select ID, STRFIELD, FLOATFIELD from EDATA;

get_id_edata:
select [int]ID, STRFIELD, [double]FLOATFIELD from EDATA
where
  ID = $INPUT_ID[int];

update_edata:
update EDATA
  set ID = $ID[int], STRFIELD = '$STRFIELD', FLOATFIELD = 
$FLOATFIELD[double]
where
  ID = $OLD_ID[int] and STRFIELD = '$OLD_STRFIELD' and FLOATFIELD = 
$OLD_FLOATFIELD[double];

update_NV_edata:
update EDATA
  set ID = $ID[int]
where
  ID = $OLD_ID[int];

insert_edata:
insert into EDATA
  (ID, STRFIELD, FLOATFIELD)
values
  ($ID[int], '$STRFIELD', $FLOATFIELD[double]);

get_mssql65:
select stor_id from dbo.stores;

delete_edata:
delete from EDATA
where
  ID = $OLD_ID and STRFIELD = '$OLD_STRFIELD' and FLOATFIELD = $OLD_FLOATFIELD;

}

Appendix B: A C++Builder Functionality Server

This appendix includes the code for a DCE based functionality server built in C++Builder. To create the program, first build a standard server from the script shown in Listing 26. Then use the source found in Listing 27 to create a second executable, called Func.exe. This second program is the functionality server your client will actually talk to. More detailed descriptions of these steps are presented in the next few paragraphs.

The functionality server you will create will generate what Delphi programmers call calculated fields. In particular, it will calculate from an existing row of the database two new fields called AmountOwed and AmountPaid. The source code that makes these calculations is found in the file called func.cpp.

You will find this example on Disc in a directory called Func. Thanks for James Ferguson for providing this example, and for the help he gave me throughout this paper.

A future paper will discuss issues involving functionality servers in more depth.

This example uses an ODBC connection to a Paradox table located in the ..DelphiDemosData directory. To create the connection to this table, bring up the 32 bit ODBC applet in the Control Panel, and make sure you have a Paradox ODBC driver installed. To create a new Alias (also known as a Data Source), click the Add button on the first page (labeled User DSN) of the ODBC applet. The Data Source you create should be called ParadoxODBC.

If you do not have a Paradox driver installed, you may be able to find a copy on the CD of some Microsoft product that you might have laying around the shop. For instance, your version of VB, Visual C++, Word, Excel, Office, Access and other Microsoft products all may come with Paradox ODBC drivers. My online search failed, but Microsoft or some other vendor may have these drivers available on the internet. If you can't locate a driver, it would not be difficult to restructure this example to go after the same InterBase tables used in previous examples shown in this paper.

To use the example shown here, just load the makefile into C++Builder, edit the paths as necessary, and press compile. Remember this is a DCE program, so you need to create DAP files like the one shown in Listing 20, and you need to set the TEnterConnection property called TransportMode to tmDCE.

Listing 20: A sample DAP file for the server. You will need to build DAP files for the client, server and func portions of the example.

[DCEApp]
Broker=ncacn_ip_tcp:ccalvertpc3[]
LogFile=dbserv.log
LogLevel=3

Note that there are two applications on the server side. The first is started by launching BRK.EXE and then running the StartServer.bat file, which contains the following simple line, very like similar ones you have seen earlier in the paper:

start clistart -q dbserv.sql -f dbserv.dap -d ParadoxODBC

At this stage, you could launch Delphi and connect to the database using techniques explained earlier in this paper.

What makes this example interesting however, is that some business rules are implemented in a program called Func.Exe. To create Func.Exe you need to generate several stubs, one set for the SQL server and the second set for the Func program itself. To generate the stubs, you first need to create some IDL files, as shown in Listing 20 and Listing 21.

Listing 21: The IDL file for the SQL Server part of the program.

/* procedure rpc definition */
/* RPC definition for dbserv case */

[
uuid(FF3F3420-38C5-11d1-BD72-00A024E22A8B),
version (1.1)
]
interface dbserv {
signed32  sql_prepare_dbserv(
	  [in] ode_char_u_n_str_t db,
	  [in] ode_char_u_n_str_t login,
	  [in] ode_char_u_n_str_t pwd);

signed32  sql_rows_dbserv(
	  [in] signed32 maxrows);

signed32  sql_set_max_rows_dbserv(
	  [in] signed32 maxrows);

[idempotent]signed32  get_customer(
	  [out] signed32* rows,
	  [out] ode_double_u_c_str_t* CustNo,
	  [out] ode_char_u_n_arr_t* Company,
	  [out] ode_char_u_n_arr_t* Addr1,
	  [out] ode_char_u_n_arr_t* City,
	  [out] ode_char_u_n_arr_t* State,
	  [out] ode_char_u_n_arr_t* Zip,
	  [out] ode_char_u_n_arr_t* Country,
	  [out] ode_char_u_n_arr_t* Addr2,
	  [out] ode_char_u_n_arr_t* Contact,
	  [out] ode_double_u_c_str_t* TaxRate);

[idempotent]signed32  get_order_totals(
	  [in] double CustNo,
	  [out] signed32* rows,
	  [out] ode_double_u_c_str_t* OrderNo,
	  [out] ode_double_u_c_str_t* ItemsTotal,
	  [out] ode_double_u_c_str_t* AmountPaid);

}

Listing 22: The IDL file for the functionality server.

/* procedure rpc definition */
/* RPC definition for dbserv case */

[
uuid(0144E3A0-38D0-11d1-BD72-00A024E22A8B),
version (1.1)
]
interface func {

[idempotent]signed32  get_payment_details(
          [out] signed32 *rows,
          [out] ode_double_u_c_str_t* CustNo,
          [out] ode_char_u_n_arr_t* Company,
          [out] ode_char_u_n_arr_t* Addr1,
          [out] ode_char_u_n_arr_t* City,
          [out] ode_char_u_n_arr_t* State,
          [out] ode_char_u_n_arr_t* Zip,
          [out] ode_char_u_n_arr_t* Country,
          [out] ode_char_u_n_arr_t* Addr2,
          [out] ode_char_u_n_arr_t* Contact,
          [out] ode_double_u_c_str_t* AmountOwed,
          [out] ode_double_u_c_str_t* AmountPaid
          );

}

Once you have the IDL files, you then have to create the stubs. When creating these stubs, I could issue a command that looks like this:

obigen -c C1.0 -s C1.0 -d func.idl

However, it is possible to generate a file called a Distributed Development Profile, with a DDP extension. The name of this file can be passed to Obigen in lieu of any other parameters:

Obigin -p func.ddp

DDP files follow a particular, and rather complex format. Fortunately, you do not need to do anything to generate these files other than type the following at the command prompt:

Obigen -P func

The result of this effort will be a file that looks like this:


[RPCApp]
RPCType=dce
IDLFiles=Func.idl
ACFFiles=
ExportedInterfaces=
TransactionalFiles=
ServiceInterface=Func.idl
LogFile=Func.log
IncludePath=.
OutDirectory=.
NoDefIdir=0

[RPCClt]
Language=
NoStub=

[RPCSvr]
Language=
MainFileName=Funcsm
InitFunction=
ExitFunction=
NoMain=0
NoStub=
MainFunction=main
SplitMain=0
GenerateTemplate=0

You can now edit the file so that it looks like the code shown in Listing 23, and then use it by typing: ObiGen - p Func.ddp at the command prompt. The output from this command should be the files func_s.h and func_s.c and funcsm.c.

Listing 23: One of the two DDP files for the Func project.

[RPCApp]
RPCType=dce
IDLFiles=func.idl
ACFFiles=
ExportedInterfaces=
TransactionalFiles=
ServiceInterface=func.idl
LogFile=func.log
IncludePath=.
OutDirectory=.
NoDefIdir=0

[RPCClt]
Language=
NoStub=

[RPCSvr]
Language=C1.0
MainFileName=funcsm
InitFunction=
ExitFunction=
NoMain=0
NoStub=
MainFunction=entera_main
SplitMain=0
GenerateTemplate=0

Notice that I have changed the file to reflect language that we want to use. In this case, C is the language of choice, so I have passed in the constant C1.0.

I have also change the name of the MainFunction, calling it entera_main. The reason for making this change becomes obvious when you consider the structure of this application. We want to build it inside C++Builder, and so our main function will be generated automatically by Borland. The plain is to have this Borland generated main function call the Entera generate main function, as follows:

int main(int argc, char **argv)
{
  int rv;
  rv = entera_main(argc, argv);
  return 0;
}

You also need to create stubs for the original RPC calls found in your Data Access server. The DDP file you want to use is shown in Listing 24.

Listing 24: The DDP file, called dbserve.ddp, for use with the Data Access Server.

[RPCApp]
RPCType=dce
IDLFiles=dbserv.idl
ACFFiles=
ExportedInterfaces=
TransactionalFiles=
ServiceInterface=dbserv.idl
LogFile=dbserv.log
IncludePath=., C:OpenEnventeradceinclude
OutDirectory=.
NoDefIdir=0

[RPCClt]
Language=C1.0
NoStub=

[RPCSvr]
Language=
MainFileName=dbservsm
InitFunction=
ExitFunction=
NoMain=0
NoStub=
MainFunction=main
SplitMain=0
GenerateTemplate=0

Once again, you make use of this file by going to the command prompt and typing the following:

Obigen -p dbserv.ddp.

The output from the file should be dbserv.h and dbserv_c.c.

You are now ready to build Func.exe. Go to the command prompt and type:

Make -f func.mak

You can now start the functionality server by first making sure the broker and Data Access server are running, and then typing:

Start func -e func.dap

It is now time to build the Delphi client that will talk to this application. The problem you face here is that there is no SQL file which you can use to fill the SQLFile property of TEnteraConnection. If you want, you can simply leave it blank and fill in the Attributes, Index, Name and Params fields of the SelectRPC property in the TEnteraProvider by yourself. (If you are having trouble finding these properties, just double click on the SelectRPC property of a TEnteraProvider.)

Clearly, its not much fun trying to fill in these fields by hands. One way to get them filled in automatically is to create a fake SQL file which can go in the SQLFile property of the TEnteraConnection component. To calculate what this fake SQL file should look like, just view func.idl and refer to the source found in func.cpp. The file I came up with is shown in Listing 25. Note that I have inserted the UUID from func.idl, and assigned the interface the name func, so that it matches the name of the functionality server itself.

Listing 25: Here is the "fake" SQL file you can use to get Delphi to automatically fill in the fields of the SelectRPC property of the TEnteraProvider.

[uuid(0144E3A0-38D0-11d1-BD72-00A024E22A8B), version(1.1)]

interface func
{
get_payment_details:
select
        [double]CustNo,
        [char]Company,
        [char]Addr1,
        [char]City,
        [char]State,
        [char]Zip,
        [char]Country,
        [char]Addr2,
        [char]Contact,
        [double]AmountOwed,
        [double]AmountPaid
        
from
        customer;

}

Once you have created this SQL file, then you can simply use the SQLFile property of the TEnteraConnection component to read it in. Then set the ConfigFile and TransportMode properties, and ensure that the ServerName property was filled in correctly so that it reads func.

Now drop down a TEnteraProvider component, hook it up to the TEnteraConnection component and set the SelectRPC property to get_payment_details. Make sure the Index property beneath SelectRPC is set to 0, since this is the first (and only) RPC call in the server. You can now drop down a TClientDataSet, a TDataSource and a TDBGrid and hook them up as you would in any normal Delphi program. You should now be able to set the Active property of the TClientDataSet to True.

An example of how to set up this entire program is found on disk in the Func directory. If you have trouble getting the example to run correctly, look for updates on the Tech Voyage area of the Borland web site.

Listing 26: The SQL script for a DCE based functionality server.

[uuid(FF3F3420-38C5-11d1-BD72-00A024E22A8B), version(1.1)]
interface dbserv
{
get_customer:
select
        [double]CustNo,
        [char]Company,
        [char]Addr1,
        [char]City,
        [char]State,
        [char]Zip,
        [char]Country,
        [char]Addr2,
        [char]Contact,
        [double]TaxRate
from
        customer;

get_order_totals:
select
        [double]OrderNo,
        [double]ItemsTotal,
        [double]AmountPaid
from
	orders
where
        CustNo = $CustNo[double];
} 

Listing 27: The C source code for C++Builder based functionality server.

//--------------------------------------------------------------------
#include 
#include 
#include 
#include 

#pragma hdrstop
//--------------------------------------------------------------------
USERES("func.res");
USEUNIT("func_s.c");
USEUNIT("funcsm.c");
USELIB("C:OpenEnvEntera32dceliboded30.lib");
USEUNIT("dbserv_c.c");
//--------------------------------------------------------------------
#include "func_s.h"
#include "dbserv.h"

#ifdef __cplusplus
extern "C" {
#endif
int entera_main(int argc, char **argv);
#ifdef __cplusplus
}
#endif

int main(int argc, char **argv)
{
	int rv;

	rv = entera_main(argc, argv);

	return 0;
}
//--------------------------------------------------------------------

signed32 get_payment_details(
	signed32   *rows,
	double**	CustNo,
	char***	Company,
	char***	Addr1,
	char***	City,
	char***	State,
	char***	Zip,
	char***	Country,
	char***	Addr2,
	char***	Contact,
	double**	AmountOwed,
	double**	AmountPaid
)
{
  signed32 CustCount, OrderCount, i, j, ORows;
  double *TaxRate;
  double*	OrderNo;
  double*	ItemsTotal;


  CustCount = get_customer(rows, CustNo, Company, Addr1, City, State,
			  Zip, Country, Addr2, Contact, &TaxRate);
  if ((CustCount < 0) || (ode_cln_get_errnum() != ode_s_ok))
    return (-1);

  *AmountOwed = (double *)ode_mem_malloc (CustCount * sizeof(double));
  *AmountPaid = (double *)ode_mem_malloc (CustCount * sizeof(double));

  for (i = 0; i < CustCount ; i++)
  {
    OrderCount = get_order_totals ((*CustNo)[i], &ORows, &OrderNo, 
      &ItemsTotal, AmountPaid);
     if ((OrderCount < 0) || (ode_cln_get_errnum() != ode_s_ok))
       return (-i);
	 for (j = 0 ; j < OrderCount ; j++)
     {
       if (ItemsTotal[j] > 0.0002)
         (*AmountOwed)[i] += ItemsTotal[j] * 
           ((TaxRate[i] + 100) / 100);
     }

  }
  return (0);
}

int func_init (int argc, char **argv)
{
  ode_cln_bind_dbserv();
  return (1);
}

Listing 28: A make file for the functionality server.

# --------------------------------------------------------------------
VERSION = BCB.01
# --------------------------------------------------------------------
!ifndef BCB
BCB = $(MAKEDIR)..
!endif
# --------------------------------------------------------------------
PROJECT = func.exe
OBJFILES = func.obj func_s.obj funcsm.obj dbserv_c.obj
RESFILES = func.res
RESDEPEN = $(RESFILES)
LIBFILES = C:OpenEnvEntera32dceliboded30.lib
DEFFILE = 
# --------------------------------------------------------------------
CFLAG1 = -Od -w -k -r- -y -v -vi- -c -a4 -b- -w-par -w-inl -Vx -Ve -x
CFLAG2 = -DWIN32 
   -I$(BCB)include;$(BCB)includevcl;c:OpenEnvEntera32dceinclude;c:O
penEnvEntera32dcedceinc 
PFLAGS = -
AWinTypes=Windows;WinProcs=Windows;DbiTypes=BDE;DbiProcs=BDE;DbiErrs=BD
E 
   -DWIN32 -U$(BCB)libobj;$(BCB)lib;c:OpenEnvEntera32dcebin 
   -
I$(BCB)include;$(BCB)includevcl;c:OpenEnvEntera32dceinclude;c:O
penEnvEntera32dcedceinc 
   -v -$Y -$W -$O- -JPHNV -M     
RFLAGS = -DWIN32 
   -i$(BCB)include;$(BCB)includevcl;c:OpenEnvEntera32dceinclude;c:O
penEnvEntera32dcedceinc 
LFLAGS = -L$(BCB)libobj;$(BCB)lib;c:OpenEnvEntera32dcebin -ap -Tpe -x 
   -v -V4.0 
IFLAGS = 
LINKER = ilink32
# -------------------------------------------------------------------
ALLOBJ = c0x32.obj $(OBJFILES)
ALLRES = $(RESFILES)
ALLLIB = $(LIBFILES) vcl.lib import32.lib cp32mt.lib 
# -------------------------------------------------------------------
.autodepend

$(PROJECT): $(OBJFILES) $(RESDEPEN) $(DEFFILE)
    $(BCB)BIN$(LINKER) @&&!
    $(LFLAGS) +
    $(ALLOBJ), +
    $(PROJECT),, +
    $(ALLLIB), +
    $(DEFFILE), +
    $(ALLRES) 
!

.pas.hpp:
    $(BCB)BINdcc32 $(PFLAGS) { $** }

.pas.obj:
    $(BCB)BINdcc32 $(PFLAGS) { $** }

.cpp.obj:
    $(BCB)BINbcc32 $(CFLAG1) $(CFLAG2) -o$* $* 

.c.obj:
    $(BCB)BINbcc32 $(CFLAG1) $(CFLAG2) -o$* $**

.rc.res:
    $(BCB)BINbrcc32 $(RFLAGS) $<
#---------------------------------------------------------------------

Appendix C: Simple RPC Calls in C++Builder

On DELENSRC.ZIP you will find an example called BCBTCP that shows how to construct simple RPC calls in C++Builder. To create the program, make sure your paths are set up correctly, and then type Make at the command line. Batch files are available to start the broker and the server.


Server Response from: ETNASC03