// Copyright (c) 2008-2023 the Urho3D project // License: MIT #include #include #include #include #include #include #include #include #include #include #include "DatabaseDemo.h" // Expands to this example's entry-point URHO3D_DEFINE_APPLICATION_MAIN(DatabaseDemo) DatabaseDemo::DatabaseDemo(Context* context) : Sample(context), connection_(nullptr), row_(0), maxRows_(50) { } DatabaseDemo::~DatabaseDemo() { // 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. GetSubsystem()->Disconnect(connection_); connection_ = nullptr; } void DatabaseDemo::Start() { // Execute base class startup Sample::Start(); // Subscribe to console commands and the frame update SubscribeToEvent(E_CONSOLECOMMAND, URHO3D_HANDLER(DatabaseDemo, HandleConsoleCommand)); SubscribeToEvent(E_UPDATE, URHO3D_HANDLER(DatabaseDemo, HandleUpdate)); // Subscribe key down event SubscribeToEvent(E_KEYDOWN, URHO3D_HANDLER(DatabaseDemo, HandleEscKeyDown)); // Hide logo to make room for the console SetLogoVisible(false); // Show the console by default, make it large. Console will show the text edit field when there is at least one // subscriber for the console command event auto* console = GetSubsystem(); console->SetNumRows((unsigned)(GetSubsystem()->GetHeight() / 16)); console->SetNumBufferedRows(2 * console->GetNumRows()); console->SetCommandInterpreter(GetTypeName()); console->SetVisible(true); console->GetCloseButton()->SetVisible(false); // Show OS mouse cursor GetSubsystem()->SetMouseVisible(true); // Set the mouse mode to use in the sample Sample::InitMouseMode(MM_FREE); // Open the operating system console window (for stdin / stdout) if not open yet OpenConsoleWindow(); // In general, the connection string is really the only thing that need to be changed when switching underlying database API // and that when using ODBC API then the connection string must refer to an already installed ODBC driver // Although it has not been tested yet but the ODBC API should be able to interface with any vendor provided ODBC drivers // In this particular demo, however, when using ODBC API then the SQLite-ODBC driver need to be installed // The SQLite-ODBC driver can be built from source downloaded from http://www.ch-werner.de/sqliteodbc/ // You can try to install other ODBC driver and modify the connection string below to match your ODBC driver // Both DSN and DSN-less connection string should work // The ODBC API, i.e. URHO3D_DATABASE_ODBC build option, is only available for native (including RPI) platforms // and it is designed for development of game server connecting to ODBC-compliant databases in mind // 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 Web platform // 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 connection_ = GetSubsystem()->Connect(Database::GetAPI() == DBAPI_ODBC ? "Driver=SQLite3;Database=:memory:" : "file://"); // Subscribe to database cursor event to loop through query resultset SubscribeToEvent(E_DBCURSOR, URHO3D_HANDLER(DatabaseDemo, HandleDbCursor)); // Show instruction Print("This demo connects to temporary in-memory database.\n" "All the tables and their data will be lost after exiting the demo.\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 "); HandleInput("create table tbl1(col1 varchar(10), col2 smallint)"); HandleInput("insert into tbl1 values('Hello', 10)"); HandleInput("insert into tbl1 values('World', 20)"); HandleInput("select * from tbl1"); } void DatabaseDemo::HandleConsoleCommand(StringHash eventType, VariantMap& eventData) { using namespace ConsoleCommand; if (eventData[P_ID].GetString() == GetTypeName()) HandleInput(eventData[P_COMMAND].GetString()); } void DatabaseDemo::HandleUpdate(StringHash eventType, VariantMap& eventData) { // Check if there is input from stdin String input = GetConsoleInput(); if (input.Length()) HandleInput(input); } void DatabaseDemo::HandleEscKeyDown(StringHash eventType, VariantMap& eventData) { // Unlike the other samples, exiting the engine when ESC is pressed instead of just closing the console if (eventData[KeyDown::P_KEY].GetI32() == KEY_ESCAPE) engine_->Exit(); } void DatabaseDemo::HandleDbCursor(StringHash eventType, VariantMap& eventData) { using namespace DbCursor; // In a real application the P_SQL can be used to do the logic branching in a shared event handler // However, this is not required in this sample demo unsigned numCols = eventData[P_NUMCOLS].GetU32(); const VariantVector& colValues = eventData[P_COLVALUES].GetVariantVector(); const Vector& colHeaders = eventData[P_COLHEADERS].GetStringVector(); // In this sample demo we just use db cursor to dump each row immediately so we can filter out the row to conserve memory // In a real application this can be used to perform the client-side filtering logic eventData[P_FILTER] = true; // In this sample demo we abort the further cursor movement when maximum rows being dumped has been reached eventData[P_ABORT] = ++row_ >= maxRows_; for (unsigned i = 0; i < numCols; ++i) Print(ToString("Row #%d: %s = %s", row_, colHeaders[i].CString(), colValues[i].ToString().CString())); } void DatabaseDemo::HandleInput(const String& input) { // Echo input string to stdout Print(input); row_ = 0; if (input == "quit" || input == "exit") engine_->Exit(); else if (input.StartsWith("set") || input.StartsWith("get")) { // We expect a key/value pair for 'set' command Vector tokens = input.Substring(3).Split(' '); String setting = tokens.Size() ? tokens[0] : ""; if (input.StartsWith("set") && tokens.Size() > 1) { if (setting == "maxrows") maxRows_ = Max(ToU32(tokens[1]), 1U); else if (setting == "connstr") { String newConnectionString(input.Substring(input.Find(" ", input.Find("connstr")) + 1)); auto* database = GetSubsystem(); 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())); } else Print("Missing setting paramater. Recognized settings are: maxrows, connstr"); } 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); // Number of affected rows is only meaningful for DML statements like insert/update/delete if (result.GetNumAffectedRows() != -1) Print(ToString("Number of affected rows: %d", result.GetNumAffectedRows())); } Print(" "); } void DatabaseDemo::Print(const String& output) { // Logging appears both in the engine console and stdout URHO3D_LOGRAW(output + "\n"); }