Page 1 of 2

Migrating TDatabase to FWH19.03's from FWH11.08

Posted: Thu May 02, 2019 9:13 am
by hua
1. This is my existing code to use dbfs as database objects with unique aliases.

Code: Select all

method activate(cAccnFrom, cAccnTo, dFrom, dCOD, oDbCgl, lPrintable) class XBrwGL
  local oldarea := select(), dTo, cStr, a_
  local oDlg, oBrw, oFntTitl, oMFont1, oImg, bInit, oSay, oBtnCgl, nWidth, lTaxCodeAlreadyOpened, cTAccn
  local aDbfs := {                   ;
                   {"oDbHgl", "hgl", "hgl2"}, ;
                   {"oDbSaleman" , "saleman" , "saleman1"}, ;
                   {"oDbJnlhis"  , "jnlhis", "jnlhis2"}  ;
                 }

  cursorWait()
  if !OpenODbfs(aDbfs,,self)
     select (oldarea)
     return nil
  endif
//------------------------------------------------------------------------------------
function OpenODbfs(a_, lExcl, o)
  /* open dbf with unique alias
     a_ must be in the format { {<oDb name>, <dbf name>, <ntx1>, <ntx2>,..., <ntxn>} }
  */

  local lRet := .t., i, nlen := len(a_), j
  local aOpened := {}, oldarea := select()

  default lExcl := .f.


  for i := 1 to nLen
      if net_use(a_[i,2], lExcl,, cGetNewAlias( 'TDF' ))
         aadd(aOpened, select())
         if len(a_[i]) > 2
            for j := 3 to len(a_[i])
                ordListAdd(a_[i,j])
            next
            set order to 1
         endif
         o:&(a_[i,1]) := TDataBase():new()
         o:&(a_[i,1]):cFile := a_[i,2]
         o:&(a_[i,1]):setBuffer(.f.)
      else
         lRet := .f.
         exit
      endif
  next
  // fail? then close those opened so far
  if !lRet
     aeval(aOpened, {|x| (x)->(dbCloseArea())})
  endif
  select (oldarea)
return lRet
 
2. The first error I got when trying to upgrade this to Harbour+FWH19.03 is direct assignment to :cFile is no longer allowed. So I tweaked
o:&(a_[i,1]) := TDataBase():new()
o:&(a_[i,1]):cFile := a_[i,2]
to
o:&(a_[i,1]) := TDataBase():new(,a_[i,2])
3. Then I get other RTE errors. After debugging, I determined it's because :cAlias is empty. So I tweaked the code further to

Code: Select all

  for i := 1 to nLen
      cAlias := cGetNewAlias( 'TDF' )
      if net_use(a_[i,2], lExcl,, cAlias)
         ...
         o:&(a_[i,1]) := TDataBase():new(cAlias, a_[i,2])
         ...
      endif
  next
 
4. At this point RTE still occur. Now it's because :nArea is 0.
How can I set :nArea without using :SetArea()? I do not wish to use :SetArea() because it assigns :cAlias and :cFile with the same value when in my case it's not.
:cAlias is some randomly generated unique alias name while :cFile is the name of the physical dbf.

TIA

Re: Migrating TDatabase to FWH19.03's from FWH11.08

Posted: Thu May 02, 2019 9:44 am
by hua
Solved by subclassing TDataBase

Code: Select all

class TDataX from TDataBase
  data cFile
  method new()
endclass
method new() class TDataX
  ::super:new()
  ::cAlias := alias()
  ::nArea  := select()
return self
 

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Thu May 02, 2019 11:29 pm
by James Bott
Hua,

I think you may be shooting yourself in the foot.

It appears that you are opening all the files at once, and setting your own aliases. Granted this is how we used to do it before using database objects. We also were limited to 255 work areas but we are no longer. Although, you don't need many workareas since you should open and close databases as needed.

With database objects, unique aliases are automatically generated so you don't even need to think about them. You can open multiple copies of the same database and thus not have to worry about saving and restoring the database state (recno, filters, scopes, index, etc.). So you can do things like lookup a customer in a function by opening a copy of the customer file in that function and closing it at the end of that function. You can do this even if there is another copy of the customer file already open. Or even 5 copies open. Everything is so much easier.

Better still is to define a subclass of TDatabase for every DBF.

Code: Select all

Class TCustomers from TDatabase
   Method New()
Endclass

Method New() Class TCustomers
   ::super():New(,"Customers",, .T.)
   ::use()
Return Self
Now you can simply open a customer database like this?

oCustomers:= TCustomers():New()

And even open another copy in the same function.

oCustomers2:= TCustomers():New()

Now you don't have to ever deal with aliases.

You should define a class for each database.

So many options with objects, and the code is much simpler.

James

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Fri May 03, 2019 2:35 am
by hua
.

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Fri May 03, 2019 3:15 am
by hua
James Bott wrote: It appears that you are opening all the files at once, and setting your own aliases.
Yup. If any of it failed to be opened there's no point proceeding because all of them are required, at least that's what I think.
I also would like to use net_use() to open dbfs in network environment as it'll attempt to open a dbf for a few second before giving up and display error message.
With database objects, unique aliases are automatically generated so you don't even need to think about them. You can open multiple copies of the same database and thus not have to worry about saving and restoring the database state (recno, filters, scopes, index, etc.). So you can do things like lookup a customer in a function by opening a copy of the customer file in that function and closing it at the end of that function. You can do this even if there is another copy of the customer file already open. Or even 5 copies open. Everything is so much easier.

Better still is to define a subclass of TDatabase for every DBF.

Code: Select all

Class TCustomers from TDatabase
   Method New()
Endclass

Method New(,"Customers") Class TCustomers
   ::use()
Return Self
Now you can simply open a customer database like this?

oCustomers:= TCustomers():New()

And even open another copy in the same function.

oCustomers2:= TCustomers():New()

Now you don't have to ever deal with aliases.

You should define a class for each database.

So many options with objects, and the code is much simpler.

James
Thanks for the code snippet James. I thought TDatabase:open() was the only way to open a dbf. I can see with your code a unique aliases is already handled by TDatabase and :td_ExecLoop() is already functionally similar to net_use().

This will help simplify my code in the future. Thanks again James

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Fri May 03, 2019 3:30 am
by hua
James Bott wrote:You can open multiple copies of the same database...
But I notice the following comment in TDatabase:use() James,

Code: Select all

   
if Empty( ::nArea ) // 2017-09-04: Prevent re-opening same dbf in multiple workareas

      if Empty( ::cAlias )
         ::cAlias := ::NewAlias()
      endif
 
Does that mean a dbf can be opened only once?

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Fri May 03, 2019 5:05 am
by James Bott
Yup. If any of it failed to be opened there's no point proceeding because all of them are required, at least that's what I think.
If this is a multi-user app, then all databases need to be opened in shared mode, so why would they fail to open?
I also would like to use net_use() to open dbfs in network environment as it'll attempt to open a dbf for a few second before giving up and display error message.
This is built into TDatabase.

James

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Fri May 03, 2019 5:23 am
by James Bott
Does that mean a dbf can be opened only once?
Apparently not-see sample test code below. Each is opened in a different workarea. That comment is either misleading or leftover from a previous version.

James

Code: Select all

#include "fivewin.ch"

Function Main()

   Local oCustomers,oCustomers2
   
   oCustomers:= TCustomers():new()
   
   oCustomers2:= TCustomers():new()
   
   msgInfo(oCustomers:Company,"oCustomers:Company")
   msgInfo(oCustomers:nArea,"oCustomers:nArea")
   
   msgInfo(oCustomers2:Company,"oCustomers2:Company")
   msgInfo(oCustomers2:nArea,"oCustomers2:nArea")

Return nil

Class TCustomers from TDatabase
   Method New()
Endclass

Method New(lShared) Class TCustomers
    default lShared:=.T.
    ::super():New(,"customers",,lShared)
    ::use()
Return self
 

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Sat May 04, 2019 1:09 am
by hua
Thanks for verifying it for me James :D

Re: Migrating TDatabase to FWH19.03's from FWH11.08

Posted: Sat May 04, 2019 11:26 pm
by nageswaragunupudi
hua wrote:Solved by subclassing TDataBase

Code: Select all

class TDataX from TDataBase
  data cFile
  method new()
endclass
method new() class TDataX
  ::super:new()
  ::cAlias := alias()
  ::nArea  := select()
return self
 
We advise you to open the dbf using the methods of TDatabase only.
Still, if you open a dbf directly, the right way to apply it to Tdatabase is :

Code: Select all

oDbf := TDatabase():New( cAlias )  // or :New( nSelect )
 
Do not directly assign cAlias, cFile and nSelect, as in your above example.
TDatabase will automatically assign the values to the Datas on its own automatically.

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Sat May 04, 2019 11:46 pm
by nageswaragunupudi
hua wrote:1. This is my existing code to use dbfs as database objects with unique aliases.

Code: Select all

method activate(cAccnFrom, cAccnTo, dFrom, dCOD, oDbCgl, lPrintable) class XBrwGL
  local oldarea := select(), dTo, cStr, a_
  local oDlg, oBrw, oFntTitl, oMFont1, oImg, bInit, oSay, oBtnCgl, nWidth, lTaxCodeAlreadyOpened, cTAccn
  local aDbfs := {                   ;
                   {"oDbHgl", "hgl", "hgl2"}, ;
                   {"oDbSaleman" , "saleman" , "saleman1"}, ;
                   {"oDbJnlhis"  , "jnlhis", "jnlhis2"}  ;
                 }

  cursorWait()
  if !OpenODbfs(aDbfs,,self)
     select (oldarea)
     return nil
  endif
//------------------------------------------------------------------------------------
function OpenODbfs(a_, lExcl, o)
  /* open dbf with unique alias
     a_ must be in the format { {<oDb name>, <dbf name>, <ntx1>, <ntx2>,..., <ntxn>} }
  */

  local lRet := .t., i, nlen := len(a_), j
  local aOpened := {}, oldarea := select()

  default lExcl := .f.


  for i := 1 to nLen
      if net_use(a_[i,2], lExcl,, cGetNewAlias( 'TDF' ))
         aadd(aOpened, select())
         if len(a_[i]) > 2
            for j := 3 to len(a_[i])
                ordListAdd(a_[i,j])
            next
            set order to 1
         endif
         o:&(a_[i,1]) := TDataBase():new()
         o:&(a_[i,1]):cFile := a_[i,2]
         o:&(a_[i,1]):setBuffer(.f.)
      else
         lRet := .f.
         exit
      endif
  next
  // fail? then close those opened so far
  if !lRet
     aeval(aOpened, {|x| (x)->(dbCloseArea())})
  endif
  select (oldarea)
return lRet
 
2. The first error I got when trying to upgrade this to Harbour+FWH19.03 is direct assignment to :cFile is no longer allowed. So I tweaked
o:&(a_[i,1]) := TDataBase():new()
o:&(a_[i,1]):cFile := a_[i,2]
to
o:&(a_[i,1]) := TDataBase():new(,a_[i,2])
3. Then I get other RTE errors. After debugging, I determined it's because :cAlias is empty. So I tweaked the code further to

Code: Select all

  for i := 1 to nLen
      cAlias := cGetNewAlias( 'TDF' )
      if net_use(a_[i,2], lExcl,, cAlias)
         ...
         o:&(a_[i,1]) := TDataBase():new(cAlias, a_[i,2])
         ...
      endif
  next
 
4. At this point RTE still occur. Now it's because :nArea is 0.
How can I set :nArea without using :SetArea()? I do not wish to use :SetArea() because it assigns :cAlias and :cFile with the same value when in my case it's not.
:cAlias is some randomly generated unique alias name while :cFile is the name of the physical dbf.

TIA
As always, we keep advising to open the dbfs by using methods of TDatabase only.

We advise the revise the function OpenDbfs() on these lines:

Code: Select all

function OpenDbfs( aDbfs, lExcl, o )

   local i, j, oDbf, cErr
   local aOpened  := {}
   local lRet     := .t.

   DEFAULT lExcl := .f.

   for i := 1 to Len( aDbfs )
      oDbf  := TDatabase():Open( nil, aDbfs[ i, 2 ], "DBFNTX", !lExcl )
      AAdd( aOpened, oDbf )
      if oDbf:Used()
         for j := 3 to Len( aDbfs[ i ] )
            TRY
               oDbf:OrdListAdd( aDbfs[ i, j ] )
            CATCH
               lRet  := .f.
               cErr  := "Can not open " + aDbfs[ i, j ] + ".ntx"
               EXIT
            END
         next
      else
         lRet  := .f.
         cErr  := "Can not open " + aDbfs[ i, 2 ] + ".dbf"
      endif
      if lRet == .f.
         EXIT
      endif
   next

   if lRet
      AEval( aDbfs, { |a,i| OSend( o, "_" + a[ 1 ], aOpened[ i ] ) } )
   else
      AEval( aOpened, { |oDbf| oDbf:End() } )
      ? cErr
   endif

return lRet
 

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Sun May 05, 2019 8:34 pm
by James Bott
Hua,

Here is a more complete New() method. You can use this as a starting point for each class (one class for each database). The file is opened (with automatic retrys) and then the primary key index is set. So with just this one line of code you open the database, open the indexes, and set the order.

oCustomers:= TCustomers():New()

Sample New() method:

Code: Select all

CLASS TCustomers from TDatabase
   Method New()
ENDCLASS

Method New( lShared ) Class TCustomers
   Default lShared:= .T.
   ::Super:New(,"customers",,lShared)
   If ::Use()
      ::SetOrder("CUSTNO") // Primary key
      ::Gotop()
   Endif
Return self
 
Note that at the start of your application you need to do this (assuming you are using CDX's):

Code: Select all

   
   REQUEST DBFCDX
   rddsetdefault( "DBFCDX" )
   SET EXCLUSIVE OFF
   SET(_SET_AUTOPEN, .T. )
 
This causes all the indexes to be automatically opened whenever you open a DBF.

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Sun May 05, 2019 9:56 pm
by nageswaragunupudi
Mr. James
We can understand from his code that he is using DBFNTX.

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Mon May 06, 2019 12:36 am
by James Bott
Nages,
We can understand from his code that he is using DBFNTX.
Thanks, I missed that. Maybe it is a good time to switch to CDXs.

Re: Migrating TDatabase to FWH19.03's from FWH11.08 (Resolved)

Posted: Mon May 06, 2019 2:16 am
by hua
nageswaragunupudi wrote: As always, we keep advising to open the dbfs by using methods of TDatabase only.

We advise the revise the function OpenDbfs() on these lines:

Code: Select all

function OpenDbfs( aDbfs, lExcl, o )
    .
    .
    .
 
That's great! Thanks Rao