ByRef ADODB.Command от VB/ASP в ATL и обратно
От: Krotow Латвия  
Дата: 26.03.04 19:15
Оценка:
Имеем ATL компоненту, которая подключается к серверам БД в нескольких вариантах (ADO или напрямую через API БД) и возвращает данные в виде Recordset (disconnected, разумеется ). Всё сделано примерно, как при вызове Connection.Execute(), глюков пока не наблюдалось. Хотелось сделать тоже самое для параметризованных запросов, типа "SELECT * FROM address WHERE city = ? AND street = ?", передавая входные параметри запроса через ADODB.Command. При этом хочется, чтобы параметры тоже были обработаны правильно, включая направления adParamOutput и adParamReturnValue.
Особенно заинтересовал последний, ибо недавно понадобылось вызвать возвратить результат из встроенной проседуры, которая лежит в Oracle Package, а это не рекордсет, а просто номер (INTEGER aka long). Простой пример в VB или VC++ работает на ура, напримерь это:

#include <stdio.h>

#import "c:\program files\common files\system\ado\msado15.dll" rename ("EOF","adoEOF") no_namespace
#define RsITEM(rs,x) rs->Fields->Item[_variant_t(x)]->Value

.....

_ConnectionPtr pConn;
_CommandPtr pCmd;
_ParameterPtr pPrm;
_RecordsetPtr pRS;
try
{
    pConn.CreateInstance(__uuidof(Connection))
    pConn->ConnectionString = L"Provider=OraOLEDB.Oracle;Data Source=my_oracle;Persist Security Info=True;User ID=test;Password=test";
    pConn->CursorLocation = adUseClient;
    pConn->Open("", "", "", -1);
    if (pConn->State == adStateOpen)
    {
        pCmd.CreateInstance(__uuidof(Command))
        pPrm.CreateInstance(__uuidof(Parameter))

        pPrm = pCmd->CreateParameter(L"retval", adInteger, adParamReturnValue, sizeof(long), 0L);
        pCmd->Parameters->Append(pPrm);

        pCmd->CommandText = L"{ ? = CALL VIES_API.VAT(0, 'lv', '90000084505', '1212') }";
        pCmd->PutRefActiveConnection(pConn);
        pRS = pCmd->Execute(NULL, NULL, adCmdText);

        // Тут хаваем наш return параметр :)
        long lRet = pCmd->GetParameters()->GetItem(0L)->GetValue();
        
        if (pRS->GetState() == adStateOpen)
        {        
            while(pRS->adoEOF == false)
            {
                printf("%d\n", long(RsITEM(pRS, 0L)));
                pRS->MoveNext();
            }
            pRS->Close();
        }
    
        pCmd->PutRefActiveConnection(NULL);        // disconnect connection
        pConn->Close();
    }
}
catch(_com_error &e)
{
    _bstr_t bstrSource(e.Source());
    _bstr_t bs = _bstr_t(" Error: ") + _bstr_t(e.Error()) + _bstr_t(" Msg: ") 
        + _bstr_t(e.ErrorMessage()) + _bstr_t(" Description: ") 
        + _bstr_t(e.Description());
    
    MessageBox(0, bs, bstrSource, MB_OK);
}
.....


Но с компонентом начинается проблемы. Понятно, чтобы подать Command от VB в ATL и обратно, надо его передать через ByRef посредством [in,out] в MIDL, но в практике это не очень то работает. И чтобы было интереснее, работа с Command объектом произходит не в вызиваемой функции, а ещё пару етажей ниже — в виртуальнвх функциях. То есть получается такя цепочка:

---[ VIDDB.idl ]---
[
    uuid(3845B14E-E963-497C-AF2B-EDB3510ABE0F),
    version(1.0),
    helpstring("VID IA connection pool, 1.0")
]
library VIDDB
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");
    importlib("c:\program files\common files\system\ado\msado15.dll");  // import ADO typelib
.....
    [
        object,
        uuid(B2D05B37-EFBC-42B2-9FC0-D0EC85A9B9A6),
        dual,
        helpstring("Client session database connection interface"),
        pointer_default(unique)
    ]
    interface IPoolConnection : IDispatch
    {
.....
        [id(10), helpstring("Execute SQL command with parameters and return recordset, if available")] 
        HRESULT ExecuteCommand([in, out] _Command *ParamCommand, [out, optional] VARIANT *RecordsAffected, [in, defaultvalue(-1)] long Options, [out, retval] _Recordset **ppRS);
.....

---[ PoolConnection.h ]---
.....
// IPoolConnection
protected:
    CConnection m_conn;                // DB connection object
.....
public:
    STDMETHOD(ExecuteCommand)(/*[in, out]*/ _Command *pCommand, /*[out, optional]*/ VARIANT *lpvAffected, /*[in, defaultvalue(-1)]*/ long lOptions, /*[out, retval]*/ _Recordset **ppRS);
.....
---[ PoolConnection.cpp ]---

// Execute SQL command with parameters and return recordset, if available.
STDMETHODIMP CPoolConnection::ExecuteCommand(_Command *pCommand, VARIANT *lpvAffected, long lOptions, _Recordset **ppRS)
{
    m_conn.ClearErrors();
    m_conn.ExecuteCommand(pCommand, lpvAffected, lOptions, ppRS);
    return S_OK;
}

---[ Connection.h ]---
......
class CConnBase
{
....
public:
......
    virtual BOOL ExecuteCommand(_CommandPtr pCmd, VARIANT *lpvAffected, long lOptions, _Recordset **ppRS) = 0;
......

class CConnection
{
protected:
    CConnBase *m_pConn;
.....
public:
.....
    BOOL ExecuteCommand(_CommandPtr pCmd, VARIANT *lpvAffected, long lOptions, _Recordset **ppRS);
.....
---[ Connection.cpp ]---

.....
BOOL CConnection::ExecuteCommand(_CommandPtr pCmd, VARIANT *lpvAffected, long lOptions, _Recordset **ppRS)
{
    if (!CheckOpenConnection())
        return FALSE;

    if (!m_pConn->ExecuteCommand(pCmd, lpvAffected, lOptions, ppRS))
        m_lErrors++;

    return (m_lErrors == 0);
}

// Set/switch database connection.
BOOL CConnection::SetConnection(LPCTSTR lpszConnection)
{
    ATLASSERT(lpszConnection != NULL);

    // Do nothing, if connection is that same.
    if (m_cinfo.GetConnection().Compare(lpszConnection) == 0)
        return FALSE;

    // Close and remove previous connection.
    if (m_pConn != NULL)
    {
        if (IsConnected())
            Close();
        delete m_pConn;
        m_pConn = NULL;
    }

    // Load connection settings.
    if (!m_cinfo.Load(lpszConnection))
    {
        m_lErrors++;
        return FALSE;
    }

    // Create connection by type from loaded connection info.
    switch (m_cinfo.GetConnType()) {
    case ConnTypeADO:            // connection, using ADO
        m_pConn = new CConnADO;
        break;
    case ConnTypeInformix:        // direct Informix connection
        m_pConn = new CConnIfx;
        break;
    default:
        m_lErrors++;
        return FALSE;    // unknown connection type
    }
    
    m_pConn->SetConnection(&m_cinfo);    // reference to connection info
    m_pConn->SetOwner(m_strOwner);        // set owner name
    return TRUE;
}
.....
---[ ConnADO.h ]---

class CConnADO : public CConnBase 
{
    _ConnectionPtr m_pConn;            // ADO connection instance
.....
public:
    BOOL ExecuteCommand(_CommandPtr pCmd, VARIANT *lpvAffected, long lOptions, _Recordset **ppRS);
.....
};

---[ ConnADO.cpp ]---

// Execute ADO command with parameterized SQL statement.
BOOL CConnADO::ExecuteCommand(_CommandPtr pCmd, VARIANT *lpvAffected, long lOptions, _Recordset **ppRS)
{
    _RecordsetPtr pRS = NULL;
    BOOL bResult = FALSE;

    // Register start of command execution.
    long lCmdID = Pool.GetCommandManager()->CommandStart(m_pCInfo->GetConnection(), m_strOwner, (LPCTSTR)_bstr_t(pCmd->CommandText));
    
    try
    {
        pRS.CreateInstance(__uuidof(Recordset));

        // До этого места никаких проблем не наблюдается.
        // А ВОТ В СЛЕДУЮЩЕЙ СТРОКЕ МЫ ЛЯЖЕМ - ПРИЧЁМ НАГЛУХО! 

        pCmd->ActiveConnection = m_pConn;        // set active connection to command
        pRS = pCmd->Execute(lpvAffected, NULL, lOptions);
        // If records is returned (recordset is open), disconnect it from
        // connection and return to caller.
        if (pRS->State == adStateOpen)
        {
            pRS->PutRefActiveConnection(NULL);    // disconnect recordset from connection
            // Returned record count to caller.
            if (lpvAffected != NULL)
                *lpvAffected = _variant_t(pRS->RecordCount).Detach();
            *ppRS = pRS.Detach();                // return recordset to caller
        }
    }
    catch (const _com_error& e)
    {
        if (!GetProviderErrors())
            CMessage::Forward(m_pCInfo->GetConnection(), m_strOwner, e);
    }
    catch (...)
    {
        CMessage::ForwardFromSystem(m_pCInfo->GetConnection(), m_strOwner);
    }


    // Register command ending state.
    if (bResult)
        Pool.GetCommandManager()->CommandEnd(m_pCInfo->GetConnection(), m_strOwner, lCmdID);
    else
        Pool.GetCommandManager()->CommandEnd(m_pCInfo->GetConnection(), m_strOwner, lCmdID, CmdFailed);

    return bResult;
}


Из VB и ASP это визывается примерно так:

Dim cn As VIDDB.PoolConnection
Dim cmd As ADODB.Command
Dim prm As Parameter
Dim lRet As Long

    Set cn = New VIDDB.PoolConnection

.....

    Set cmd = New ADODB.Command
    cmd.CommandText = "{ ? = CALL VIES_API.VAT(0, 'lv', '90000084505', '1212')}"

    Set prm = cmd.CreateParameter("retval", adInteger, adParamReturnValue)
    cmd.Parameters.Append prm
    
    cn.ExecuteCommand cmd
    Debug.Print "Returned parameter value: " & cmd.Parameters(0).Value & vbNewLine


В конце получается то, что задаём команде параметер(ры), потом всё красиво доходит до места в ConnADO.cpp, где надо указать ActiveConnection — и тут летим в catch (const _com_error& e) c

Code: 800a0bb9; Meaning: Unknown error 0x800A0BB9; Source: ADODB.Connection; Description: Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another.


То же мы получаем, если хочем что нибудь сделать с параметрами этой команды, напримерь почистить и задать новые. Создаётся впечатление, что входящая команда сейчас вроде только для чтения. Подумал и указал входящую команду повсюду как _Command **ppCommand с последующим Attach() Detach() к _CommandPtr в конечной функции. Не помогло Может кто нибудь знает
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.