Browse Source

Use numCols to determine whether a result object impl has resultset.
Only supports one resultset per execution at the moment.
Enhance db sample demo to accept connectiong string setting to connect to different database during runtime.
[skip ci]

Yao Wei Tjong 姚伟忠 10 years ago
parent
commit
33145bf18f

+ 9 - 5
CMake/Modules/FindODBC.cmake

@@ -26,6 +26,7 @@
 #  ODBC_INCLUDE_DIRS
 #  ODBC_INCLUDE_DIRS
 #  ODBC_LIBRARIES
 #  ODBC_LIBRARIES
 #  ODBC_DEFINES
 #  ODBC_DEFINES
+#  ODBC_VERSION
 #
 #
 
 
 if (ODBC_FOUND)
 if (ODBC_FOUND)
@@ -33,21 +34,25 @@ if (ODBC_FOUND)
 endif ()
 endif ()
 
 
 if (WIN32)
 if (WIN32)
-    set (ODBC_INCLUDE_DIRS)   # The headers should be available in the default include search path
+    set (ODBC_INCLUDE_DIRS)     # The headers should be available in the default include search path
     set (ODBC_LIBRARIES odbc32) # This should be also the case for MinGW cross-compiler toolchain
     set (ODBC_LIBRARIES odbc32) # This should be also the case for MinGW cross-compiler toolchain
+    set (ODBC_DEFINES)
+    set (ODBC_VERSION 3)        # Assume it is 3
     set (ODBC_FOUND 1)
     set (ODBC_FOUND 1)
 else ()
 else ()
     # On Unix-like host system, use the ODBC config tool
     # On Unix-like host system, use the ODBC config tool
     find_program (ODBC_CONFIG NAMES odbc_config iodbc-config DOC "ODBC config tool" NO_CMAKE_FIND_ROOT_PATH)
     find_program (ODBC_CONFIG NAMES odbc_config iodbc-config DOC "ODBC config tool" NO_CMAKE_FIND_ROOT_PATH)
     if (ODBC_CONFIG)
     if (ODBC_CONFIG)
         # Get ODBC compile and link flags and turn them into include dirs and libraries list
         # Get ODBC compile and link flags and turn them into include dirs and libraries list
-        execute_process(COMMAND ${ODBC_CONFIG} --cflags OUTPUT_VARIABLE ODBC_COMPILE_FLAGS OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
-        execute_process(COMMAND ${ODBC_CONFIG} --libs OUTPUT_VARIABLE ODBC_LINK_FLAGS OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
+        execute_process (COMMAND ${ODBC_CONFIG} --cflags OUTPUT_VARIABLE ODBC_COMPILE_FLAGS OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
+        execute_process (COMMAND ${ODBC_CONFIG} --libs OUTPUT_VARIABLE ODBC_LINK_FLAGS OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
         string (REGEX REPLACE " *-I" ";" ODBC_INCLUDE_DIRS "${ODBC_COMPILE_FLAGS}")    # Stringify in case it returns empty string
         string (REGEX REPLACE " *-I" ";" ODBC_INCLUDE_DIRS "${ODBC_COMPILE_FLAGS}")    # Stringify in case it returns empty string
         string (REGEX REPLACE " *-l" ";" ODBC_LIBRARIES "${ODBC_LINK_FLAGS}")
         string (REGEX REPLACE " *-l" ";" ODBC_LIBRARIES "${ODBC_LINK_FLAGS}")
         list (GET ODBC_INCLUDE_DIRS 0 ODBC_DEFINES)     # Assume the list of defines always come before the list of include dirs
         list (GET ODBC_INCLUDE_DIRS 0 ODBC_DEFINES)     # Assume the list of defines always come before the list of include dirs
         list (REMOVE_AT ODBC_INCLUDE_DIRS 0)
         list (REMOVE_AT ODBC_INCLUDE_DIRS 0)
         list (REMOVE_AT ODBC_LIBRARIES 0)
         list (REMOVE_AT ODBC_LIBRARIES 0)
+        # Get ODBC version
+        execute_process (COMMAND ${ODBC_CONFIG} --odbcversion OUTPUT_VARIABLE ODBC_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
         set (ODBC_FOUND 1)
         set (ODBC_FOUND 1)
     endif ()
     endif ()
 endif ()
 endif ()
@@ -59,5 +64,4 @@ elseif (ODBC_FIND_REQUIRED)
     message (FATAL_ERROR "Could not find ODBC driver manager, please install the development package of unixODBC or libiodbc")
     message (FATAL_ERROR "Could not find ODBC driver manager, please install the development package of unixODBC or libiodbc")
 endif ()
 endif ()
 
 
-mark_as_advanced (ODBC_INCLUDE_DIRS ODBC_LIBRARIES ODBC_DEFINES ODBC_CONFIG)
-
+mark_as_advanced (ODBC_INCLUDE_DIRS ODBC_LIBRARIES ODBC_DEFINES ODBC_VERSION ODBC_CONFIG)

+ 38 - 9
Source/Samples/41_DatabaseDemo/DatabaseDemo.cpp

@@ -52,6 +52,7 @@ DatabaseDemo::~DatabaseDemo()
     // Although the managed database connection will be disconnected by Database subsystem automatically in its destructor,
     // Although the managed database connection will be disconnected by Database subsystem automatically in its destructor,
     // it is a good practice for a class to balance the number of connect() and disconnect() calls.
     // it is a good practice for a class to balance the number of connect() and disconnect() calls.
     GetSubsystem<Database>()->Disconnect(connection_);
     GetSubsystem<Database>()->Disconnect(connection_);
+    connection_ = 0;
 }
 }
 
 
 void DatabaseDemo::Start()
 void DatabaseDemo::Start()
@@ -97,18 +98,22 @@ void DatabaseDemo::Start()
     // This demo will always work when using SQLite API as the SQLite database engine is embedded inside Urho3D game engine
     // This demo will always work when using SQLite API as the SQLite database engine is embedded inside Urho3D game engine
     //   and this is also the case when targeting HTML5 in Emscripten build
     //   and this is also the case when targeting HTML5 in Emscripten build
 
 
+    // We could have used #ifdef to init the connection string during compile time, but below shows how it is done during runtime
+    // The "URHO3D_DATABASE_ODBC" compiler define is set when URHO3D_DATABASE_ODBC build option is enabled
     // Connect to a temporary in-memory SQLite database
     // Connect to a temporary in-memory SQLite database
     connection_ =
     connection_ =
         GetSubsystem<Database>()->Connect(Database::GetAPI() == DBAPI_ODBC ? "Driver=SQLITE3;Database=:memory:" : "file://");
         GetSubsystem<Database>()->Connect(Database::GetAPI() == DBAPI_ODBC ? "Driver=SQLITE3;Database=:memory:" : "file://");
 
 
     // Subscribe to database cursor event to loop through query resultset
     // Subscribe to database cursor event to loop through query resultset
-    SubscribeToEvent(connection_, E_DBCURSOR, HANDLER(DatabaseDemo, HandleDbCursor));
+    SubscribeToEvent(E_DBCURSOR, HANDLER(DatabaseDemo, HandleDbCursor));
 
 
     // Show instruction
     // Show instruction
     Print("This demo connects to temporary in-memory database.\n"
     Print("This demo connects to temporary in-memory database.\n"
         "All the tables and their data will be lost after exiting the demo.\n"
         "All the tables and their data will be lost after exiting the demo.\n"
-        "Enter 'quit' or 'exit' to exit the demo. Enter 'set maxrows=n' to set the maximum rows to be printed out.\n"
         "Enter a valid SQL statement in the console input and press Enter to execute.\n"
         "Enter a valid SQL statement in the console input and press Enter to execute.\n"
+        "Enter 'get/set maxrows [number]' to get/set the maximum rows to be printed out.\n"
+        "Enter 'get/set connstr [string]' to get/set the database connection string and establish a new connection to it.\n"
+        "Enter 'quit' or 'exit' to exit the demo.\n"
         "For example:\n ");
         "For example:\n ");
     HandleInput("create table tbl1(col1 varchar(10), col2 smallint)");
     HandleInput("create table tbl1(col1 varchar(10), col2 smallint)");
     HandleInput("insert into tbl1 values('Hello', 10)");
     HandleInput("insert into tbl1 values('Hello', 10)");
@@ -168,21 +173,45 @@ void DatabaseDemo::HandleInput(const String& input)
     else if (input.StartsWith("set") || input.StartsWith("get"))
     else if (input.StartsWith("set") || input.StartsWith("get"))
     {
     {
         // We expect a key/value pair for 'set' command
         // We expect a key/value pair for 'set' command
-        Vector<String> tokens = input.Substring(3).Trimmed().Split('=');
-        if (input.StartsWith("set"))
+        Vector<String> tokens = input.Substring(3).Split(' ');
+        String setting = tokens.Size() ? tokens[0] : "";
+        if (input.StartsWith("set") && tokens.Size() > 1)
         {
         {
-            if (tokens[0].Trimmed() == "maxrows")
+            if (setting == "maxrows")
                 maxRows_ = (unsigned)Max(ToUInt(tokens[1]), 1);
                 maxRows_ = (unsigned)Max(ToUInt(tokens[1]), 1);
+            else if (setting == "connstr")
+            {
+                String newConnectionString(input.Substring(input.Find(" ", input.Find("connstr")) + 1));
+                Database* database = GetSubsystem<Database>();
+                DbConnection* newConnection = database->Connect(newConnectionString);
+                if (newConnection)
+                {
+                    database->Disconnect(connection_);
+                    connection_ = newConnection;
+                }
+            }
+        }
+        if (tokens.Size())
+        {
+            if (setting == "maxrows")
+                Print(ToString("maximum rows is set to %d", maxRows_));
+            else if (setting == "connstr")
+                Print(ToString("connection string is set to %s", connection_->GetConnectionString().CString()));
+            else
+                Print(ToString("Unrecognized setting: %s", setting.CString()));
         }
         }
-        if (tokens[0].Trimmed() == "maxrows")
-            Print(ToString("maxrows is set to %d", maxRows_));
         else
         else
-            Print("Unrecognized setting");
+            Print("Missing setting paramater. Recognized settings are: maxrows, connstr");
     }
     }
     else
     else
     {
     {
+        // In this sample demo we use the dbCursor event to loop through each row as it is being fetched
+        // Regardless of this event is being used or not, all the fetched rows will be made available in the DbResult object,
+        //   unless the dbCursor event handler has instructed to filter out the fetched row from the final result
         DbResult result = connection_->Execute(input, true);
         DbResult result = connection_->Execute(input, true);
-        if (result.NumAffectedRows() > 0)
+
+        // Number of affected rows is only meaningful for DML statements like insert/update/delete
+        if (result.NumAffectedRows() != -1)
             Print(ToString("Number of affected rows: %d", result.NumAffectedRows()));
             Print(ToString("Number of affected rows: %d", result.NumAffectedRows()));
     }
     }
     Print(" ");
     Print(" ");

+ 5 - 1
Source/Urho3D/CMakeLists.txt

@@ -24,6 +24,9 @@
 set (TARGET_NAME Urho3D)
 set (TARGET_NAME Urho3D)
 
 
 add_definitions (-DURHO3D_IS_BUILDING)
 add_definitions (-DURHO3D_IS_BUILDING)
+if (ODBC_VERSION AND NOT ODBC_VERSION VERSION_LESS 3)
+    add_definitions (-DODBC_3_OR_LATER)
+endif ()
 
 
 # Windows platform has its own naming convention for library
 # Windows platform has its own naming convention for library
 set (LIB_NAME ${TARGET_NAME})
 set (LIB_NAME ${TARGET_NAME})
@@ -327,7 +330,8 @@ install_header_files (FILES ${CMAKE_CURRENT_BINARY_DIR}/librevision.h ${CMAKE_CU
 
 
 # Generate platform specific pkg-config file for the benefit of Urho3D library users via SDK without CMake
 # Generate platform specific pkg-config file for the benefit of Urho3D library users via SDK without CMake
 get_directory_property (URHO3D_COMPILE_DEFINITIONS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMPILE_DEFINITIONS)
 get_directory_property (URHO3D_COMPILE_DEFINITIONS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMPILE_DEFINITIONS)
-list (REMOVE_ITEM URHO3D_COMPILE_DEFINITIONS HAVE_STDINT_H GLEW_STATIC GLEW_NO_GLU)     # Remove those defines that are only used for building the library and not needed by library user
+# Remove those defines that are only used for building the library and not needed by library user
+list (REMOVE_ITEM URHO3D_COMPILE_DEFINITIONS HAVE_STDINT_H GLEW_STATIC GLEW_NO_GLU URHO3D_IS_BUILDING ODBC_3_OR_LATER)
 if (ABSOLUTE_PATH_LIBS)
 if (ABSOLUTE_PATH_LIBS)
     string (REPLACE ";" "\" \"" URHO3D_ABS_PATH_LIBS "\"${ABSOLUTE_PATH_LIBS}\"")   # Note: need to always "stringify" a variable in list context for replace to work correctly, besides the list could be empty
     string (REPLACE ";" "\" \"" URHO3D_ABS_PATH_LIBS "\"${ABSOLUTE_PATH_LIBS}\"")   # Note: need to always "stringify" a variable in list context for replace to work correctly, besides the list could be empty
     string (REPLACE "${SYSROOT}" "\${pc_sysrootdir}" URHO3D_ABS_PATH_LIBS "${URHO3D_ABS_PATH_LIBS}")
     string (REPLACE "${SYSROOT}" "\${pc_sysrootdir}" URHO3D_ABS_PATH_LIBS "${URHO3D_ABS_PATH_LIBS}")

+ 1 - 1
Source/Urho3D/Database/Database.cpp

@@ -30,7 +30,7 @@ namespace Urho3D
 
 
 Database::Database(Context* context_) :
 Database::Database(Context* context_) :
     Object(context_),
     Object(context_),
-#ifdef URHO3D_DATABASE_ODBC
+#ifdef ODBC_3_OR_LATER
     usePooling_(false)
     usePooling_(false)
 #else
 #else
     usePooling_(true)
     usePooling_(true)

+ 3 - 3
Source/Urho3D/Database/Database.h

@@ -52,20 +52,20 @@ public:
 
 
     /// Create new database connection. Return 0 if failed.
     /// Create new database connection. Return 0 if failed.
     DbConnection* Connect(const String& connectionString);
     DbConnection* Connect(const String& connectionString);
-    /// Disconnect a database connection.
+    /// Disconnect a database connection. The connection object pointer should not be used anymore after this.
     void Disconnect(DbConnection* connection);
     void Disconnect(DbConnection* connection);
 
 
     /// Return true when using internal database connection pooling. The internal database pooling is managed by the Database subsystem itself and should not be confused with ODBC connection pooling option when ODBC is being used.
     /// Return true when using internal database connection pooling. The internal database pooling is managed by the Database subsystem itself and should not be confused with ODBC connection pooling option when ODBC is being used.
     bool IsUsePooling() const { return usePooling_; }
     bool IsUsePooling() const { return usePooling_; }
 
 
-    /// Set whether to use internal database connection pooling. The internal database pooling is managed by the Database subsystem itself and should not be confused with ODBC connection pooling option when ODBC is being used.
+    /// Set whether to use internal database connection pooling. The internal database pooling is managed by the Database subsystem itself and should not be confused with ODBC connection pooling option when ODBC driver manager version 3.0 or later is being used.
     void SetUsePooling(bool usePooling) { usePooling_ = usePooling; }
     void SetUsePooling(bool usePooling) { usePooling_ = usePooling; }
 
 
 private:
 private:
     /// Internal helper to disconnect all the database connections in the collection.
     /// Internal helper to disconnect all the database connections in the collection.
     void DisconnectAll(PODVector<DbConnection*>& collection);
     void DisconnectAll(PODVector<DbConnection*>& collection);
 
 
-    /// Using database connection pool flag. Default to false when using ODBC as ODBC driver manager manages database connection pooling better.
+    /// Using database connection pool flag. Default to false when using ODBC 3.0 or later as ODBC 3.0 driver manager manages its database connection pooling.
     bool usePooling_;
     bool usePooling_;
     /// Active database connections.
     /// Active database connections.
     PODVector<DbConnection*> connections_;
     PODVector<DbConnection*> connections_;

+ 12 - 5
Source/Urho3D/Database/ODBC/ODBCConnection.cpp

@@ -40,7 +40,7 @@ DbConnection::DbConnection(Context* context, const String& connectionString) :
     }
     }
     catch (std::runtime_error& e)
     catch (std::runtime_error& e)
     {
     {
-        LOGERRORF("Could not connect: %s", e.what());
+        HandleRuntimeError("Could not connect", e.what());
     }
     }
 }
 }
 
 
@@ -54,7 +54,7 @@ DbConnection::~DbConnection()
     catch (std::runtime_error& e)
     catch (std::runtime_error& e)
     {
     {
         // This should not happen after finalizing the connection, log error in Release but assert in Debug
         // This should not happen after finalizing the connection, log error in Release but assert in Debug
-        LOGERRORF("Could not disconnect: %s", e.what());
+        HandleRuntimeError("Could not disconnect", e.what());
         assert(false);
         assert(false);
     }
     }
 }
 }
@@ -71,9 +71,9 @@ DbResult DbConnection::Execute(const String& sql, bool useCursorEvent)
     try
     try
     {
     {
         result.resultImpl_ = nanodbc::execute(connectionImpl_, sql.Trimmed().CString());
         result.resultImpl_ = nanodbc::execute(connectionImpl_, sql.Trimmed().CString());
-        if (!result.resultImpl_.affected_rows())
+        short numCols = result.resultImpl_.columns();
+        if (numCols)
         {
         {
-            short numCols = result.resultImpl_.columns();
             result.columns_.Resize((unsigned)numCols);
             result.columns_.Resize((unsigned)numCols);
             for (short i = 0; i < numCols; ++i)
             for (short i = 0; i < numCols; ++i)
                 result.columns_[i] = result.resultImpl_.column_name(i).c_str();
                 result.columns_[i] = result.resultImpl_.column_name(i).c_str();
@@ -139,13 +139,20 @@ DbResult DbConnection::Execute(const String& sql, bool useCursorEvent)
                     break;
                     break;
             }
             }
         }
         }
+        result.numAffectedRows_ = numCols ? -1 : result.resultImpl_.affected_rows();
     }
     }
     catch (std::runtime_error& e)
     catch (std::runtime_error& e)
     {
     {
-        LOGERRORF("Could not execute: %s", e.what());
+        HandleRuntimeError("Could not execute", e.what());
     }
     }
 
 
     return result;
     return result;
 }
 }
 
 
+void DbConnection::HandleRuntimeError(const char* message, const char* cause)
+{
+    Vector<String> tokens = (String(cause) + "::").Split(':');      // Added "::" as sentinels against unexpected cause format
+    LOGERRORF("%s: nanodbc:%s:%s", message, tokens[1].CString(), tokens[2].CString());
+}
+
 }
 }

+ 3 - 0
Source/Urho3D/Database/ODBC/ODBCConnection.h

@@ -56,6 +56,9 @@ public:
     bool IsConnected() const { return connectionImpl_.connected(); }
     bool IsConnected() const { return connectionImpl_.connected(); }
 
 
 private:
 private:
+    /// Internal helper method to handle runtime exception by logging it to stderr stream.
+    void HandleRuntimeError(const char* message, const char* cause);
+
     /// The connection string for SQLite3 is using the URI format described in https://www.sqlite.org/uri.html, while the connection string for ODBC is using DSN format as per ODBC standard.
     /// The connection string for SQLite3 is using the URI format described in https://www.sqlite.org/uri.html, while the connection string for ODBC is using DSN format as per ODBC standard.
     String connectionString_;
     String connectionString_;
     /// The underlying implementation connection object.
     /// The underlying implementation connection object.

+ 9 - 1
Source/Urho3D/Database/ODBC/ODBCResult.h

@@ -35,6 +35,12 @@ class URHO3D_API DbResult
     friend class DbConnection;
     friend class DbConnection;
 
 
 public:
 public:
+    // Default constructor constructs an empty result object.
+    DbResult() :
+        numAffectedRows_(-1)
+    {
+    }
+
     /// Return number of columns in the resultset or 0 if there is no resultset.
     /// Return number of columns in the resultset or 0 if there is no resultset.
     int NumColumns() const { return columns_.Size(); }
     int NumColumns() const { return columns_.Size(); }
 
 
@@ -42,7 +48,7 @@ public:
     long NumRows() const { return rows_.Size(); }
     long NumRows() const { return rows_.Size(); }
 
 
     /// Return number of affected rows by the DML query or -1 if the number of affected rows is not available.
     /// Return number of affected rows by the DML query or -1 if the number of affected rows is not available.
-    long NumAffectedRows() const { return resultImpl_ ? resultImpl_.affected_rows() : -1; }
+    long NumAffectedRows() const { return numAffectedRows_; }
 
 
     /// Return the underlying implementation result object.
     /// Return the underlying implementation result object.
     const nanodbc::result& GetResultImpl() const { return resultImpl_; }
     const nanodbc::result& GetResultImpl() const { return resultImpl_; }
@@ -60,6 +66,8 @@ private:
     Vector<String> columns_;
     Vector<String> columns_;
     /// Fetched rows from the resultset.
     /// Fetched rows from the resultset.
     Vector<VariantVector> rows_;
     Vector<VariantVector> rows_;
+    /// Number of affected rows by recent DML query.
+    long numAffectedRows_;
 };
 };
 
 
 }
 }

+ 2 - 2
Source/Urho3D/Database/SQLite/SQLiteConnection.cpp

@@ -86,7 +86,7 @@ DbResult DbConnection::Execute(const String& sql, bool useCursorEvent)
     bool filtered = false;
     bool filtered = false;
     bool aborted = false;
     bool aborted = false;
 
 
-    while (1)
+    while (numCols)
     {
     {
         rc = sqlite3_step(pStmt);
         rc = sqlite3_step(pStmt);
         if (rc == SQLITE_ROW)
         if (rc == SQLITE_ROW)
@@ -152,7 +152,7 @@ DbResult DbConnection::Execute(const String& sql, bool useCursorEvent)
         }
         }
     }
     }
 
 
-    result.numAffectedRows_ = sqlite3_stmt_readonly(pStmt) ? -1 : sqlite3_changes(connectionImpl_);
+    result.numAffectedRows_ = numCols ? -1 : sqlite3_changes(connectionImpl_);
     return result;
     return result;
 }
 }