Calling Stored Procedures

This article goes out to all MFC programmers who wish to improve their applications performance and those who have tried calling stored procedures and failed.

Your first question may be, Why use stored procedures when I can just say,

rs.Open( CRecordset::snapshot, "Select * from Clients where
account_number = '1234567'");.

Well theres 2 answers that I can think of. First off, stored procedures are tremendously fast. Secondly, stored procedures are more reliable.

From what I've tested on a 200 record table, after the initial connection stored procedures return INSTANTLY!! Where an SQL statement returns in 500ms on the initial connection and 100ms thereafter. This can be very helpful in the Client/Server environment and in any application that is database intensive.

This example calls a stored procedure with the following definition:

CREATE PROCEDURE OutputParams @InputParam char(20) , @OutputParam
char(20) = '' OUTPUT As

SELECT @OutputParam = @InputParam
RETURN 1

Basically all it does is return the same string you send it. You can define all the paramaters you like. The reason for this example was to show how to return a character output parameter. MFC does not support it. So you have to create your own RFX_Text function to support it.

If you would like to call a stored procedure that returns a recordset, its not much different. The VC help files tell you exactly how to do it. You can do a searh on "stored procedures and recordsets".

// StoredProcedure.h : header file
//

/*
Craig Lucas, CoderMan@netrox.net
*/
#include "afxdb.h"
/////////////////////////////////////////////////////////////////////////////
// StoredProcedure recordset

class StoredProcedure : public CRecordset
{
public:
	StoredProcedure(CDatabase* pDatabase = NULL);
	DECLARE_DYNAMIC(StoredProcedure)
		
	// Field/Param Data
	//{{AFX_FIELD(StoredProcedure, CRecordset)
	long m_retRETURN_VALUE;
	CString m_paramInputParam;  //The input param 
	CString m_paramOutputParam; //The output param 
	//}}AFX_FIELD
	
	// Overrides
	// ClassWizard generated virtual function overrides
	//{{AFX_VIRTUAL(StoredProcedure)
public:
	virtual CString GetDefaultConnect();    // Default connection string
	virtual CString GetDefaultSQL();    // Default SQL for Recordset
	virtual void DoFieldExchange(CFieldExchange* pFX);  // RFX support
	virtual void Move( long nRows, WORD wFetchType = SQL_FETCH_RELATIVE );
	//}}AFX_VIRTUAL
	
	// Implementation
#ifdef _DEBUG
	virtual void AssertValid() const;
	virtual void Dump(CDumpContext& dc) const;
#endif
};



// StoredProcedure.cpp : implementation file
//

#include "stdafx.h"
#include "StoredProcedure.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif


void AFXAPI RFX_TextOut(CFieldExchange* pFX, LPCTSTR szName,
CString& value, int nMaxLength, int nColumnType, short nScale);
/////////////////////////////////////////////////////////////////////////////
// StoredProcedure

IMPLEMENT_DYNAMIC(StoredProcedure, CRecordset)

StoredProcedure::StoredProcedure(CDatabase* pdb)
: CRecordset(pdb)
{
	//{{AFX_FIELD_INIT(StoredProcedure)
	m_retRETURN_VALUE = 0;
	m_paramInputParam = _T("");
	m_paramOutputParam = _T("");
	m_nFields = 0;
	//}}AFX_FIELD_INIT
	m_nParams = 3;
	m_nDefaultType = snapshot;
}


CString StoredProcedure::GetDefaultConnect()
{
	return _T("ODBC;DSN=CodeGuru;UID=sa;PWD=;");
}

CString StoredProcedure::GetDefaultSQL()
{
	return _T("{? = call OutputParams;1 (?,?)}"); 
	// this is the SQL String to call a stored procedure
	// 1 question mark for every parameter in the 
	// stored procedure
}

void StoredProcedure::DoFieldExchange(CFieldExchange* pFX)
{
	//{{AFX_FIELD_MAP(StoredProcedure)
	
	//}}AFX_FIELD_MAP
	
	//Make sure these are put outside the AFX_FIELD_MAP comments
	pFX->SetFieldType(CFieldExchange::outputParam); //set the field type to
	outputParam for the return value
		RFX_Long(pFX, "RETURN_VALUE", m_retRETURN_VALUE); //Bind the return value
	to the variable 
		pFX->SetFieldType(CFieldExchange::inputParam); //reset the field type to
	inputParam
		RFX_TextOut(pFX, "@InputParam", m_paramInputParam,255,SQL_CHAR,0); //call
	the new RFX_Text to get the character output params
		pFX->SetFieldType(CFieldExchange::inoutParam); // reset the field
	type to receive the output param
		RFX_TextOut(pFX, "@OutputParam", m_paramOutputParam,255,SQL_CHAR,0);
	//bind the output parameter to the variable
}

/////////////////////////////////////////////////////////////////////////////
// StoredProcedure diagnostics

#ifdef _DEBUG
void StoredProcedure::AssertValid() const
{
	CRecordset::AssertValid();
}

void StoredProcedure::Dump(CDumpContext& dc) const
{
	CRecordset::Dump(dc);
}
#endif //_DEBUG

void StoredProcedure::Move( long nRows, WORD wFetchType )
{
	// Protection so that if the procedure returns no result sets, no 
	// fetch operations are attempted.
	if (m_nFields)
		CRecordset::Move(nRows, wFetchType);
	else
		m_bBOF = m_bEOF = TRUE;
}

void AFXAPI RFX_TextOut(CFieldExchange* pFX, LPCTSTR szName,
						CString& value, int nMaxLength, int nColumnType, short nScale)
{
	//This is MFC's RFX_Text function with 2 modifications:
	//   1. All unicode definitions are removed for brevity.
	//   2. A fixed version of SQLBindParamaters() was inserted
	
	ASSERT(AfxIsValidAddress(pFX, sizeof(CFieldExchange)));
	ASSERT(AfxIsValidString(szName));
	ASSERT(AfxIsValidAddress(&value, sizeof(CString)));
	
	RETCODE nRetCode;
	UINT nField;
	if (!pFX->IsFieldType(&nField))
		return;
	
	LONG* plLength = pFX->m_prs->GetFieldLengthBuffer(
		nField - 1, pFX->m_nFieldType);
	switch (pFX->m_nOperation)
	{
	default:
		pFX->Default(szName, value.GetBuffer(0), plLength,
			SQL_C_CHAR, value.GetLength(), nMaxLength);
		value.ReleaseBuffer();
		return;
		
	case CFieldExchange::BindParam:
		{
			// Preallocate to nMaxLength and setup binding address
			value.GetBufferSetLength(nMaxLength);
			void* pvParam = value.LockBuffer(); // will be overwritten if UNICODE
			*plLength = pFX->m_prs->IsParamStatusNull(nField - 1) ?
								SQL_NULL_DATA : SQL_NTS;
			
			// This is the new version of SQLBindParamaters with the 2nd to last
			param fixed.
				AFX_SQL_SYNC(::SQLBindParameter(pFX->m_hstmt, (UWORD)nField,
				(SWORD)pFX->m_nFieldType, SQL_C_CHAR, (SWORD)nColumnType,
				nMaxLength, nScale, pvParam, nMaxLength, plLength));
			
			//The reason character output params can not be returned was because
			//MS had hardcoded the max_buffer length to 0.
			
			/*   This was the old version of SQLBindParamaters.
			AFX_SQL_SYNC(::SQLBindParameter(pFX->m_hstmt, (UWORD)nField,
			(SWORD)pFX->m_nFieldType, SQL_C_CHAR, (SWORD)nColumnType,
			nMaxLength, nScale, pvParam, 0, plLength));
			*/
			
			value.ReleaseBuffer();
			
			if (nRetCode != SQL_SUCCESS)
				pFX->m_prs->ThrowDBException(nRetCode, pFX->m_hstmt);
			
			// Add the member address to the param map
			pFX->m_prs->m_mapParamIndex.SetAt(&value, (void*)nField);
		}
		return;
	case CFieldExchange::BindFieldToColumn:
		{
			// Assumes all bound fields BEFORE unbound fields
			CODBCFieldInfo* pODBCInfo =
				&pFX->m_prs->m_rgODBCFieldInfos[nField - 1];
			UINT cbColumn = pODBCInfo->m_nPrecision;
			
			switch (pODBCInfo->m_nSQLType)
			{
			default:
#ifdef _DEBUG
				// Warn of possible field schema mismatch
				if (afxTraceFlags & traceDatabase)
					TRACE1("Warning: CString converted from SQL type %ld.\n",
					pODBCInfo->m_nSQLType);
#endif // _DEBUG
				
				// Add room for extra information like sign, decimal point, etc.
				cbColumn += 10;
				break;
				
			case SQL_LONGVARCHAR:case SQL_CHAR: case SQL_VARCHAR:
				break;
				
			case SQL_FLOAT: case SQL_REAL:case SQL_DOUBLE:
				// Add room for sign, decimal point and " E +XXX"
				cbColumn += 10;
				break;
				
			case SQL_DECIMAL:case SQL_NUMERIC:
				// Add room for sign and decimal point
				cbColumn += 2;
				break;
				
			case SQL_TIMESTAMP: case SQL_DATE:case SQL_TIME:
				// May need extra space, i.e. "{TS mm/dd/yyyy hh:mm:ss}"
				cbColumn += 10;
				break;
				
			case SQL_TINYINT:case SQL_SMALLINT: case SQL_INTEGER:case SQL_BIGINT:
				// Add room for sign
				cbColumn += 1;
				break;
			}
			
			// Constrain to user specified max length, subject to 256 byte min
			if (cbColumn > (UINT)nMaxLength || cbColumn < 256)
				cbColumn = nMaxLength;
			
			// Set up binding addres
			void* pvData;
			value.GetBufferSetLength(cbColumn+1);
			pvData = value.LockBuffer();    // will be overwritten if UNICODE
			
			AFX_SQL_SYNC(::SQLBindCol(pFX->m_prs->m_hstmt, (UWORD)nField,
				SQL_C_CHAR, pvData, cbColumn+1, plLength));
			value.ReleaseBuffer();
			if (!pFX->m_prs->Check(nRetCode))
				pFX->m_prs->ThrowDBException(nRetCode);
			
			// Add the member address to the field map
			pFX->m_prs->m_mapFieldIndex.SetAt(&value, (void*)nField);
		}
		return;
		
	case CFieldExchange::Fixup:
		if (*plLength == SQL_NULL_DATA)
		{
			pFX->m_prs->SetNullFieldStatus(nField - 1);
			value.GetBufferSetLength(0);
			value.ReleaseBuffer();
		}
		else
		{
			LPTSTR lpsz = value.GetBuffer(0);
			if (pFX->m_prs->m_pDatabase->m_bStripTrailingSpaces)
			{
				// find first trailing space
				LPTSTR lpszFirstTrailing = NULL;
				while (*lpsz != '\0')
				{
					if (*lpsz != ' ')
						lpszFirstTrailing = NULL;
					else
					{
						if (lpszFirstTrailing == NULL)
							lpszFirstTrailing = lpsz;
					}
					lpsz = _tcsinc(lpsz);
				}
				// truncate
				if (lpszFirstTrailing != NULL)
					*lpszFirstTrailing = '\0';
				
			}
			value.ReleaseBuffer();
			*plLength = value.GetLength();
		}
		return;
		
	case CFieldExchange::SetFieldNull:
		if ((pFX->m_pvField == NULL &&
			pFX->m_nFieldType == CFieldExchange::outputColumn) ||
			pFX->m_pvField == &value)
		{
			if (pFX->m_bField)
			{
				// Mark fields null
				pFX->m_prs->SetNullFieldStatus(nField - 1);
				// Set string 0 length
				value.GetBufferSetLength(0);
				value.ReleaseBuffer();
				*plLength = SQL_NULL_DATA;
			}
			else
			{
				pFX->m_prs->ClearNullFieldStatus(nField - 1);
				*plLength = SQL_NTS;
			}
#ifdef _DEBUG
			pFX->m_nFieldFound = nField;
#endif
		}
		return;
		
	case CFieldExchange::MarkForAddNew:
		// can force writing of psuedo-null value (as a non-null) by setting
		field dirty
			if (!value.IsEmpty())
			{
				pFX->m_prs->SetDirtyFieldStatus(nField - 1);
				pFX->m_prs->ClearNullFieldStatus(nField - 1);
			}
			return;
			
	case CFieldExchange::MarkForUpdate:
		if (value.IsEmpty())
			pFX->m_prs->SetNullFieldStatus(nField - 1);
		else
			pFX->m_prs->ClearNullFieldStatus(nField - 1);
		pFX->Default(szName, &value, plLength,
			SQL_C_CHAR, value.GetLength(), nMaxLength);
		return;
		
	case CFieldExchange::LoadField:
		{
			// Get the field data
			CFieldInfo* pInfo = &pFX->m_prs->m_rgFieldInfos[nField - 1];
			CString* pStrCachedValue = (CString*)pInfo->m_pvDataCache;
			
			// Restore the status
			pFX->m_prs->SetFieldStatus(nField - 1, pInfo->m_bStatus);
			
			// If not NULL, restore the value and length
			if (!pFX->m_prs->IsFieldStatusNull(nField - 1))
			{
				value = *pStrCachedValue;
				*plLength = value.GetLength();
			}
			else
			{
				*plLength = SQL_NULL_DATA;
			}
			
#ifdef _DEBUG
			// Buffer address must not change - ODBC's SQLBindCol depends upon this
			void* pvBind;
			
#ifdef _UNICODE
			pvBind = pFX->m_prs->m_pvFieldProxy[nField-1];
#else // !_UNICODE
			pvBind = value.GetBuffer(0);
			value.ReleaseBuffer();
#endif
			
			if (pvBind != pInfo->m_pvBindAddress)
			{
				TRACE1("Error: CString buffer (column %u) address has changed!\n",
					nField);
				ASSERT(FALSE);
			}
#endif // _DEBUG
		}
		return;
		
	case CFieldExchange::StoreField:
		AfxStoreField(*pFX->m_prs, nField, &value);
		return;
		
	case CFieldExchange::AllocCache:
		{
			CFieldInfo* pInfo = &pFX->m_prs->m_rgFieldInfos[nField - 1];
			pInfo->m_pvDataCache = new CString;
			pInfo->m_nDataType = AFX_RFX_TEXT;
		}
		return;
		
#ifdef _DEBUG
	case CFieldExchange::DumpField:
		*pFX->m_pdcDump << "\n" << szName << " = " << value;
		return;
#endif // _DEBUG
		
	}
}