SSMS
 sql >> Database >  >> Database Tools >> SSMS

Oggetti SMO SSMS:ottieni risultati di query

La cosa più semplice è forse semplicemente stampare il numero che ottieni per ExecuteNonQuery :

int rowsAffected = server.ConnectionContext.ExecuteNonQuery(/* ... */);
if (rowsAffected != -1)
{
     Console.WriteLine("{0} rows affected.", rowsAffected);
}

Dovrebbe funzionare, ma non rispetterà il SET NOCOUNT impostazione della sessione/ambito corrente.

Altrimenti lo faresti come faresti con ADO.NET "semplice". Non utilizzare ServerConnection.ExecuteNonQuery() metodo, ma crea un SqlCommand oggetto accedendo al sottostante SqlConnection oggetto. Su quello iscriviti a StatementCompleted evento.

using (SqlCommand command = server.ConnectionContext.SqlConnectionObject.CreateCommand())
{
    // Set other properties for "command", like StatementText, etc.

    command.StatementCompleted += (s, e) => {
         Console.WriteLine("{0} row(s) affected.", e.RecordCount);
    };

    command.ExecuteNonQuery();
}

Utilizzo di StatementCompleted (invece, diciamo, stampando manualmente il valore che ExecuteNonQuery() restituito) ha il vantaggio di funzionare esattamente come SSMS o SQLCMD.EXE:

  • Per i comandi che non hanno un ROWCOUNT non verrà chiamato affatto (es. GO, USE).
  • Se SET NOCOUNT ON era impostato, non verrà chiamato affatto.
  • Se SET NOCOUNT OFF è stato impostato, verrà chiamato per ogni istruzione all'interno di un batch.

(Barra laterale:sembra StatementCompleted è esattamente ciò di cui parla il protocollo TDS quando DONE_IN_PROC l'evento è menzionato; vedere Osservazioni del comando SET NOCOUNT su MSDN.)

Personalmente, ho utilizzato questo approccio con successo nel mio "clone" di SQLCMD.EXE.

AGGIORNAMENTO :Va notato che questo approccio (ovviamente) richiede di dividere manualmente lo script/le dichiarazioni di input in GO separatore, perché sei tornato a usare SqlCommand.Execute*() che non può gestire più batch alla volta. Per questo, ci sono più opzioni:

  • Dividi manualmente l'input su righe che iniziano con GO (avvertenza:GO può essere chiamato come GO 5 , ad esempio, per eseguire il batch precedente 5 volte).
  • Utilizzare ManagedBatchParser class/library per aiutarti a suddividere l'input in singoli batch, in particolare implementare ICommandExecutor.ProcessBatch con il codice sopra (o qualcosa di simile).

Scelgo l'ultima opzione, che è stata un bel po' di lavoro, dato che non è abbastanza ben documentata e gli esempi sono rari (google un po', troverai alcune cose, o usa riflettore per vedere come gli SMO-Assemblies usano quella classe) .

Il vantaggio (e forse l'onere) dell'utilizzo di ManagedBatchParser vale a dire che analizzerà anche tutti gli altri costrutti di script T-SQL (destinati a SQLCMD.EXE ) per te. Compreso::setvar , :connect , :quit , ecc. Non è necessario implementare il rispettivo ICommandExecutor membri, se i tuoi script non li usano, ovviamente. Ma ricorda che potresti non essere in grado di eseguire script "arbitrari".

Bene, dove ti ha messo? Dalla "semplice domanda" di come stampare "...righe interessate" al fatto che non è banale farlo in maniera robusta e generica (visto il lavoro di fondo richiesto). YMMV, buona fortuna.

Aggiornamento sull'utilizzo ManagedBatchParser

Non sembra esserci una buona documentazione o esempio su come implementare IBatchSource , ecco cosa ho scelto.

internal abstract class BatchSource : IBatchSource
{
    private string m_content;

    public void Populate()
    {
        m_content = GetContent();
    }

    public void Reset()
    {
        m_content = null;
    }

    protected abstract string GetContent();

    public ParserAction GetMoreData(ref string str)
    {
        str = null;

        if (m_content != null)
        {
            str = m_content;
            m_content = null;
        }

        return ParserAction.Continue;
    }
}

internal class FileBatchSource : BatchSource
{
    private readonly string m_fileName;

    public FileBatchSource(string fileName)
    {
        m_fileName = fileName;
    }

    protected override string GetContent()
    {
        return File.ReadAllText(m_fileName);
    }
}

internal class StatementBatchSource : BatchSource
{
    private readonly string m_statement;

    public StatementBatchSource(string statement)
    {
        m_statement = statement;
    }

    protected override string GetContent()
    {
        return m_statement;
    }
}

Ed ecco come lo useresti:

var source = new StatementBatchSource("SELECT GETUTCDATE()");
source.Populate();

var parser = new Parser(); 
parser.SetBatchSource(source);
/* other parser.Set*() calls */

parser.Parse();

Nota che entrambe le implementazioni, sia per le istruzioni dirette (StatementBatchSource ) o per un file (FileBatchSource ) hanno il problema di leggere subito il testo completo in memoria. Ho avuto un caso in cui è esploso, con un enorme (!) script con miliardi di INSERT generati dichiarazioni. Anche se non penso che sia un problema pratico, SQLCMD.EXE potrebbe gestirlo. Ma per la mia vita, non riuscivo a capire esattamente come, avresti bisogno di formare i blocchi restituiti per IBatchParser.GetContent() in modo che il parser possa ancora lavorare con loro (sembra che dovrebbero essere dichiarazioni complete, il che vanificherebbe lo scopo dell'analisi in primo luogo...).