It is very easy to add a Xbase application with desktop look and feel to an existing Rails web application. All common databases used by Rails's ActiveRecord can be accessed with Xbase++2.0 data engines out of the box. The new DataObject even allows a surprisingly simple implementation of ActiveRecord way of doing it. SQL-free full ORM with belongs_to association in 200 lines of code demonstrate the power of Xbase:
// record dataobject - rest read with sql crud // needs s-pluralising and id primary key // fields are vars in DataObject // supports belongs_to association // usage: // sXml := LoadFromUrl( "http://reposit/adsx/emps/1" ) // empsalary := Record():new( sXml ) // create from xml // empsalary := Record():new( "empsalary" ) // create empty object // empsalary:find( 1 ) // query by id // empsalary:find( "empno=14" ) // query by where // more := empsalary:salary + 100.00 // access field // empsalary:salary := more // update record // empsalary:update() // commit changes // empsalary:delete() // delete record // full := empsalary:employee:firstname + " " + ; // empsalary:employee:lastname // even in belongs_to #include "class.ch" CLASS Record FROM DataObject VAR __model, __cid, __oAct, __has METHOD init, has_one, getNoIVar, set, fields, fieldn, get, geto, execute, readr EXPORTED: METHOD find, create, update, destroy ENDCLASS METHOD Record:init( sModel ) // build dataobject vars from xml record type or empty for model LOCAL oXmlHandle, oXmlRoot, aTags, aTags2, aTags3, oAttributes LOCAL i ::__oAct := DataObject():New() ::__has := {} oXmlHandle := XMLDocOpenString( sModel ) oXmlRoot := XMLDocGetRootTag( oXmlHandle ) XMLGetTag( oXmlRoot, @aTags ) XMLGetTag( aTags[3][1], @aTags2 ) IF aTags2[3] != NIL // found valid xml-record - handle as query ::__model := aTags2[1] FOR i := 1 TO len(aTags2[3]) XMLGetTag( aTags2[3][i], @aTags3 ) IF SubStr( aTags3[1], 1, 2 ) != "__" ::set( aTags3[1], aTags3[2] ) IF aTags3[1] == "id" ::__cid := aTags3[2] ENDIF ENDIF NEXT i ELSE ::__model := sModel ENDIF RETURN self METHOD Record:has_one( sParent ) AAdd( ::__has, sParent ) RETURN METHOD Record:getNoIVar( sAssoc ) // query parent on first read access LOCAL oParent DO CASE CASE IsMemberVar( self, sAssoc + "_id" ) // belongs_to if model_id field exists oParent := Record():new( sAssoc ) ::setNoIVar( sAssoc, oParent ) oParent:__cid := ::get( sAssoc + "_id" ) oParent:readr() CASE AScan( ::__has, sAssoc ) // has_one if __has entry oParent := Record():new( sAssoc ) ::setNoIVar( sAssoc, oParent ) oParent:readr( sAssoc + "_id = " + LTrim( STR(::__cid) ) ) OTHERWISE oParent := NIL ENDCASE RETURN oParent METHOD Record:execute( sSQL ) // sql statement - use area for select LOCAL oSqlStmt oSqlStmt := DacSqlStatement(DbePgSess()):FromChar( sSQL ) IF SubStr( sSQL, 1, 6 ) == 'SELECT' oSqlStmt:Build():Query(, "RFIND") ELSE // ::_Session:beginTransaction() oSQLStmt:build():execute() // ::_Session:commitTransaction() ENDIF RETURN METHOD Record:find( iId ) IF Valtype( iId ) == "C" ::readr( iId ) ELSE ::__cid := iId ::readr() ENDIF RETURN self METHOD Record:set( sKey, sVal ) ::setNoIVar( sKey, sVal ) ::__oAct:setNoIVar( sKey, sVal ) RETURN sVal METHOD Record:fields() LOCAL aMembers aMembers := ::__oAct:classDescribe( CLASS_DESCR_MEMBERS ) RETURN aMembers METHOD Record:fieldn() LOCAL aAll, aMembers, i aMembers := {} aAll := ::classDescribe( CLASS_DESCR_MEMBERS ) FOR i := 1 TO len(aAll) IF SubStr( aAll[i][1], 1, 2 ) != "__" AAdd( aMembers, aAll[i] ) ENDIF NEXT i RETURN aMembers METHOD Record:get( sField ) LOCAL rVal rVal := ::&(sField) RETURN rVal METHOD Record:geto( sField ) LOCAL rVal rVal := ::__oAct:&(sField) RETURN rVal // SQL create/insert read/select update destroy/delete METHOD Record:readr( sQuery ) // select and build field vars // prepare parent query with id LOCAL i, sWhere, sSelectSql, sField, sBelongsto, oParent IF PCount() > 0 sWhere := sQuery ELSE sWhere := "id = " + LTrim( STR(::__cid) ) ENDIF sSelectSql := "SELECT * FROM " + ::__model + "s WHERE " + sWhere ::execute( sSelectSql ) FOR i := 1 TO RFIND->(FCount()) sField := RFIND->(FieldName( i )) IF SubStr( sField, 1, 2 ) != "__" ::set( sField, RFIND->(FieldGet( i )) ) ENDIF IF sField == "id" ::__cid := RFIND->(FieldGet( i )) ENDIF NEXT i CLOSE RFIND RETURN sSelectSql METHOD Record:update() // update changed fields (oAct is old value) LOCAL aFields, sField, sUpdateSql LOCAL i, needsep := .F. aFields := ::fields() sUpdateSql := "UPDATE " + ::__model + "s SET " FOR i := 1 TO len(aFields) sField := aFields[i][1] IF ::get( sField ) != ::geto( sField ) .AND. sField != "id" IF needsep sUpdateSql := sUpdateSql + ", " ENDIF sUpdateSql := sUpdateSql + sField + " = " + SqlValue( ::get( sField ) ) ::__oAct:&(sField) := ::get( sField ) needsep := .T. ENDIF NEXT i sUpdateSql := sUpdateSql + " WHERE id = " + SqlValue( ::__cid ) ::execute( sUpdateSql ) RETURN sUpdateSql METHOD Record:create() // insert record with all attrs (fields) LOCAL aFields, sField, sInsertSql LOCAL i, needsep := .F. aFields := ::fieldn() sInsertSql := "INSERT INTO " + ::__model + "s (" FOR i := 1 TO len(aFields) sField := aFields[i][1] IF needsep sInsertSql := sInsertSql + ", " ENDIF sInsertSql := sInsertSql + sField needsep := .T. NEXT i sInsertSql := sInsertSql + ") VALUES (" needsep := .F. FOR i := 1 TO len(aFields) sField := aFields[i][1] IF needsep sInsertSql := sInsertSql + ", " ENDIF sInsertSql := sInsertSql + SqlValue( ::get( sField ) ) ::__oAct:&(sField) := ::get( sField ) needsep := .T. NEXT i sInsertSql := sInsertSql + ")" ::execute( sInsertSql ) RETURN sInsertSql METHOD Record:destroy() // delete with id LOCAL sDeleteSql LOCAL i sDeleteSql := "DELETE FROM " + ::__model + "s WHERE id = " + SqlValue( ::id ) ::execute( sDeleteSql ) RETURN sDeleteSql
If your Rails application is already connected to PostgreSQL you might want to use the DBF-emulation-mode of Xbase++2.0. Since your tables cannot be "upsized" you have to add the emulation layer to them. Use this script as an example to enable Xbase emulation layer to access the PostgreSQL tables ("upsizeify.sql"):
DELETE FROM "alaska-software.isam.tables" WHERE "table_name"='employees'; INSERT INTO "alaska-software.isam.tables"(table_name, unc_name, updated, created, record_count, update_count, lock_owner) VALUES ('employees', 'd:\\data\\employees.dbf', '2015-10-05 07:38:36', '2015-10-05 07:38:36', 0, 0, 0); ALTER TABLE employees ADD COLUMN __deleted boolean NOT NULL DEFAULT false, __record serial PRIMARY KEY, __rowversion integer NOT NULL DEFAULT 0, __keyversion integer NOT NULL DEFAULT 0, __lock_owner integer NOT NULL DEFAULT 0; CREATE INDEX "employees_record" ON employees USING btree (__record); CREATE TRIGGER employees_isam_tablemeta AFTER INSERT OR DELETE OR UPDATE ON employees FOR EACH ROW EXECUTE PROCEDURE isam_tablemeta_update(); CREATE TRIGGER employees_isam_rowversion AFTER UPDATE ON employees FOR EACH ROW EXECUTE PROCEDURE isam_rowversion_update(); update "alaska-software.isam.tables" set record_count = (select count(*) from "employees") where table_name = 'employees';