C# singleton pro přístup k více mySQL databázím

Petr Wiedemann, 4. duben 2009

Při psaní aplikací v C# jsem se ze začátku setkával s problémem, jak přistupovat k mySQL databázi z více tříd v programu. V C# neexistují globální proměnné, jako je tomu například v C++. Pokud máme v aplikaci například více oken odvozených od třídy Form a každé z nich musí komunikovat s databází, nemůžeme se spolehnout na globální instanci MySqlConnection, protože ve stylu C++ jí nevytvoříme. Jednou z možností, jak se spojit s databází je vytvořit si k ní nové spojení při každém otevření okna. Tento způsob ale asi nebude moc efektivní, přeci jen inicializace spojení k serveru stojí nějaký čas a asi je zbytečné, když bude mít jedna aplikace aktivní 3 spojení s databází. Pro tento problém si můžeme vytvořit singleton třídu, která bude uchovávat pole všech použitých MySqlConnection objektů.

Vytvoříme si tedy třídu ConnectionPool s následujícími vlastnostmi:

  • poskytuje statickou metodu Instance, kterou stačí zavolat jen jednou před 1. použití a která vytvoří instanci samotné třídy
  • nabízí metodu GetConnection, kterou získáme objekt třídy MySqlConnection z pole
  • metodou AddConnection přidáme do pole nové spojení k databázi

Třída ConnectionPool vnitřně pracuje s polem tříd SingleConnection, která si udržuje objekt MySqlConnection a která sama zajišťuje spojení s databázovým serverem. Při neúspěchu vrací zpět informaci o chybě Message z vyjímky MySqlException.

Nejprve tedy vytvoříme třídu SingleConnection, se kterou budeme následně pracovat. Obsahuje pouze konstruktor, vlastnost IsConnectionOpen a 2 proměnné. Proměnné, které budeme potřebovat jsou objekty tříd MySqlConnection, který udržuje spojení k mySQL a String který obsahuje popis chyby, pokud selže volání metody Open třídy MySqlConnection. Konstruktoru předáváme adresu mySQL serveru, jméno uživatele, heslo, název databáze, příkaz, který se má provést po úspěšném připojení k serveru a případný suffix, který se připojí ke ConnectionStringu.

Implementace třídy SingleConnection

Parametry konstruktoru dbInitCommand a dbConnectionStringSuffix si zaslouží trochu vysvětlení.

  • dbInitCommand se provede při úspěšném spojení s databází. Využít se může například pro určení znakové sady klienta. Například set names cp1250;
  • dbConnectionStringSuffix se zase připojí ke ConnectionStringu ještě před voláním metody Open třídy MySqlConnection. Například CharSet=latin2;

Výpis třídy SingleConnection:

class SingleConnection
{
  public MySqlConnection Connection = null;
  public String ConnectionErrorString = "";

  public SingleConnection(String dbServer,
    String dbUser, String dbPassword, String dbDatabase,
    String dbInitCommand, String dbConnectionStringSuffix)
  {
    if (IsConnectionOpen)
      Connection.Close();

    ConnectionErrorString = "";
    String connStr = String.Format("server={0}; user id={1}; password={2}; database={3}; pooling=false;",
      dbServer, dbUser, dbPassword, dbDatabase);
    connStr += dbConnectionStringSuffix;

    try
    {
      Connection = new MySqlConnection(connStr);
      Connection.Open();

      if (dbInitCommand.Length > 0)
      {
        MySqlCommand cmdIniCP = new MySqlCommand(dbInitCommand, Connection);
        cmdIniCP.ExecuteNonQuery();
      }
    }
    catch (MySqlException ex)
    {
      ConnectionErrorString = ex.Message;
      if (Connection != null)
      {
        Connection.Close();
        Connection = null;
      }
    }
  }

  public bool IsConnectionOpen
  {
    get
    {
      if (Connection == null)
        return false;

      if (Connection.State == System.Data.ConnectionState.Open)
        return true;
      else
        return false;
    }
  }
}

Vlastnost IsConnectionOpen pouze zkontroluje, jestli existuje objekt MySqlConnection a jestli je jeho stav roven System.Data.ConnectionState.Open. Je následně využitá třídou ConnectionPool.

Implementace třídy ConnectionPool

ConnectionPool udržuje seznam aktivních spojení k databázím v poli Connections, které je tvořeno třídou Dictionary.

static Dictionary<String, SingleConnection> Connections = new Dictionary<String,
SingleConnection>();

Před 1. použitím třídy musíme zavolat její metodu Instance, která vytvoří novou instanci ConnectionPool ale pouze v případě, že ještě neexistuje. Všechny metody třídy ConnectionPool jsou statické. Mimo Instance máme ještě k dispozici metodu GetConnection, která vrací objekt typu MySqlConnection z pole Connections. Další dostupnou metodou je AddConnection, která zajistí vytvoření nového spojení k databázi, pokud ještě neexistuje.

Výpis třídy ConnectionPool:

public class ConnectionPool
{
    static readonly object padlock = new object();
    static ConnectionPool instance = null;
    static Dictionary<String, SingleConnection> Connections = new Dictionary<String, SingleConnection>();

    // jedina instance tridy ConnectionPool spolecna celemu programu
    public static ConnectionPool Instance
    {
        get
        {
            lock (padlock)
            {
                if (instance == null)
                {
                    instance = new ConnectionPool();
                }

                return instance;
            }
        }
    }

    // konstruktor tridy volany pri 1. pouziti vlastnosti Instance
    private ConnectionPool()
    {
        // odstraneni vsech spojeni na databazi v poli
        Connections.Clear();
    }

    // escape uvozovek a stredniku
    static public String EscapeData(String srcStr)
    {
        String retStr = srcStr.Trim();
        retStr = retStr.Replace("\\", "\\\\");
        retStr = retStr.Replace("'", "\\'");
        retStr = retStr.Replace("\"", "\\\"");

        return retStr;
    }

    // vraci MySqlConnection urciteho spojeni
    static public MySqlConnection GetConnection(String ConnectionName)
    {
        MySqlConnection conn = null;
        try
        {
            conn = Connections[ConnectionName].Connection;
        }
        catch (KeyNotFoundException)
        {
            return null;
        }
        return conn;
    }

    // vytvori nove spojeni s mySQL v poli
    static public MySqlConnection AddConnection(String ConnectionName,
        String ConnectionServer, String ConnectionUser,
        String ConnectionPassword, String ConnectionDatabase,
        String ConnectionInitCommand, String ConnectionConnectionStringSuffix,
        out String ErrorString)
    {
        ErrorString = "";
        // test, jestli toto spojeni jiz existuje
        MySqlConnection conn = GetConnection(ConnectionName);
        if (conn == null)
        {
            // vytvoreni noveho
            SingleConnection singConn = new SingleConnection(ConnectionServer,
                ConnectionUser, ConnectionPassword, ConnectionDatabase,
                ConnectionInitCommand, ConnectionConnectionStringSuffix);
            // vyskytla se chyba?
            if (singConn.ConnectionErrorString.Length > 0)
            {
                // ulozeni popisu chyby
                ErrorString = singConn.ConnectionErrorString;
                // funkce vraci null (nastaveno vysledkem volani funkce GetConnection(ConnectionName))
            }
            else
            {
                // pridani noveho spojeni do pole
                Connections.Add(ConnectionName, singConn);
                conn = GetConnection(ConnectionName);
            }
        }

        return conn;
    }
}

Použití v aplikaci

Mějme 2 WinFrom třídy Form1 a Form2. Form1 je hlavní okno aplikace a Form2 je z ní volán. Celá aplikace potřebuje spojení ke 2 databázím. Ve Form1 zavoláme při startu metodu Instance a Connect2Databases.

using Wiedemann;
public class Form1 : Form
{
  private ConnectionPool ConnectionPoolInstance = ConnectionPool.Instance;

  // konstruktor tridy
  public Form1()
  {
    InitializeComponent();

    if (!Connect2Databases())
    {
      throw new Wiedemann.ConnectionFailedException();
    }

    LoadSomeRecordsFromDatabase();
  }

  // spojeni k databazim
  bool Connect2Databases()
  {
    String ErrorString;

    MySqlConnection conn1 = ConnectionPool.AddConnection("spojeni_k_databazi_1",
        "server1", "uzivatel1",
        "heslo1", "databaze1",
        "", "CharSet=latin2;",
        out ErrorString);
    if (ErrorString.Length > 0)
    {
        MessageBox.Show(ErrorString);
        return false;
    }

    MySqlConnection conn2 = ConnectionPool.AddConnection("spojeni_k_databazi_2",
        "server2", "uzivatel2",
        "heslo2", "databaze2",
        "", "",
        out ErrorString);
    if (ErrorString.Length > 0)
    {
        MessageBox.Show(ErrorString);
        return false;
    }

    return true;
  }

  // nacteni zaznamu z databaze
  void LoadSomeRecordsFromDatabase()
  {
    MySqlConnection connection = ConnectionPool.GetConnection("spojeni_k_databazi_1");
    if (connection != null)
    {
      String query = "select bool_column, string_column from table";
      MySqlCommand commLoad = new MySqlCommand(query, connection);
      MySqlDataReader reader = commLoad.ExecuteReader();

      while (reader.Read())
      {
        if (reader.GetBoolean(0))
          MessageBox.Show(reader.GetString(1));
      }

      reader.Close();
    }
  }

  // funkce volana pri stisku tlacitka buttonForm2, ktera otevre nasi Form2
  private void buttonForm2_Click(object sender, EventArgs e)
  {
    using (Form2 form2 = new Form2())
    {
      form2.ShowDialog(this);
    }
  }
}

V naší třídě Form2 již Instance volat nemusíme. Následující příklad načte data z druhé databáze.

using Wiedemann;
public class Form2 : Form
{
  // konstruktor tridy
  public Form2()
  {
    InitializeComponent();
    LoadOtherRecordsFromDatabase();
  }

  // nacteni zaznamu z druhe databaze
  void LoadOtherRecordsFromDatabase()
  {
    MySqlConnection connection = ConnectionPool.GetConnection("spojeni_k_databazi_2");
    if (connection != null)
    {
      String query = "select bool_column, string_column from other_table";
      MySqlCommand commLoad = new MySqlCommand(query, connection);
      MySqlDataReader reader = commLoad.ExecuteReader();

      while (reader.Read())
      {
        if (reader.GetBoolean(0))
          MessageBox.Show(reader.GetString(1));
      }

      reader.Close();
    }
  }
}

Zdrojový soubor třídy: