Umstellung auf 3-Tier-Architektur

JK_net

Erfahrenes Mitglied
Hallo!

Ich habe mein kleines DB-Projekt umgestellt auf die "3-Tier"-Architektur.
Als DB-System verwende ich MySql (sollte ja eigentlich egal sein...).
Jetzt habe ich meinen Update, meinen Insert und meinen Delete-Command, bzw. den DB-Zugriff in meinen Data Access - Layer ausgelagert. Soweit auch kein Problem.

Mein Problem ist jetzt, dass ich in meinem UI-Layer Daten aus der DB via MySqlDataReader lese. Insgesamt habe ich 3 Select-Commands.
Jetzt möchte ich diesen gesamten Block auch in meinen DA-Layer auslagern, und da ist mein Problem, dass ich keinen Einstiegspunkt habe wie dies gemacht wird.
Ich würde gerne auf DataSets verzichten.

Gibt es überhaupt die Möglichkeit den Zugriff komplett in den DA-Layer auszulagern, und dann via Business Logic-Layer darauf zuzugreifen?

Würde mich freuen, wenn mir jemand Hilfestellung dazu geben könnte!

MfG
Jens
 

Norbert Eder

Erfahrenes Mitglied
Zur Begrifflichkeit: Mit DA-Layer meinst du einen ganz normalen DAL, also einen Database Abstraction Layer? Wenn ja, weiterlesen :)

Ein DAL arbeitet ja eigentlich dynamisch. Das heißt, du mappst Datenbank-Inhalte auf Objekte. Dafür gibt es natürlich unterschiedliche Ansätze, die unterschiedlich leicht bzw. schwer zu implementieren sind.

Ein Ansatz besteht zb darin, dass du deinen einzelnen Klassen, die den einzelnen Tabellen entsprechen die Insert, Update und Select Statements mitgibst. Das würde bedeuten, dass du dein Objekt an deine Datenbank-Klasse übergibst. Deine Datenbank-Klasse holt sich über einen Getter das entsprechende Statement, führt es aus, und füllt dein Objekt auf (wenn Select, sonder eben nicht).

Eine weitere Möglichkeit wäre eben, dass du via Reflection arbeitest und du deiner Database-Klasse einfach nur das Objekt übergibst. Die Datenbank-Klasse liest nun via Reflection alle public Properties aus und mappt diese auf die einzelnen Spalten in deiner Tabelle. Ist sicherlich der schwierigste Ansatz, hier musst du dich allerdings dann gar nicht mehr um irgendwas kümmern. Du legst dann ein Objekt an, eine Tabelle dazu und fertig. Die SQL-Statements werden dann durch deine Database-Klasse automatisch generiert und alle anderen notwendigen Informationen - und ist natürlich wiederverwendbar in allen weiteren Projekten.

Wenn du allerdings nur 3 Tabellen hast, wäre das natürlich schon ein entsprechender Overhead.

Und ja, es gibt auch noch weitere Möglichkeiten wie du das machen kannst. Kann ich ja gerne mal ausführen, wenn es dich interessiert bzw. keine der obenstehenden Lösungen für dich in Frage kommen.
 

JK_net

Erfahrenes Mitglied
Hallo Norbert!

Vielen Dank schon mal für deine Antwort!

Ich bin mir momentan ehrlich gesagt nicht sicher, wo wir den selben Layer meinen.
Ich verstehe unter Data Acces Layer meine Klasse, die den DB-Zugriff regelt.
Der Klasse übergeordnet habe ich meinen Business Logic Layer und anschließend mein User Interface Layer.

Meine Datenbank ist recht simpel, so dass ich momentan nur 1 Tabelle habe.

Der erste Ansatz kommt meinem Problem sehr nahe, wobei mein Problem das ausführen meines Select-Statements ist. Ist die Annahmen richtig, dass ich die Datensätze, die ich erhalte einem (in diesem Fall) MySqlDataReader-Objekt übergebe, und dieses dann im UI auslese?

Welche anderen Möglichkeiten kannst du mir denn noch empfehlen?

MfG
Jens
 

Norbert Eder

Erfahrenes Mitglied
Nein, ein wenig komplizierter ist es. Ich versuch dir das mal darzulegen.

Wenn wir vom ersten Fall ausgehen, dann besitzt du für deine Tabelle auch ein entsprechendes Objekt.

Hierzu musst du dir ein Objekt bauen, welches du vererben kannst, um die notwendigen Methoden fix zu haben. (Das brauchst du dann beim Befüllen).

Ich hab dir schnell ein paar Klassen geschrieben, die das veranschaulichen sollen.

Zuerst die Klasse BasisObjekt. Hier werden Methoden deklariert, die jede Klasse die du auf eine Tabelle mappen willst besitzen muss:

Code:
public class BasisObject
{
		internal string select = null;
		internal string update = null;
		internal string insert = null;

		public BasisObject()
		{
		}

		public string GetSelect()
		{
			return select;
		}

		public string GetUdpate()
		{
			return update;
		}

		public string GetInsert() 
		{
			return insert;
		}
}

Nun erstellst du das Objekt, welches auf die Tabelle gemappt werden soll. Diese muss nun von BasisObject erben:

Code:
public class Tabelle1 : BasisObject
{
	
	public Tabelle1()
	{
		this.insert = "INSERT INTO ....";
		this.update = "UPDATE ....";
		this.select = "SELECT bla,blab FROM ...";
	}
}

Hier erstellst du im Konstruktor deine SQL-Befehle. Was du dann noch brauchst ist natürlich deine Datenbank-Klasse. Dieser Klasse übergibst du dein Objekt und führst die gewünschte Aktion dann aus. Die Klasse würde dann in etwa so aussehen:

Code:
public class Storage
{
	private BasisObject dbObject = null;
	private string connectionString = "";
	private SqlConnection conn = null;

	public Storage()
	{
	}

	public BasisObject DbObject 
	{
		get { return this.dbObject; }
		set { this.dbObject = value; }
	}

	private void Open() 
	{
		if (this.conn == null)
			conn = new SqlConnection(this.connectionString);
		if (this.conn.State != System.Data.ConnectionState.Open)
			this.conn.Open();
	}

	private void Close() 
	{
		if (this.conn!=null)
			conn.Close();
	}

	public void Select() 
	{
		SqlCommand com = new SqlCommand(this.dbObject.GetInsert(), conn);
		this.Open();
		com.ExecuteNonQuery();
		this.Close();
	}

	public void Insert() 
	{
	}

	public void Update() 
	{
	}
}

Natürlich ist jetzt nicht alles ausprogrammiert und der ConnectionString ist nicht gesetzt. Das Befüllen ist dann etwas komplizierter da du mit Reflection arbeiten musst, oder du dir die entsprechende Tabelle rausliest und da ein switch auf das entsprechende Objekt machst, da hier ja nur das BasisObjekt übergeben wird.

Und wie gesagt, das ist EIN möglicher Ansatz, der für kleine Projekte durchaus brauchbar ist. Für größere Projekte wird das schnell unübersichtlich. Da empfiehlt sich dann shcon ein echter Datenbank-Abstraktions-Layer.

Der Vorteil bei dieser Version ist der, dass du in deiner Mapping-Klasse eigentlich alles notwendige drinnen hast und sofort weißt wo du was zu setzen hast. Die einzelnen Felder würden in diesem Fall in die Klasse Tabelle1 als Properties reinkommen, nur damit du weißt wo die Daten dann zu speichern wären.
 

JK_net

Erfahrenes Mitglied
Hallo Norbert,

vielen Dank!

Werde versuchen es so umzusetzen und meine Anwendung zu erweitern.


MfG
Jens

PS.: Ich schließe den Beitrag vorerst noch nicht, falls ich doch noch Fragen haben sollte... ;)
 

JK_net

Erfahrenes Mitglied
Hallo Norbert!

Ich habe deinen Vorschlag mit einer kleinen Änderung angenommen.

Jetzt sieht es bei mir (auszugsweise) so aus:

Klasse "ClsDataAccess":
(Beispiel Insert und Select)

Code:
public void Insert(ClsBusinessLogic contactData)
    {         
      openConnection();
    
 myContacts = new MySqlCommand("INSERT INTO contact (db_user, contact_name, picture) VALUES (?dbuser, ?contact, ?picture)", myConnection);
                
      myContacts.Parameters.Add("?dbuser", DataAccess.DBUser);
      myContacts.Parameters.Add("?contact", contactData.Contact);
      myContacts.Parameters.Add("?picture", contactData.Picture);
    
      try
      {
        myContacts.ExecuteNonQuery();
      }
      catch (Exception ex)
      {
   new FrmException(ex, MyException.MessageStatus.None, MyException.ButtonStatus.Cancel, true).ShowDialog(); 
        DBLogger log = new DBLogger(DataAccess.DBUser, "ClsDataAccess", "Insert()", ex.Message);
      }
                
      closeConnection();   
    }
    
    public void GetContact(ClsBusinessLogic contactData)
    {
      openConnection();
    
 myContacts = new MySqlCommand("SELECT contact_name, picture FROM contact WHERE id = ?contactid AND db_user = ?dbuser", myConnection);
      myContacts.Parameters.Add("?contactid", contactData.Contactid);
      myContacts.Parameters.Add("?dbuser", DataAccess.DBUser);
                
      myReader = null;
      
      try
      {
        myReader.Read();
        contactData.Contact = myReader.GetString(0);
        contactData.Picture = myReader.GetString(1);
    
      }
      catch (Exception ex)
      {
        new FrmException(ex, MyException.MessageStatus.None, MyException.ButtonStatus.Cancel, true).ShowDialog();
                        
        DBLogger log = new DBLogger(DataAccess.DBUser, "ClsDataAccess", "getContact()", ex.Message);
      }
      finally
      {
        if (myReader != null) myReader.Close();
      }
    
      closeConnection();
    }

Klasse "ClsBusinessLogic":

Code:
public ClsBusinessLogic()
    {
      contactData = new ClsDataAccess();
    }
    
    public String Contactid 
    {
      get { return this.contactid; }
      set { this.contactid = value; }
    }
    
    public String Contact 
    {
      get { return this.contact; }
      set { this.contact = value; }
    }
    
    public byte[] Picture 
    {
      get { return this.picture; }
      set { this.picture = value; }
    }
    
    public void Insert()
    {
      contactData.Insert(this);
    }
    
    public void GetContact()
    {
      contactData.GetContact(this);
    }

Formular "FrmMain":

Beispiel einzelnen Datensatz auslesen:
Code:
if (this.lvwContacts.SelectedItems.Count > 0)
    {
      ClsBusinessLogic contact = new ClsBusinessLogic();
      contact.Contactid = this.lvwContacts.SelectedItems[0].SubItems[0].Text;
      
      contact.GetContact();
      
      this.txtName.Text = contact.Contact;
      this.txtPosition.Text = contact.Position;
      this.txtCompany.Text = contact.Company;
    }

Beispiel neuen Datensatz speichern:
Code:
ClsBusinessLogic contact = new ClsBusinessLogic();
    contact.Contact = this.txtName.Text;
    contact.Picture = data;
    contact.Insert();


Auf diese Art und Weise habe ich auch mein "Select"-Problem zur Hälfte gelöst.

Leider fehlt mir noch, wie ich meine Datensätze unabhängig von der Anzahl in ein ListView-Control einlesen kann. Ich bekomme dann ja vorraussichtlich mehrere Datensätze zurück, aber wie handhabe ich das?
Kannst du mir da evtl. noch ein wenig Unterstützung geben?

Und mich würde auch einmal deine Meinung von der Gliederung/Strukturierung interessieren.

Vielen Dank für Deine Unterstüzung!

MfG
Jens
 

Norbert Eder

Erfahrenes Mitglied
Bei nem Select kannst du ja (wenn du mehrere Datensätze zurück bekommst) für jeden Datensatz ein Objekt erstellen und das Objekt stopfst du einfach eine zb. eine ArrayList. Diese kannst du an deinen VIsualisierungs-Layer zurückliefern, an der Stelle dann die Objecte auslesen und deiner ListView oder was auch immer übergeben.
 

Thomas Darimont

Erfahrenes Mitglied
Hallo!

Haltet ihr das wirklich für eine gute Idee die BusinessObjects (BOs) mit wissen über "ihren" Persistenzmechanismus auszustatten? Vermindert das nicht die Erweiterbarkeit und Datenbankunabhänigkeit?

Mein Vorschlag wäre es die Persistenzlogik aus den BOs herauszulösen und das DAO Pattern zu verwenden. Die Architektur wäre dann im groben folgende:
Du hast deine BOs, die die Objekte der Anwendungsdomäne repräsentieren (Zustand + Verhalten).

Zusätzlich zu deinen BOs hast du BusinessServices. Diese BusinessServices bieten Methoden die jeweils einen UseCase darstellen. Innerhalb dieser Methoden arbeiten dann u.U. mehrere BOs zusammen. Soll auf einen Datenspeicher zugegriffen werden, so wird innerhalb der BusinessService-Methode auf das DAO zurückgegriffen. Das DAO enthält die Logik für die Persistenzoperationen der BOs (Create, Read, Update, Delete).

Unser DAO Interface
Code:
    	interface ISomeDomainDAO
    	{
    		void save(SomeDomainObject o);
    		void update(SomeDomainObject o);
    		void deleteByPK(long pk);
    		SomeDomainObject getByPk(long pk);
    		List findByAttribute(String someAttribute);
    	}

Unser BusinessService:
Code:
    	class BusinessService
    	{
    		ISomeDomainDAO someDomainDAO
    		{
    			get
    			{
    				return someDomainDAO;
    			}
    			set
    			{
    				someDomainDAO = value;
    			}
    		}
    
    		public BusinessService()
    		{
    		}
    
    		public void someBusinessMethod(long pk)
    		{
    			SomeBusinessObject bo = someDomainDAO.getByPk(pk);
    			bo.doSomeWork(1, 2, 3);
    			someDomainDAO.update(bo);
    		}
    	}

Unsere (spezifische) DAO Implementierung:
Code:
    	class SomeDomainDAOMySQLImpl : ISomeDomainDAO
    	{
    		void save(SomeBusinessObject o)
    		{
    			...
    		}
    		void update(SomeBusinessObject o)
    		{
    			...
    		}
    		void deleteByPK(long pk)
    		{
    			...
    		}
    		SomeBusinessObject getByPk(long pk)
    		{
    			...
    		}
    		List findByAttribute(String someAttribute)
    		{
    			...
    		}
    	}

Der Business Service bekommt nun das Property someDomainDAO mit der spezifischen DAO Implementierung untergeschoben. Da wir hier mit einem Interface Arbeiten können wir die DAO Implementierung ganz einfach austauschen (z.Bsp. von MySQL zu MSSQL...) -> +1 für Erweiterbarkeit

Diese BusinessServices können natürlich auch andere BusinessServices verwenden ;)

Auf diese Weise bleiben die BOs von technischen Angelegenheiten verschont :)
+1 für Einfachheit.

//Edit wie ich gerade sehe, wurde genau dies (ähnlich) implementiert :), na ja, doppelt hält besser.

Gruß Tom
 

Norbert Eder

Erfahrenes Mitglied
Wie gesagt, ich hatte angemerkt, dass es viele Möglichkeiten gibt dies abzudecken. Da er nur sehr wenige Tabellen verwendet kannst du das so schon implementieren.

So weit sind wir von der Architektur nicht entfernt.

Bei kleinen Projekten ist es nicht tragisch, wenn du die Persistensanweisungen direkt zum Objekt hängst. So hast du den Vorteil, dass du ganz genau weißt, wo du etwas verändern musst.

Bei größeren Projekten wäre dies eher ein Fehler, daher würde ich dann zu einem eigenen DAL raten.

Falls jemand nachschlagen möchte, hier die unterschiedlichen Varianten die mir auf die schnelle einfallen:

- Row Data Gateway
- Table Data Gateway
- Active Record
- Data Mapper

Zum Mappen auf die Datenbank gibts natürlich auch wieder unterschiedliche Ansätze:

- Foreign Key Mapping
- Association Table Mapping
- Single Table Inheritance
- Concrete Table Inheritance
- Class Table Inheritance

Wer sich also näher damit beschäftigen will, kann sich anhand dieser Patterns schon sicher etwas aus dem Netz ziehen.

Das Problem bei der ganzen Geschichte ist es, dass das zu verwendende Pattern natürlich auch zum restlichen Design passen muss. Daher ist es teilweise schwierig einen Rat zu geben, welches denn nun das bessere ist. Vor allem ists auch so, dass der eine Softwaredesigner der Meinung ist, seine Variante ist die bessere und ein anderer findet etwas ganz anderes besser. Welcome in the world of design-patterns :)

Wie gesagt, ich denke schon, dass seine Lösung nicht so schlecht ist, Verbesserungsmöglichkeiten gibts natürlich immer. Es macht nur meiner Meinung nach nicht viel Sinn, mehr Arbeit als notwendig in das Objekt-Tabellen Mapping zu stecken, vor allem wenn es um _eine_ Tabelle geht *hehe*.

Aber: sollte es dem Jens langweilig sein, kann er ja einen eigenen DAL per Reflection inkl. einem sinnvollen Mapping bauen, dann hat er das Problem nie wieder :) Und je nach Implementierung funktionierts auch datenbankunabhängig.

Ach, das Thema ist spannend, aber es wird NIE nur EINER Recht haben *kicher*.

@Thomas: Schon mal überlegt einen Pattern-Bereich einzuführen, oder in diese Richtung was zu machen? Wäre ne coole Sache.
 

JK_net

Erfahrenes Mitglied
Hallo Ihr zwei!

Vielen Dank noch einmal für Eure Erläuterungen und Hilfestellungen!

Der Zugriff klappt nun einwandfrei!

@Norbert: Hmm, das mit der langen Weile könnte tatsächlich bald der Fall sein, da ich demnächst Urlaub habe... ;)

MfG
Jens

PS.: Jetzt schließe ich den Thread auch...