ASP.NET – Diskuzní forum (2)

Dnešní díl bude zaměřen na vytvoření datového adaptéru pro naši komponentu Forum. Podle specifikace rozhraní vytvoříme třídu, která bude mít za úkol zprostředkovávat komunikaci s databází typu Access.
V předchozím dílu jsem definovali základní kostru komponenty. Ta se bude skládat s následujících hlavních komponent (vrstev):
  • Prezentační vrstva - bude zajišťována pomocí Web Custom Controlu. Více informací o tom, jak vytvářet uživatelské komponenty v ASP.NET naleznete v jednom z předchozích článků tohoto seriálu.
  • Vrstva logických pravidel - bude obsahovat třídu Forum, která je nositelem všech potřebných informací o daném diskusním foru. Obsahuje všechny příspěvky seřazeny do objektové stromové struktury. Takže například 2. odpověď na 3. příspěvek můžeme získat pomocí:

  • Forum.Messages[3].Replies[2]

  • Vrstva logických pravidel obsahuje také definici pro třídu ForumMessage, která popisuje vlastnosti daného příspěvku, jako například text, autora, nadpis, datum apod. Tyto objekty jsou naprosto nezávislé na databázi.

  • Databázová vrstva - jedním s hlavních požadavků pro komponentu byla nezávislost na datovém zdroji. Proto jsem zvolil návrh struktury pomocí Adaptérů. Všechny adaptéry musí implementovat velice jednoduché rozhraní IForumDataAdapter. Tímto přístupem jsme získali možnost mít na straně databáze v podstatě cokoliv - XML soubor, Access, SQL, textový soubor, nebo například NNTP server.

IForumDataAdapter

Je velice jednoduché rozhraní, které definuje 3 metody (Load, Save a Create) a jedno public pole (CurrentForum):

/// <summary>
/// IForumDataAdapter defines an interface responsible for filling data from/into the Forum, given as a read-only
/// property CurrentForum. This should be initialized by constructor of implementing object. This interface allows to have
/// multiple possibilities for storing data. For example an SQL server with stored procedures, Access database with pure SQL
/// access, XML file, or NNTP server etc.
/// </summary>

public interface IForumDataAdapter
{
  /// <summary>
  /// Used for backreferencing current Forum, for which the IForumDataAdapter is serving as data provider. Should be
  /// initialized in constructor, if any dependances on datasource occur (such as ID mapping etc.)
  /// </summary>
  Forum CurrentForum{get;}

  /// <summary>Fills CurrentForum with values from underlying datasource./// </summary>
  void Load();

  /// <summary>
  /// Checks all messages in <see cref="CurrentForum">CurrentForum</see> for Changed property. If true, Save determines wheter an update or adding of new
  /// record needed and performs actual operation of datasource. It also has to check if any messages have been deleted.
  /// </summary>
  void Save();

Podívejme se nyní na jednotlivé položky tohoto rozhraní:

CurentForum - obsahuje aktuální forum, pro které slouží náš adaptér jako zdroj dat. Tato vlastnost je pouze pro čtení. Je na samotném adaptéru, jak zajistí její nastavení.

Jedna z možností je vyžadovat objekt Forum jako parametr pro konstruktor, další možností je vytvořit nové Forum.

Load() - má na starosti naplnit dané forum daty z datového zdroje. To znamená vytvořit celou strukturu objektů ForumMessage a správně je zařadit do CurrentForum.

Save() - uloží celé CurrentForum do databáze. Abychom nemuseli složitě držet dvě kopie toho samého fora, pro zjištění změn, přidal jsem do objektu ForumMessage pole Changed, které je true v případě, že došlo ke změně ve vlastnostech dané ho příspěvku. Metoda Save() se tím stává jednodušší - stačí prohledat všechny příspěvky a pouze u změněných vykonat potřebný zápis do datového zdroje.

Jak můžete vidět, návrh rozhraní ještě pořád neobsahuje žádné vlastnosti, které by byli specifické pouze pro přístup k jednomu typu datového zdroje. Možná se budete divit, jak tedy například určím ID záznamu daného Foru (pokud je zdrojem databázová tabulka). Odpovědí je, že to už musí zařídit implementující adaptér sám. Rozhraní slouží právě k tomu, aby před námi všechny odlišnosti ukrylo.

OleDbDataAdapter

Tato třída bude ukázkou implementace uvedeného rozhraní. Bude zajišťovat přístup do standardní databáze(např. Access, SQL Server) pomocí Ole Db. Pro provádění samotných příkazů použijeme čisté SQL dotazy - nebudeme tedy používat uložené procedury.

Podívejme se nejdříve na příklad použití třídy, později si rozebereme konkrétní implementaci:

Příklad č.1 vytvoří instanci třídy Forum, předá ji společně s požadovaným ID fora adapteru a načte všechnny příspěvky pomocí volání metody Load().

Forum                  f      = new Forum();
OleDbForumDataAdapter adapter = new OleDbForumDataAdapter(f,1);
adapter.Load();

Příklad č.2 navazuje dále a vytváří nový příspěvek, pak reakci na tento příspěvek a nakonec data uloží do datového zdroje:

f.Messages.Add(new ForumMessage("Janko Hrasko","hrasko@hh.cc","","Text od Janka","","",DateTime.Now));
f.Messages[m.Messages.Count-1].Replies.Add(new ForumMessage("Jankuv Reply","","Text odpovedi","","","",DateTime.Now));

adapter.Save();

Pokud chceme vytvořit nové forum, které ješte v databázi neexituje, můžeme použít jiný konstruktor, například:

OleDbForumDataAdapter adapter = new OleDbForumDataAdapter();
Forum                  f      = adapter.CurrentForum; 

To by byla cílová specifikace pomocí příkladů užití. Nyní se podívejme na samotné provedení.

V čem jsou základní problémy, kterým budeme v implementaci čelit? Hlavním problémem je, jak udržet souvislost mezi záznamy v databázi a jednotlivými objekty ForumMessage. Musíme vytvořit nějakou pomocnou tabulku, která bude určovat, jaké ID koresponduje jaké instanci daného příspěvku.

Poznámka: Já jsem se rozhodl pro použití dvou hašovacích tabulek (třída HashTable), které nám umožní lineární závislost doby běhu algoritmu na počtu příspěvků (lineární časová složitost O(n)).

Podívejme se nejdříve na konstruktory daného adaptéru:

public class OleDbForumDataAdapter:IForumDataAdapter
{
 
  /// <summary>
  /// Contains the current forum, for which the OleDdForumDataAdapter will serve as data provider.
  /// </summary>
  public Forum CurrentForum{get{return _forum;}}

  /// <summary>
  /// ID of current forum in database.
  /// </summary>
  public readonly int ForumID;

  private Forum _forum;
  private bool loaded;
 
      /// <summary>
      /// Creates new Forum. ID is generated automatically (by incrementing maximum ForumID) and stored in ForumID field.
      /// </summary>
      public OleDbForumDataAdapter()
      {
        _forum = new Forum();
        ForumID =(int) Tools.ExecuteSQLScalar("SELECT MAX(ForumID) FROM ForumMessage;") + 1;
        Load();
      }

      /// <summary>
      /// Prepares Data Adapter for use. CurrentForums becomes the supplied object.
      /// </summary>
      /// <param name="f">Forum object to load data into</param>
      /// <param name="ID">Database record ForumID</param>
      public OleDbForumDataAdapter(Forum f, int ID)
      {
        _forum = f;
        ForumID = ID;
      }

      /// <summary>
      /// Prepares Data Adapter for use. New instance of Forum object is created in CurrentForum.
      /// </summary>
      /// <param name="ID">Database record ForumID</param>
      public OleDbForumDataAdapter(int ID)
      {
        _forum = new Forum();
        ForumID = ID;
        Load();
      }

Vidíme, že za pomocí této implementace máme velice široké možnosti použití.

  • Pokud zavoláme new OleDbForumDataAdapter() , vytvoří nám adapter nové forum, vytvoří také odpovídající objekt v CurrentForum, který můžeme dále použít.
  • Při použití new OleDbForumDataAdapter(int ID) můžeme zadat ID fora. Konstruktor načte příspěvky do nově vytvořeného objektu Forum. To můžeme posléze získat pomocí vlastnosti CurrentForum.
  • Poslední použití je new OleDbForumDataAdapter(Forum f, ID). To nám umožňuje specifikovat navíc i vlastní Forum, takže můžeme například přidat příspěvky do již existujícího fora. Příklad použití - chceme mít forum, které bude obsahovat více příspěvků z různých fór. Mohlo by to vypadat například takto:
Forum f;

OleDbForumDataAdapter adapter1 = new OleDbForumDataAdapter(1);
OleDbForumDataAdapter adapter2 = new OleDbForumDataAdapter(adapter1.CurrentForum, 2);
OleDbForumDataAdapter adapter3 = new OleDbForumDataAdapter(adapter2.CurrentForum, 1);


f = adapter3.CurrentForum;   

adapter1.Load();
adapter2.Load(false);
adapter3.Load(false);

To false v posledních dvou voláních metody Load() určuje, že se data mají přidávat. Jak konkrétně vypadá metoda Load() můžete vidět zde:

/// <summary>
/// Fills CurrentForum with data from underlying database supporting OleDb access. Clears the forum before loading data.
/// </summary>
public void Load()
{
Load(true);
}
     
/// <summary>
/// Fills CurrentForum with data from underlying database supporting OleDb access.
/// </summary>
/// <param name="ClearMessages">Set true if you want to clear messages prior to loading</param>
public void Load(bool ClearMessages)
      {
        DataTable list = Tools.ExecuteSQLDataTable("SELECT * FROM ForumMessage WHERE ForumID = " + ForumID + " ORDER BY ParentID, PostDate ;");
       
ObjectToID = new Hashtable(list.Rows.Count);
IDToObject = new Hashtable(list.Rows.Count);
ForumMessage m;

// While we loop through all records, we are simultaneously creating mappings for ID corresponding to Parent Message reference.
// Using this mapping for parent retrieval we can achieve linear time complexity.
// Note, that we previously ordered ForumMessage records by ParentID in SQL query.
// Hashtable is used for optimum performance on sparse set of values.

if (ClearMessages) _forum.Messages.Clear();

foreach(DataRow r in list.Rows)
{
m = new ForumMessage();
m.Author = r["Author"].ToString();
m.AuthorEmail = r["AuthorEmail"].ToString();
m.Body = r["Body"].ToString();
m.IPAddress = r["IPAddress"].ToString();
m.PostDate = DateTime.Parse(r["PostDate"].ToString());
m.Subject = r["Subject"].ToString();
m.Changed=false;

if(r["ParentID"].ToString()=="0") _forum.Messages.Add(m);
else ((ForumMessage)IDToObject[r["ParentID"]]).Replies.Add(m);

IDToObject.Add((int)r["ID"],m);
ObjectToID.Add(m,(int)r["ID"]);

loaded = true;
}

Metoda Load vytváří velice důležitou spojitost mezi objekty a jejich odpovídajícími ID v databázové tabulce pomocí dvou hašovacích tabulek ObjectToID a IDToObject . Při použití :

ObjectToID[prispevek] - Nám hašovací tabulka vrátí ID odpovídající danému příspěvku (instanci třídy ForumMessage).

IDToObject[ID] - Umožňuje, jak už určitě mnozí z vás pochopili, obrácený postup.

Poté, co metoda Load vytvoří mapování ID do objektů, můžeme s forem jakkoliv manipulovat, přidávat, mazat, měnit příspěvky. Při zavolání metody Save jsou zjištěny změny a uloženy do databáze. Při ukládání je použito rekurzivního prohledávání celého stromu příspěvků na příspěvky s vlastností Changed nastavenou na hodnotu true. Metoda DoSave pak zajišťuje samotný zápis do databáze pomocí SQL:

private void DoSave(ForumMessage Msg, ForumMessage ParentMsg)
{
int ParentID=0;
if(ParentMsg!=null) ParentID = (int)ObjectToID[ParentMsg];
if(ObjectToID[Msg]==null)      // Message is new - we have to add a record to table
{
Tools.ExecuteSQLNonQuery("INSERT INTO ForumMessage(ParentID, ForumID, Author, AuthorEmail, Subject, PostDate, Body, IPAddress, Attachment) " +
            "VALUES ("+ParentID+","+ForumID+",`"+Msg.Author+"`,`"+Msg.AuthorEmail+"`,`"+Msg.Subject+"`, `"+Msg.PostDate+"`,`"+Msg.Body+"`, `"+Msg.IPAddress+"`,`"+Msg.Attachment+"`);");

Msg.Changed=false;
int newID = (int) Tools.ExecuteSQLScalar("SELECT MAX(ID) FROM ForumMessage;");

ObjectToID.Add(Msg,newID);
IDToObject.Add(newID,Msg);
}
else    // Message has been changed - UPDATE needed
{
Tools.ExecuteSQLNonQuery("UPDATE ForumMessage SET"+
      "ParentID    = "+ParentID+","+
      "ForumID     = "+ForumID+","+
      "Author      = `"+Msg.Author+"`,"+
      "AuthorEmail = `"+Msg.AuthorEmail+"`,`"+
      "Subject     = `"+Msg.Subject+"`,`"+
      "PostDate    = `"+Msg.PostDate+"`,`"+
      "Body        = `"+Msg.Body+"`,`"+
      "IPAddress   = `"+Msg.IPAddress+"`,`"+
      "Attachment  = `"+Msg.Attachment+
      " WHERE ID   = "+ObjectToID[Msg]+"`);"); 
     
Msg.Changed=false;
}   
}

/// <summary>
/// Browses recurently through whole message tree looking for changes.
/// </summary>
/// <param name="Msg">Current message to be processed</param>
/// <param name="ParentMsg">Parent message owning Msg</param>
private void SaveMsg(ForumMessage Msg, ForumMessage ParentMsg)
{
NotDeleted.Add(Msg);
if(Msg.Changed) DoSave(Msg,ParentMsg);
foreach(ForumMessage m in Msg.Replies)
if(m.Replies!=null) SaveMsg(m,Msg);
}

/// <summary>Save any changes made to CurrentForum to database</summary>
public void Save()
{
if(!loaded) throw(new Exception("Cannot Save Forum. You have to load a Forum first"));
NotDeleted = new ArrayList(ObjectToID.Count);
foreach(ForumMessage m in _forum.Messages)
if(m.Replies!=null) SaveMsg(m,null);

// Delete messages not present in current Forum

ArrayList Deleted = new ArrayList(5);
foreach(ForumMessage m in ObjectToID.Keys) if (!NotDeleted.Contains(m)) Deleted.Add(m);

foreach(ForumMessage m in Deleted)
{
  Tools.ExecuteSQLNonQuery("DELETE FROM ForumMessage WHERE ID ="+ObjectToID[m]+";");
       
  IDToObject.Remove(ObjectToID[m]);
  ObjectToID.Remove(m);
}
}

Co přístě ?

Doufám, že na základě dnešního popisu budete schopni vytvořit adaptér pro jakýkoliv datový zdroj. Příště budeme pokračovat a stavět prezentační vrstvu - komponentu, která nám zobrazí dané forum.
Váš názor Další článek: Pentium 4 na 2,6 a 2,66 GHz dříve

Témata článku: Software, Programování, Public, From, Adapter, Have, Interface, Standardní databáze, Optima, Příspěvek, Forum, Private, Optimum, Pure, Access, Save, Message, Check In, ASP, Attachment


Určitě si přečtěte

Nejlepší notebooky do 10 000 korun: Co koupit a čemu se raději vyhnout

Nejlepší notebooky do 10 000 korun: Co koupit a čemu se raději vyhnout

** Do deseti tisíc korun lze dnes koupit slušné notebooky ** V nabídce ale i tak převládají zastaralé a pomalé modely ** Poradíme, jak dobře vybrat i s omezeným rozpočtem

David Polesný | 102

Starý smartphone nemusí skončit v koši. 10 způsobů, jak ho ještě můžete využít

Starý smartphone nemusí skončit v koši. 10 způsobů, jak ho ještě můžete využít

** Co dělat s vysloužilým chytrým telefonem? Neházejte ho do koše! ** Našli jsme pro vás deset možností, jak ho prakticky využít ** I stará zařízení tak mohou být užitečná

Karel Kilián | 48

Biblická potopa Česka: Jak bychom dopadli, kdyby nás zatopil oceán

Biblická potopa Česka: Jak bychom dopadli, kdyby nás zatopil oceán

** Představte si biblickou potopu ** Nejprve zaniknou Děčín a Břeclav, pak i Brno a Praha ** Hlavním městem se stane Jihlava a zbytky Čechů přežijí na Kvildě

Jakub Čížek | 93

Pojďme programovat elektroniku: České chytré zásuvky Netio pro kutily i firmy

Pojďme programovat elektroniku: České chytré zásuvky Netio pro kutily i firmy

** Wi-Fi zásuvky nevyrábí pouze Čína ** Vyzkoušeli jsme českou Netio PowerCable ** Je přímo určená pro vývojáře, má totiž jednoduché JSON API

Jakub Čížek | 43


Aktuální číslo časopisu Computer

Megatest: 20 powerbank s USB-C

Test: mobily do 3 500 Kč

Radíme s výběrem routeru

Tipy na nejlepší vánoční dárky