[C#/ALG] OR mapper en lazy loading

Pagina: 1
Acties:

  • zoepercavia
  • Registratie: September 2001
  • Laatst online: 26-12-2025
Ik heb Fowlers (ja, ik weet dat z'n naam gevoelig is :P) boek Patterns of Enterprise Application Architecture gekocht en ben begonnen met het implementeren van wat patterns uit dat boek (niet voor een project, maar voor mezelf). Het zit nu al een beetje in elkaar en ik heb nu een datamapper, met identity map, unit of work en lazy loading. Alhoewel ik snap waarom het afgeraden wordt zelf een mapper te schrijven (af en toe is het heel erg complex) begint het al echt ergens op te lijken. Creeren, verwijderen en veranderen van objecten kan nu allemaal in de BLL zonder dat je ook maar iets ziet van de mapper laag.
Een van de problemen die nu nog openstaan is het goed laten samen werken van de unit of work en lazy loading, deze zitten namelijk nog al 's in elkaars weg. Een voorbeeldje, ik heb de volgende business class waar zowel ondersteuning voor unit of work en lazy loading in zit:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Class Person {

private string _name = "";

public string Name {
get {
Load(); //support voor lazy loading
return _name;
}
set {
MarkDirty(); //object wordt als dirty aangegeven in unit of work
_name = value;
}
}


De functie Load(), vult de instantie van het object simpelweg door iets uit de database te trekken en dan het veld up te daten middels de setter. Het probleem is dat hiermee ook het object als dirty wordt gemarkeerd, immers wordt de setter van het veld aangeroepen. Dit is te voorkomen door de status van het object te bekijken, is deze nog niet geladen dan kan het object niet als dirty worden gemarkeerd. Dit werkt opzich goed, alleen het gaat mis als je iets aan het object wilt veranderen als hij nog niet geladen is, het object wordt dan niet als dirty gemarkeerd en zal niet worden geupdate.

Het probleem: eigenlijk zou ik het liefst het object van 'achteren' (niet zichtbaar voor de BLL) willen vullen, hierbij komt de unit of work niet van te pas. Pas als het object van 'voren' wordt veranderd (tijdens gebruik in de BLL) komt de unit of work er aan te pas. Ik kan nu 2 oplossingen bedenken:
1) er wordt gecontroleerd wie de setter aanroept, is dit de BLL dan wordt het object dirty, is dit de loader (voor lazy loading) dan gebeurt er niets. Is het mogelijk om te controleren door wie de setter wordt aangeroepen in c#?
2) ik maak een speciale setter voor de lazy loader (SetNameForLazyLoader of iets dergelijks). Alleen wil ik niet dat deze zichtbaar is voor de BLL. Is het mogelijk om methodes alleen voor bepaalde classes zichtbaar te maken?

Ik hoop dat ik het probleem een beetje duidelijk heb gemaakt.

Panacea.NL als je geinteresserd bent in IT en Geneeskunde!


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 22-08 01:56
Ok, ik ken Fowlers en zijn boek niet, maar ik heb wel enig idee van de concepten waar je over schrijft dus ik ga toch een poging doen tot een zinnig antwoord.
zoepercavia schreef op 04 mei 2003 @ 14:37:
Het probleem: eigenlijk zou ik het liefst het object van 'achteren' (niet zichtbaar voor de BLL) willen vullen, hierbij komt de unit of work niet van te pas. Pas als het object van 'voren' wordt veranderd (tijdens gebruik in de BLL) komt de unit of work er aan te pas. Ik kan nu 2 oplossingen bedenken:
1) er wordt gecontroleerd wie de setter aanroept, is dit de BLL dan wordt het object dirty, is dit de loader (voor lazy loading) dan gebeurt er niets. Is het mogelijk om te controleren door wie de setter wordt aangeroepen in c#?
2) ik maak een speciale setter voor de lazy loader (SetNameForLazyLoader of iets dergelijks). Alleen wil ik niet dat deze zichtbaar is voor de BLL. Is het mogelijk om methodes alleen voor bepaalde classes zichtbaar te maken?
Optie 1 is een ad-hoc oplossing die ten eerste niet bijdraagt aan de efficiëntie van het programma en ten tweede (en veel belangrijker) het ontwerp tekort doet. Je verspreid je functionaliteit (lazy loading) nu over verschillende methoden, die je ook nog door mengt met andere functionaliteit (het markeren van 'dirty' data). Gelukkig is het in C# (voor zover ik weet) dan ook niet mogelijk om te zien wie de methode aangeroepen heeft. In principe kun je dat natuurlijk omzeilen door een boolean-argument mee te geven (met een default waarde), die bepaalt of de data wel of niet als dirty wordt aangemerkt, maar zelfs al is het zo op te lossen, dan lijkt optie 1 me nog steeds niet echt aantrekkelijk.

Optie 2 is daarintegen een zeer goede optie; sterker nog, het is helemaal niet nieuw of uitzonderlijk: heel veel gesynchroniseerde klassen worden op een vergelijkbare manier geïmplementeert. In zo'n geval gaat het erom dat voor het uitvoeren van de setter een lock wordt verkregen op de data. Als de setter vanuit een andere methode van dezelfde klasse aangeroepen wordt, is die lock doorgaans al verkregen, en dus kan de originele setter niet aangeroepen worden.

In jouw geval werk je niet met synchronisatie, maar de oplossing blijft hetzelfde: maak een private setter, die alleen de interne data bijwerkt, en roep deze aan vanuit je lazy loading methode. Bovendien maak je een public setter die niets anders doet dan de interne data als dirty markeren en vervolgens de private setter aanroepen. De resulterende oplossing is netjes en zeer efficiënt.

  • mbravenboer
  • Registratie: Januari 2000
  • Laatst online: 06-11-2025
Soultaker: Gelukkig is het in C# (voor zover ik weet) dan ook niet mogelijk om te zien wie de methode aangeroepen heeft.
Geen taal feature, maar wel een API feature ;) .

Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Diagnostics;
using System.Reflection;

public class StackTest {

  public static void Main(string[] args) {
    method2();
  }

  public static void method2() {
    StackFrame frame = new StackFrame(1);
    MethodBase method = frame.GetMethod();
    
    Console.WriteLine("Invoked by " + method.Name);
  }
}

Uiteraard gaat deze method gigantisch op z'n gat wanneer de compiler
besluit om methoden te gaan inlinen. Met Mono gebeurt dit bijvoorbeeld
al als je er een simpele methode aanroep tussen doet. Misschien
zijn er nog wel 'betere' methoden (ben geen doorgewinterde .NET-er ;) )

Blog, Stratego/XT: Program Transformation, SDF: Syntax Definition, Nix: Software Deployment


  • zoepercavia
  • Registratie: September 2001
  • Laatst online: 26-12-2025
Ik ben het er wel mee eens dat de 2e oplossing niet de mooiste is, omdat je als het ware uit het systeem treedt. En de eerste oplossing lijkt me inderdaad ook de mooiste, maar daar zitten nog wel wat praktische bezwaren aan.

Ik heb het zo opgezet dat de domeinklasse en de mapper gescheiden zijn (bv. Person.cs en PersonMapper.cs). Deze hebben verder geen relatie met elkaar (dus geen overervering). Dit betekent dat private en protected methods van Person (de ideale setter) niet beschikbaar zijn in de mapper. Dus zou ik een publieke setter moeten maken die alleen voor de mapper bdoeld is (NameForMapper), maar deze is dan ook beschikbaar in de BLL, waar ik niet zou blij mee ben.

Overigens beschrijft Fowler in z'n boek precies hetzelfde probleem, maar geeft helaas geen zaligmakende oplossingen. Hij komt met de volgende mogelijkheden:
- het samenvoegen van domeinklasse en mapper in een package (java) of assembly (c#). Dan kan je dus met access modifiers werken (internal). Een echt mooie oplossing vind ik het alleen niet voor dit probleem
- gebruik maken van reflection, met als nadeel wat boven door soultaker beschreven is
- gebruik maken van speciale setters voor de mapper die beschermd worden door een status veld. Ook niet echt mooi, vanwege de boven beschreven problemen.

Ik hoopte dus eigenlijk dat er iemand een mooie oplossing had bedacht :)

Panacea.NL als je geinteresserd bent in IT en Geneeskunde!


  • EfBe
  • Registratie: Januari 2000
  • Niet online
zoepercavia schreef op 04 mei 2003 @ 19:44:
Ik ben het er wel mee eens dat de 2e oplossing niet de mooiste is, omdat je als het ware uit het systeem treedt. En de eerste oplossing lijkt me inderdaad ook de mooiste, maar daar zitten nog wel wat praktische bezwaren aan.
Aan het hele idee van lazy loading zitten bezwaren aan. Lazy loading gebruik je bv bij: Customer.Orders.Order(id);. Je wilt dan dat hij de orders van Customer pas gaat laden wanneer daar om gevraagd wordt, niet wanneer Customer geinstantieerd wordt. Echter, dit idee van classes onderbrengen in elkaar is flawed. Je hebt bv in de Northwind database van SQLserver ook een Employee entity die orders heeft. Een Orders object, lazy loaded, is die uitwisselbaar met de Employee.Orders ? Nee, semantisch niet. Syntactisch wel. Dit levert dus problemen op. Ook leveren zaken als "Haal de orders op bij deze customer EN bij deze employee". Die wil je OOK lazy loaden, maar wanneer?

De oplossing is dat je NIET bv Orders onderbrengt bij Customer of Employee, maar als zelfstandige entiteit gebruikt, als 'collection' variant van de entiteit Order. Je code van deze Collection variant moet methods bevatten waar je Entity objects aan kunt meegeven, bv:
Customer c = new Customer(CustomerID); // load customer
Employee e = new Employee(EmployeeID); // load employee
Orders o = Orders.Retrieve(c, e); // load in o all orders of c taken by e.

Je laadt lazy, want je geeft exact aan wanneer je wilt laden, en je hebt duidelijke controle over wanneer je wat laadt, PLUS het is duidelijk, je kunt nl. geen rariteiten krijgen bij uitwisseling van objects van hetzelfde type die eigenlijk niet uitwisselbaar mogen zijn.
Ik heb het zo opgezet dat de domeinklasse en de mapper gescheiden zijn (bv. Person.cs en PersonMapper.cs). Deze hebben verder geen relatie met elkaar (dus geen overervering). Dit betekent dat private en protected methods van Person (de ideale setter) niet beschikbaar zijn in de mapper. Dus zou ik een publieke setter moeten maken die alleen voor de mapper bdoeld is (NameForMapper), maar deze is dan ook beschikbaar in de BLL, waar ik niet zou blij mee ben.
Fowler doet moeilijk om niks. Het is een simpele factory opstelling die het data access object pattern gebruikt die je brengt waar je wilt zijn. Een CustomerFactory bouwt CustomerEntity objects, dit is jouw person. De CustomerFactory bouwt IN de customerentity een object bestaande uit EntityFields, die gevuld worden door de CustomerDAO, gebouwd door de DAO factory. De properties van CustomerEntity indexeren in het EntityFields object in de CustomerEntity.

Je mapt dus in feite de datacore die je IN je entity class schuift. Standaard is die entity class leeg, maar door de DAO te vullen en er in te plaatsen krijgt de entity een waarde. Dit komt een beetje overeen met het DAO pattern van Sun: http://java.sun.com/blueprints/patterns/DAO.html (goh, wie had ooit gedacht dat ik een pattern example op de SUN site zou aanprijzen :D).

De 'mappings', dus de map van EntityField op table/view field zit in de definitie van het EntityField op positie X in het object EntityFields dat voor de CustomerEntity wordt gecreeerd door een factory. Door een simpele enum indexeer je hier in en je hebt altijd de juiste mapping EN values bij de hand. Simpele versioning kan je direct op het veld implementeren, alsook dirty flag management. Omdat je de fields zet dmv de properties van de entity classes, weet je wanneer de entity dirty is EN je weet de volgorde van de wijzigingen.

BL code voeg je toe aan een class die je derived van de CustomerEntity class, die dmv een overriden virtual method wijzigingen van base class properties ontvangt en daarop anticipeert (BL checks uitvoert en bv exceptions gooit wanneer dat niet goed is).
Ik hoopte dus eigenlijk dat er iemand een mooie oplossing had bedacht :)
er zijn er een aantal hoor, ik heb in ad-hoc style de mijne opgelepeld (is wel complexer in het echt mind you, maar dat lazy loading zou ik iig niet doen), en een aantal andere zijn wel gedocumenteerd bv: http://www.uq.net.au/~zzabergl/simpleorm/whitepaper.html

Overigens kun je je Unit Of Work zien als een transaction coordinator: wanneer je een wijziging hebt, plaats je je entity class of collection class in de unit of work (na een initialisatie) en dat doe je totdat je je transactie wilt afsluiten, je commit en de unit of work kachelt alle wijzigingen in 1 transaction af, na gesort te hebben op de juiste volgorde (mocht je dat willen). Enkele save acties op entiteiten kun je ook direct doen zonder unit of work.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
In Employee klasse:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Department Department
{
    get
    {
        if (IsExpanded)
        {
            return _department;
        }
        else
        {
            Expand();
            return _department;
        }

    }
    set
    {
        if (IsExpanded)
        {               
            _department = value;
            base.IsDirty = true;
        }
        else
        {
            _department = value;
        }
    }
}

Simpel... als een object aan het expanden is NIET dirty zetten, anders wel! Jimmy Nilsson heeft me op het idee gebracht entiteiten een IExpandable interface te laten implementeren. Werkt overigens ook prima in geval van inheritance. Je hebt dan een Expand() in de base class als in de concrete implementatie klasse bv Employee.

De mapper implementeerd of een ISimpleLoader of IAdvancedLoader wat duidt op lazy loading behavior of de varianten daarop:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
    public class EmployeeMapper : AbstractMapper, IEmployeeFinder, IAdvancedLoader
    {
        public EmployeeMapper()
        {
        }

        public AbstractDomainEntity Load(DataRow dr, bool lazy) 
        {
            AbstractDomainEntity domainObject;
            int id = Convert.ToInt32(dr["EmployeeID"].ToString());

            if (((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.ContainsKey(id))
            {
                return ((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.GetDomainEntity(id);
            }
            else
            {
                if (lazy)
                {
                    domainObject = DoLazyLoad(dr);
                }
                else
                {
                    domainObject = DoLoad(dr);
                }

                ((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.AddDomainEntity(((Employee)domainObject).ID, domainObject);
                return domainObject;
            }
        }
        
        public BaseDomainEntityCollection LoadAll(DataTable dt, bool lazy)
        {
            AbstractDomainEntity domainObject;
            BaseDomainEntityCollection clDomainObjects = new EmployeeCollection();

            foreach(DataRow dr in dt.Rows)
            {
                domainObject = Load(dr, true);
                
                clDomainObjects.Add(((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.GetDomainEntity(((Employee)domainObject).ID));
            }

            return clDomainObjects;
        }

        public AbstractDomainEntity DoLoad(DataRow dr)
        {
            throw new NotImplementedException();
        }

        public AbstractDomainEntity DoLazyLoad(DataRow dr)
        {
            Employee employee = new Employee(
                Convert.ToInt32(dr["EmployeeID"].ToString()),
                dr["FirstName"].ToString(),
                dr["MiddleName"].ToString(),
                dr["LastName"].ToString(),
                dr["Initials"].ToString(),
                dr["Description"].ToString(),
                Convert.ToDateTime(dr["StartedOn"].ToString()),
                Convert.ToInt32(dr["SecurityID"].ToString()),
                null);

            return employee;
        }

        public Employee Find(int id)
        {           
            if (((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.ContainsKey(id))
            {
                return (Employee)((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.GetDomainEntity(id);
            }
            else
            {
                EmployeesGateway empsGateway = new EmployeesGateway();              
                empsGateway.EmployeeID = id;
                DataTable empTable = empsGateway.SelectOne();

                return (Employee)Load(empTable.Rows[0], true);
            }
        }

        public EmployeeCollection FindByFirstName(string firstname)
        {
            DataTable empsTable = new DataTable();

            empsTable = new EmployeesGateway().ExecuteQuery( "SELECT * " +
                "FROM Employees " +
                "WHERE (FirstName = '" + firstname + "')" );

            return (EmployeeCollection)LoadAll(empsTable, true);
        }

        public EmployeeCollection FindByDepartment(int departmentid)
        {
            DataTable empsTable = new EmployeesGateway().ExecuteQuery("SELECT Employees.EmployeeID, Employees.FirstName, Employees.MiddleName, Employees.LastName, Employees.Initials, Employees.Description, Employees.StartedOn, Employees.SecurityID " +
                "FROM Employees INNER JOIN " +
                "DepartmentEmployees ON Employees.EmployeeID = DepartmentEmployees.EmployeeID " +
                "WHERE (DepartmentEmployees.DepartmentID = " + departmentid.ToString() + ")" );

            return (EmployeeCollection)LoadAll(empsTable, true);
        }

        public Employee FindByInitials(string initials)
        {
            if (((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.ContainsEmployeeWithInitials(initials))
            {
                return ((PromasSTPersistenceRegistry)_persistReg).EmployeeIdentityMap.GetEmployeeByInitials(initials);
            }
            else
            {
                DataTable empTable = new EmployeesGateway().ExecuteQuery("SELECT * " +
                    "FROM Employees " +
                    "WHERE (Initials = N'" +  initials + "')");

                return (Employee)Load(empTable.Rows[0], true);
            }
        }

        public EmployeeCollection Find()
        {           
            return (EmployeeCollection)LoadAll(new EmployeesGateway().SelectAll(), true);
        }
    }

#if NUNIT_TESTS
    [TestFixture]
    public sealed class TestEmployeeMapper
    {
        [Test]
        public void EmployeeFinder()
        {
            EmployeeMapper empMapper = new EmployeeMapper();
            Employee emp;

            emp = empMapper.Find(1);
            Assertion.Assert(emp != null);
        }
    }
#endif


Zo ziet de mapper d'r uit

[ Voor 101% gewijzigd door Scare360 op 05-05-2003 12:29 ]


  • zoepercavia
  • Registratie: September 2001
  • Laatst online: 26-12-2025
Bedankt allemaal voor de waardevolle (en uitgebreide) antwoorden.
De oplossing is dat je NIET bv Orders onderbrengt bij Customer of Employee, maar als zelfstandige entiteit gebruikt, als 'collection' variant van de entiteit Order. Je code van deze Collection variant moet methods bevatten waar je Entity objects aan kunt meegeven, bv:
Customer c = new Customer(CustomerID); // load customer
Employee e = new Employee(EmployeeID); // load employee
Orders o = Orders.Retrieve(c, e); // load in o all orders of c taken by e.
Als ik het zo lees vind ik dit eigenlijk ook wel een erg mooie oplossing. Misschien ga ik dat later zo proberen te implementeren. Op dit moment is de situatie min of meer zoals beschreven door paulgielens (waarschijnlijk ook uit Fowler's boek). Ik was even vergeten dat mijn DomainObject een status heeft 'isLoading'. Hoogstwaarschijnlijk kan ik daarvan gebruik maken om te controleren of het object dirty mag of niet.

Tenslotte nog even een denk vraagje, wat is meer OO: de situatie zoals Efbe die beschrijft; dus referenced collections buiten de owner plaatsen en ze opzoeken door in de mapper van de collection te zoeken, OF: de situatie als ik hem nu heb referenced collection binnen de class van de owner plaatsen en deze laten vullen door de mapper van de owner.

De klassieke voorbeelden volgend zou ik denken het laatste, echter vind ik ook veel te zeggen voor de oplossing van EfBe omdat je hierbij de DomainEntities wat meer gescheiden van elkaar houdt.

Wat is hier theoretisch en praktisch voor te zeggen?

Panacea.NL als je geinteresserd bent in IT en Geneeskunde!


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
In de Department klasse, instantie van "een" mapper maken en die vult de collectie. Ik zou alle entiteiten en collecties voorzien van een interface en deze interfaces bundelen in een package. Deze package zou dan beschikbaar moeten zijn over zowel de persistence laag als de business laag.

code:
1
2
// Ophalen employees voor deze afdeling
EmployeeCollection employees = new EmployeeMapper().FindByDepartment(_id);


Ik zou ZEKER een identity map per entity (of indien je met Guid's werkt 1 identity map voor alle entiteiten)

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class EmployeeIdentityMap : AbstractIdentityMap
{
    public EmployeeIdentityMap(string name)
    {
        _name = name;
    }

    public bool ContainsEmployeesWithDepartmentId(int departmentid)
    {           
        foreach(object element in _domainEntsMap.Values)
        {
            if ( ((Employee)element).Department.ID == departmentid )
            {
                return true;
            }
        }
        
        return false;
    }

    public bool ContainsEmployeesWithFirstName(string firstname)
    {           
        foreach(object element in _domainEntsMap.Values)
        {
            if ( ((Employee)element).FirstName.Equals(firstname) )
            {
                return true;
            }
        }
        
        return false;
    }

    public bool ContainsEmployeeWithInitials(string initials)
    {           
        foreach(object element in _domainEntsMap.Values)
        {
            if ( ((Employee)element).Initials.Equals(initials) )
            {
                return true;
            }
        }
        
        return false;
    }

    public EmployeeCollection GetEmployeesByDepartmentId(int departmentid)
    {
        EmployeeCollection clEmps = new EmployeeCollection();

        foreach(object element in _domainEntsMap.Values)
        {
            int empid = ((Employee)element).ID;
            int empdeptid = ((Employee)element).Department.ID;

            if ( ((Employee)element).Department.ID == departmentid )
            {
                clEmps.Add( (Employee)element );
            }
        }
        
        return clEmps;
    }

    public EmployeeCollection GetEmployeesByFirstName(string firstname)
    {
        EmployeeCollection clEmps = new EmployeeCollection();

        foreach(object element in _domainEntsMap.Values)
        {
            if ( ((Employee)element).FirstName.Equals(firstname) )
            {
                clEmps.Add( (Employee)element );
            }
        }
        
        return clEmps;
    }

    public Employee GetEmployeeByInitials(string initials)
    {
        foreach(object element in _domainEntsMap.Values)
        {
            if ( ((Employee)element).Initials.Equals(initials) )
            {
                return (Employee)element;
            }
        }

        throw new Exception();
    }
}


Ik heb de Find(Guid guid) niet in de AbstractDomainEntity, AbstractMapper, AbstractIdentityMap gefactored, maar Fowler geeft ook duidelijk aan wanneer dat wel/niet te doen.

IdentityMap kan je dus ook round-trips besparen... maar primair doel is identiteit geven aan business objecten.

ps: Zorg dat je je niet laat verleiden om objecten "zo laat mogelijk" weg te schrijven naar de db... gewoon meteen persisten als een dirty gezet wordt. Ik gebruik daar een IEntityStatusReporter voor die een event registreert en door de betreffende mapper geimplementeerd wordt.

if (object.getType() == typeOf(Employee))
new EmployeeMapper().Save((Employee)object);

Hierbij zou je de mapper transaction supported moeten maken, zodat business processen meerdere business objecten binnen een transactie kunnen persisten en de de sturende rol op zich nemen.

Ow en praktisch! Collecties vullen via een in param geeft je problemen als je Remoting gebruikt. MarshalByRefObject is not the way to go in dit geval, lees de weblog van Clemens Vasters maar een door.

[ Voor 17% gewijzigd door Scare360 op 05-05-2003 15:49 ]


  • EfBe
  • Registratie: Januari 2000
  • Niet online
Objects persisten binnen een transactie kun je bv delegeren aan een UnitOfWork class, die zodra er een entity wordt ge-add aan het geinitialiseerde UnitOfWork object, deze actie meteen uitvoert, binnen de transactie gecontrolleerd door de UnitOfWork. Je hebt dan alleen geen mogelijkheid acties te sorten op volgorde ivm FK relaties maar dat is meestal ook niet zo nodig. Je kunt dus transparant voor de developer dus zowel laat als vroeg persisten: de UnitOfWork.Commit() start de acties bij de late persistence actie en bij een vroege persistence actie geeft hij louter aan dat de UnitOfWork klaar is met zn werk en de transactie af kan sluiten.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Verwijderd

Ook ik ben met fowler aan de slag gegaan, en kom op de volgende oplossing:

Na een load van een domain object wordt MarkLoaded() angeroepen. Deze method zorgt ervoor dat het domain object een andere status krijgt, namelijk loaded (=clean object) en is dus NIET dirty meer. Dit werkt prima

public void MarkLoaded()
{
bool result = _unitOfWork.RegisterClean(this);

if(result)
{
_status = Status.Loaded;
}
}

hier nog wat statussen die een object doorloopt:

public enum Status
{
New,
Ghost,
Loading,
Loaded,
Dirty,
Removed,
}


wel leuk dat er nog meerdere tweakers hiermee bezig zijn, misschien moeten we eens wat krachten bundelen > ORM wordt namelijk nog veel ingewikkelder dan dit (oa inheritance, cross tables).

Verwijderd

ow, voor de mensen die het boek niet hebben, hier staat eea : http://www.martinfowler.com/eaaCatalog/

  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
Verwijderd schreef op 06 May 2003 @ 16:42:
wel leuk dat er nog meerdere tweakers hiermee bezig zijn, misschien moeten we eens wat krachten bundelen > ORM wordt namelijk nog veel ingewikkelder dan dit (oa inheritance, cross tables).
Ik zou zeggen doe maar eens een code drop, heb zelf hier een werkend mapping framework m.b.v. Fowlers boek, maar dan voor read only data, inclusief demo voor de LLBLGen demo database + n:n relaties. Ik zou graag het Unit of Work pattern verwerken en de zaak open-sourcen! Een volgende aanvulling zou een query object zijn bv.

Ik ben nog steeds van mening dat een abstract Framework the bomb is, mensen implementeren zelf de mappers en entiteiten tegen het framework aan (eventueel mbv een generator, LLBLGen wordt gebruikt voor de DataGateway's). Dan heb je een efficiente data en business laag met transparante persistence!

  • zoepercavia
  • Registratie: September 2001
  • Laatst online: 26-12-2025
wel leuk dat er nog meerdere tweakers hiermee bezig zijn, misschien moeten we eens wat krachten bundelen > ORM wordt namelijk nog veel ingewikkelder dan dit (oa inheritance, cross tables).
.

Ik vind het best :P
Op dit moment heb ik al een aantal dingen min of meer aan de praat uit Fowlers boek. Dit is de data mapper met lazy loading, unit of work, identity map en inheritance mappers. Ik zeg min of meer omdat ik het absoluut nog niet goed getest heb en er nog een hele zooi moet gebeuren. De problemen waar ik nu tegen aan loop zijn:
- complexiteit: testen is moeilijk omdat het als mens lastig is alle situaties te overzien
- unit of work werkt wel maar tussentijds committen en de goeie volgorde van committen moet nog
- los van elkaar zijn lazy loading en unit of work prachtig maar te samen kost het heel wat hoofdbrekens
- liefst zou ik code generation willen, maar wel met optimale vrijheid voor de developer, de commerciele mappers wordt ik toch niet zo blij van (sommige zijn bijvoorbeeld alleen maar datacontainers, andere hebben naar mijn smaak een te strikte syntax waardoor het 'OO-gevoel verdwijnt'
- lazy loaden van lijsten zit nog bagger in elkaar, ik vond het voorbeeld uit Fowler's boek niet zo mooi
- ik moet nog een mooi plekje vinden voor de concurrency controller, unit of work lijkt me mooi, maar het zou leuk zijn als ie pluggable kan

Daarnaast ben ik er nog niet helemaal uit over hoe om te gaan met collections:
'Person.Departments' vind ik meer OO
maar
DepartmentCollection deps = new DepartmentMapper().FindForPerson(1)
heeft ook al wat en is misschien handiger zoals Efbe al aangaf

Maar een samenwerking lijkt me natuurlijk altijd leuk, al is het maar om het samenwerken :)
(Je eigen opensource mapper voor je projectjes is natuurlijk ook nooit gek }))

Panacea.NL als je geinteresserd bent in IT en Geneeskunde!


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
Misschien dat we eens een workspace open moeten gooien op gotdotnet... wie weet komt er iets leuks uit. Het lijkt me verstandig eerst het mapping framework (structuur) goed uit te werken, waarna concrete implementaties redelijk tot goed te genereren zijn. Ik ben al eens bezig geweest met een extensie voor de LLBLGen code-base.

M.b.t. collections:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;

using Paul.Persistence;

namespace Paul.Domain.Entities
{
    /// <summary>
    /// Decription
    /// </summary>
    /// <remarks>
    /// ...
    /// </remarks>
    /// <authors>
    ///   <author name="PGIE" email="p.gielens@connexx.nl" />
    /// </authors>
    public class EmployeeCollection : BaseDomainEntityCollection
    {
        public EmployeeCollection() : base() {}
        public EmployeeCollection(EmployeeCollection value) : base() {}
        public EmployeeCollection(Employee[] value) : base() {}
    }
}


Een goede base class doet wonderen. Het is op zich niet zo zinnig collecties lazy te maken... je kan veel beter de objecten die in de collectie geladen worden zelf lazy houden.

  • EfBe
  • Registratie: Januari 2000
  • Niet online
Het probleem is vaak: afronden EN op zo'n manier dat een ANDER er wat aan heeft. Quote van Ronald Laermans, VC++ compiler team:
"For both this and the previous point, it is important to be aware (and most
people learn by being burnt very badly once) that if you have a product that
works perfectly for internal use, you are somewhere between 5% and 20% on
the way to having something commercially viable. If you want to hear a non
Microsoft opinion on that, just talk to Chris Sells (from Developmentor)
about his experiences being the development lead for Gen<X>."

http://groups.google.com/....356%40tkmsftngp04&rnum=6

En het is juist die vele extra uren en zweetdruppels die je erin steekt die een tool/platform/applicatie het waard maken voor anderen. Het streven is nobel, maar weet waar je aan begint, want het valt zeker niet mee. Plus: 'commerciele kwaliteit' moet je halen, wil je dat het succesvol is. :)

Hier nog wat tips:
Maak collection classes EN entity class. 1 collection class per entity class. De collection class heeft de 1 of meer-retrieval routines (static), de entity class alleen de single retrieval routine (via constructor (PK) of method (UC))
Collection class C van entity E heeft alle properties (fields) van E. Om bulk-operaties op een set van E los te laten, definieer je methods op C. Je kunt bv alle Entities retrieven die de waarde voor C.Property1 en C.Property2 hebben (die zijn gechanged, dus gebruik je die), en je fetcht die nadat je eerst de properties zet en daarna Fetch aanroept.
Definieer relaties tussen fields van entities. Deze relaties genereer je in static classes. Die relaties gebruik je bij het definieren van ad-hoc queries.
Bij bulk-retrieval van lijsten, gebruik een serie innerjoins van alle betrokken tabellen/views en subselect daar dan de columns uit. Dit is sneller dan een serie subselects in 1 query.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
ps: voor de geinteresseerden hier eens bekijken.

[ Voor 3% gewijzigd door Scare360 op 07-05-2003 09:37 ]


  • EfBe
  • Registratie: Januari 2000
  • Niet online
paulgielens schreef op 07 mei 2003 @ 09:37:
ps: voor de geinteresseerden hier eens bekijken.
*oef* :) Dat is een hoop materiaal. Bedankt Paul! :) Het is altijd leuk het allemaal zelf te verzinnen, maar dat kost wel tijd, en als het al verzonnen is, dan is die tijd dus te besparen :) (alhoewel ik nog geen persistence framework ben tegengekomen die stateless werkt, ze gebruiken allemaal in-core caching)

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • EfBe
  • Registratie: Januari 2000
  • Niet online
Ik zit nu die pdf door te lezen van die meneer Ambler, die lijst met punten waar een persistence layer aan moet voldoen bv, en ik zit het echt met stijgende verbazing aan te kijken... Ten eerste roept hij dat persistence layers wijzigingen aan de database niet door laten werken in je BL code. Dit is onzin. Als je eerst een attribuut "leeftijd" hebt voor de entiteit "Klant", dan bouw je BL code daarvoor. Als je daarna dat attribuut weghaalt uit de database, moet je je BL code ook wijzigen. Het is een mythe dat je een persistent layer kan bouwen die database wijzigingen verbergt. (ook nog los van het feit dat wanneer je kolommen gaat renamen in de database, en je de properties behorende bij die velden dezelfde namen laat houden ivm BL code, je een onbeheersbare kluwen krijgt op een gegeven moment, want property X in entiteitclass Y mapt op kolom ABC, in tabel DEF die entiteit G representeert. )

Ten tweede begint hij over een OID (ObjectID). Erm... we praten over entiteiten, die HEBBEN een unieke referentie, die wordt o.a. gebruikt bij het opslaan van een entiteit in een database: de primary key. Een primary key is precies wat een OID doet: een entiteit uniek identificeren. Je hebt dus helemaal geen OID nodig, integendeel: 2 keys voor hetzelfde is onzinnig en geeft aan dat je in implementatie-technische aspecten denkt en niet in logische, fundamentele aspecten: tenslotte gaat het om entiteiten, niet om objects.

Het allermooiste vond ik nog dat je meerdere connecties moet ondersteunen zodat je entiteiten van de ene database (bv een centrale database) naar de andere (bv een lokale database) kunt kopieren. Ja laten we dat doen :) Hoe meer distributie van data, hoe beter, des te sneller is alles out of sync en onbeheersbaar (en niet meer productief).

(edit 2): hij heeft ook wel goede punten overigens, het is wel degelijk een goede whitepaper, zeker wanneer het aankomt op de daadwerkelijke core engine: hoe bouw ik flexibel sql queries.

[ Voor 25% gewijzigd door EfBe op 07-05-2003 12:08 ]

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • zoepercavia
  • Registratie: September 2001
  • Laatst online: 26-12-2025
EfBe schreef op 06 mei 2003 @ 22:52:
[...]

Hier nog wat tips:
Maak collection classes EN entity class. 1 collection class per entity class. De collection class heeft de 1 of meer-retrieval routines (static), de entity class alleen de single retrieval routine (via constructor (PK) of method (UC))
Collection class C van entity E heeft alle properties (fields) van E. Om bulk-operaties op een set van E los te laten, definieer je methods op C. Je kunt bv alle Entities retrieven die de waarde voor C.Property1 en C.Property2 hebben (die zijn gechanged, dus gebruik je die), en je fetcht die nadat je eerst de properties zet en daarna Fetch aanroept.
Definieer relaties tussen fields van entities. Deze relaties genereer je in static classes. Die relaties gebruik je bij het definieren van ad-hoc queries.
Bij bulk-retrieval van lijsten, gebruik een serie innerjoins van alle betrokken tabellen/views en subselect daar dan de columns uit. Dit is sneller dan een serie subselects in 1 query.
Ik neem aan dat je ook nog finders moet definieren op entity mapper M voor E, als volgt: M heeft methodes om collection C van entity E te vinden (EmployeeCollection EmployeeMapper().FindForPerson(Person person)). Of moeten deze ook gedefinieerd worden op C? Dit omdat Person bv. geen property is van een department.

Daarnaast nog een andere vraag. Hoe leg ik nieuwe verbindingen tussen entities? Stel dat Employee <-> Department een n:n relatie is dan zat ik te denken aan bv. Department.AddPerson(Person person). Alleen hoe moet dit gemapt worden? Doe je een vroege commit (cq direct), dan gaat de gemakkelijkheid van een verborgen transaction manager (unit of work) verloren. Doe je een late commit door de unit of work dan is de relatie pas na de commit beschikbaar (dus niet tijdens runtime).

Fowler lost dit in zijn geheel anders op door collecties aan entiteiten te plakken (Person.Departments.Add(Department department)) waardoor je altijd de correcte lijst te pakken hebt en dan door de mapper te laten controleren wat er aan de collection is veranderd, kan hij op de juiste manier gecommit worden.

Is er een oplossing om dit ook voor elkaar te krijgen als je werkt met collections die niet direct aan entiteiten hangen?

Panacea.NL als je geinteresserd bent in IT en Geneeskunde!


  • EfBe
  • Registratie: Januari 2000
  • Niet online
Employee.GetDepartments() -> levert typed collection met department entity instances voor de betreffende employee (ivm een m:n relatie). Je kunt door foreign key analyse alle m:n relaties opzoeken: iedere tabel met 2 of meer foreign keys verwezenlijkt 1 of meer m:n relaties.

De Department.AddPerson() is een custom routine die bv een zekere hoeveelheid business logic in zich draagt (niet iedereen mag bv bij een department ik noem maar iets). Ik weet niet of je dat zo zou moeten implementeren. Wil je fysiek de relatie leggen tussen een employee en een department, dan zul je het wellicht wel zo moeten doen, maar 'addperson' zou dan puur een row in de koppeltabel moeten stoppen. Echter dit levert problemen op bij 'objectified' m:n relaties. (dit zijn relaties in koppeltabellen met een aparte key, bv een identity field, en vaak ook een extra attribute, bv een datum wanneer een employee voor een department is gaan werken). Beter is dan een entiteit te realiseren voor de koppeltabel en daar dus een nieuwe entiteit-instance aan toe te voegen mbv de 2 entiteiten die je hebt: department en employee.

Dit is wel een heikel punt hoor, voor veel verschillende oplossingen valt wat te zeggen en ook weer wat negatiefs voor aan te dragen. (een entiteit in het leven roepen voor een koppeltabel is bv niet altijd gewenst, je verplicht de developer dan toch tot op zekere hoogte kennis te nemen in de relaties tussen entiteiten en hoe die lopen, maar ik ben van mening dat je daar niet aan ontkomt).

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • zoepercavia
  • Registratie: September 2001
  • Laatst online: 26-12-2025
Dat betekent dus dat je zoiets krijgt als een entiteit PersonDepartmentRelation. Je zou hier dan een aparte mapper voor kunnen schrijven die de koppeling persisteert. Hoewel je misschien makkelijker objectified m:n relaties kan maken vind ik dit toch minder intuitief dan het direct koppelen van een persoon aan een department. Misschien valt het in de praktijk mee, maar ik heb toch het idee dat de database dan te veel doorschemert (de domein laag begint dan weer verdacht veel te lijken op je data laag, waar je immers ook functionaliteit voor koppelingen (koppeltabellen) hebt).
Welke oplossing je ook kiest, mijn probleem, van wanneer de koppeling te persisteren, blijft. Misschien even een voorbeeldje ter verduidelijking:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1) met koppel entiteit, maar wanneer ga je persisteren?
Person laurens = new Person("Laurens");
Department got = new DepartmentMapper().FindByName("GoT");
PersonDepartmentRelation gotpeopleRelation = new PersonDepartmentRelation(got, laurens, DateTime.Now);

//zoek de mensen van GoT op
PersonCollection gotpeople = new PersonMapper().FindForDepartment("GoT");

//schrijf personen in GoT weg, collectie bevat laurens nog niet, tenzij de
 PersonDepartmentRelationMapper de koppeling direct wegschrijft, dan 
heb je echter niet de voordelen van de transaction manager
foreach(Person person in gotpeople) {
Response.Write(person.Name);
}

//commit de hele zooi
UnitOfWork.Commit();


A la Fowler:
code:
1
2
3
4
5
6
7
8
9
10
11
12
//2) met de collection direct aan de entiteit
Person laurens = new Person("Laurens");
Department got = new DepartmentMapper().FindByName("GoT");
got.Persons.Add(laurens);

//Got bevat laurens al (collection is geheel in-memory)
foreach(Person person in got.Persons) {
Response.Write(person.Name);
}

//commit de hele zooi
UnitOfWork.Commit();


Voordelen van elk:
- 1 biedt meer mogelijkheden voor objectified relations
- 1 voorkomt het (tricky) lazy laden van lijsten binnen entiteiten
- 1 voorkomt mixen van collections en entiteiten van verschillende klasses
- 2 altijd de juiste lijst beschikbaar
- 2 intuitiever (imho), ontwikkelaar ziet direct welke relaties er bestaan voor een bepaalde entiteit
- 2 geen extra entiteit nodig voor de relatie
- 2 de transaction manager kan zelf alles afhandelen

Nu weet ik dus helemaal niet meer wat het handigste is... aargghh :(

[ Voor 2% gewijzigd door zoepercavia op 11-05-2003 14:25 . Reden: opmaak ]

Panacea.NL als je geinteresserd bent in IT en Geneeskunde!


  • EfBe
  • Registratie: Januari 2000
  • Niet online
Heh :) zoals ik zei: voor veel mogelijkheden valt wat te zeggen en een duidelijke keuze is erg moeilijk, het hangt af met wie je praat. Als je bv een dynamic query engine bouwt in je O/R mapper, dus een mogelijkheid waarbij je makkelijk via typesafe constructies een selectie kunt bouwen zonder sql en die uit kunt voeren, ontkom je niet aan het feit dat de developer kennis moet hebben van zekere relaties tussen entiteiten (ik vind dit ook niet erg, ik bedoel, je kunt naar developers communiceren dat order aan customer hangt, en product aan order via orderdetails, that's it). Bouw je alles hardcoded in, dan kun je het inderdaad afschermen, maar je moet dan wel erg veel werk doen.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
Vreemde constructies mensen:

Laurens kijk eens in die code die ik je gestuurd heb...
code:
1
2
3
4
5
6
7
8
9
10
Employee paul = persReg.EmployeeMapper.FindByAlias("PGIE");
Console.WriteLine(paul.Department.Name);

EmployeeCollection employees = persReg.EmployeeMapper.Find();

foreach (Employee employee in employees)
{
  Console.WriteLine(employee.Department.Name);

}

Ff fout afhandeling daargelaten...
code:
1
2
3
4
5
6
7
Employee paul = new Employee();
paul.Name = "p.gielens";
paul.Department = persReg.DepartmentMapper.Find("A");
paul.Save();

// write forceren
UnitOfWork.Commit()


Mappers encapsuleren relaties! Veelal zul je voor iedere ingang voor het ophalen van een relatie, een finder methode introduceren. Als je een query object implementeert kun je je een hoop werk besparen m.b.t. de finders. De enige voorwaarde is dat entiteiten een lege constructor hebben. Desnoods bouw je security in, zodat alleen de persistence laag "lege" objecten kan instantieeren. Laurens kijk ook eens in de EmployeeIdentityMap, alsje per entiteit een identity map opmaakt, kun je voor zoeken van objecten, bv met de alias "PGIE" gewoon de identity map raadplegen. Dit scheelt dus weer roundtrips! Hierboven staat die code ook geloof ik.

Het mooiste is natuurlijk OID, zodat je je generieke Mapper, Find en Contains methodes kunt factoren in de abstracte laag van het framework. Entiteit specifieke zaken trek je een nivo hoger... ook weer een punt waar kant en klare oplossingen als EntityBroker de plank misslaan. Je levert zoveel in aan flexibiliteit met een "uit de doos" oplossing, terwijl je in bedrijfs kritische applicaties gewoon die persistence laag wil optimaliseren naar gelang de situatie (de manier waarop de entiteiten gebruikt worden). ps: KeyFactory, zodat je als je bv een bestaand model gebruikt allerhande variateit aan prim keys (int, varchar) kunt mixen.

[ Voor 90% gewijzigd door Scare360 op 12-05-2003 09:11 ]


  • EfBe
  • Registratie: Januari 2000
  • Niet online
paulgielens schreef op 12 May 2003 @ 08:49:
Mappers encapsuleren relaties! Veelal zul je voor iedere ingang voor het ophalen van een relatie, een finder methode introduceren. Als je een query object implementeert kun je je een hoop werk besparen m.b.t. de finders.
Je kunt geen query object implementeren zonder het veronderstellen van relaties. Als jij Customer <-> Order <-> OrderDetail <-> Product <-> Supplier in je database hebt, en jij wilt alle Customers die producten hebben gekocht van supplier X, dan MOET je relaties kennen om dat te bouwen met een query builder achtige functionaliteit, OF je moet dat hardcoded in je mapper opnemen (maar dat is veel werk) OF je moet dat op een hoger niveau kunnen genereren mbv de O/R mapper die je gebruikt (ik kan je verzekeren, dat wordt lastig). Die relaties kun je wel in classes omvatten, zoals Customer.Relations.Order levert een object dat de relatie Customer - Order impliceert en de gegevens voor de query builder hoe die relatie te gebruiken (dus welke tables te joinen over welke velden).

Het omvatten van relaties tussen entiteiten in een mapper is veel werk als jedat met de hand moet doen. Je hebt voor iedere 1:1, 1:m en m:n relatie methods nodig, en daarbinnen filters die flexibel zijn (dus 1, 2 wellicht 3 velden en alle combinaties daartussen).
De enige voorwaarde is dat entiteiten een lege constructor hebben. Desnoods bouw je security in, zodat alleen de persistence laag "lege" objecten kan instantieeren. Laurens kijk ook eens in de EmployeeIdentityMap, alsje per entiteit een identity map opmaakt, kun je voor zoeken van objecten, bv met de alias "PGIE" gewoon de identity map raadplegen. Dit scheelt dus weer roundtrips! Hierboven staat die code ook geloof ik.
Je bedoelt dat je instances cached ?
Het mooiste is natuurlijk OID, zodat je je generieke Mapper, Find en Contains methodes kunt factoren in de abstracte laag van het framework. Entiteit specifieke zaken trek je een nivo hoger... ook weer een punt waar kant en klare oplossingen als EntityBroker de plank misslaan.
Het klinkt leuk, maar ik begrijp niet echt waar je op doelt. De find, retrieve of hoe je ze ook noemt, methods die entiteiten ophalen, hebben entiteitspecifieke zaken nodig om te kunnen functioneren. Je kunt die wel generiek proberen te implementeren maar veel verder dan een paar regels code kom je niet. Of je moet je find methods in je entity classes implementeren mbv je query builder, wat generieke code oplevert. EntityBroker is op zich niet zo slecht hoor, het is niet zo handig in het gebruik en mist essentiele zaken maar ik snap niet echt waarom je je als developer uberhaupt druk moet maken over het feit of een 'CustomerEntity' class nu uit 2 delen (een entity specific class en een abstracte generieke class) of 1 deel bestaat (een entity class met alle logica daarin). Je werkt met de CustomerEntity class en that's it.

Waar kant en klare frameworks de plank misslaan is waar ze de BL code verstoppen. Die moet je vaak met kunst en vliegwerk toevoegen aan de entity class (bv een property 'AccountValue' van een entity Account mag niet < 0 zijn), Entity Broker bv verlangt van je dat je alle properties nog eens gaat implementeren, ze zijn abstract. Ik volg het Strategy pattern, je erft van een validation class en override elke method die je wilt aanpassen, de rest heeft basic checks (ivm database field definities bv stringlength) en kun je laten voor wat het is.
Je levert zoveel in aan flexibiliteit met een "uit de doos" oplossing, terwijl je in bedrijfs kritische applicaties gewoon die persistence laag wil optimaliseren naar gelang de situatie (de manier waarop de entiteiten gebruikt worden).
Mja, dat impliceert dat je t.a.t. de persistence laag met de hand schrijft, en dat lijkt me een inmens monikkenwerk. Volgens mij gebruik je 'entiteiten' als concept altijd hetzelfde. Je kunt om performance redenen, instances van entiteiten proberen te cachen, maar voor de rest is het vrij uniform. Tuurlijk zijn O/R frameworks die je koopt en die een persistence layer voor je maken compromissen en zul je hoe dan ook in sommige situaties nadelen ondervinden van die compromissen, net zoals je in andere situaties de voordelen ervan ondervindt. Ik gooi bv alle stored procedures eruit, alles wordt nu gebouwd met een dynamische query engine. Dat is in sommige gevallen wat nadelig ivm security. Het voordeel is weer dat je geen stored procedures hoeft te onderhouden. Ik moet nog uitgebreide tests doen, maar op zich hoef ik ze niet terug. (scheelt ook wer een generator bouwen ;)).

Als je het met de hand bouwt, dan is het zo op het oog wellicht optimaler voor je situatie, maar vergeet 1 ding niet: omdat je het bouwt voor die situatie, is het pure extra moeite om dingen in je framework te bouwen die je NIET nodig hebt maar later wellicht wel (maar wellicht ook niet). Oplossingen die je aanschaft hebben die vaak wel, wat in andere projecten weer voordeel oplevert.

Om performance-redenen het met de hand zelf bouwen is een aloude mythe uit de game-wereld waar veel bedrijven aan ten onder zijn gegaan. Hoeveel game-studio's gingen niet eerst anderhalf jaar aan een 3D engine werken en daarna nog eens aan het spel, terwijl ze ook direct een game-engine konden licenseren en meteen aan het spel konden gaan werken. De laatste optie scheelt niet alleen in de tijd en kosten, maar je weet ook dat je op tijdstip T met je productie van je spel bezig kunt, en niet dat je moet wachten op de bouw van je game-engine.

Tuurlijk is zelfbouwen leuk en leerzaam. Het moet dan ook zo zijn dat een O/R mapper die de boel voor je genereert genoeg ruimte overlaat voor je om zelf aan de handel te sleutelen, echter ik zou me de moeite besparen om het met de hand te bouwen.
ps: KeyFactory, zodat je als je bv een bestaand model gebruikt allerhande variateit aan prim keys (int, varchar) kunt mixen.
That is... als je wilt cachen. Je moet dan ook ervoor zorgen dat je binnen je applicatie slechts 1 engine hebt, dus in het application object van je asp.net webapp 1 engine voor alle threads, in je multi-user winforms application wordt dat al lastiger. Cachen kost erg veel resources qua development en overhead. Ik vraag me echt af of het de moeite loont. Ik ben op dit moment er niet van overtuigd dat cachen van instances je helpt bij het bouwen van je applicatie, het compliceert de boel, levert meer overhead op in je persistence layer en als je het niet juist implementeert levert het gewoon een gammele applicatie op, door het al vaker bediscussieerde multi-user stateful verhaal.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
Als ik in dat framework unit of work heb zitten zal ik de zaak waarschijnlijk publiceren. Ik heb voor Visual Assist templates gemaakt, zodat je in je IDE gestroomlijnd nieuwe entities en de achterban (concrete mapper, identity map, finders) kunt maken. Wellicht dat ik er nog ooit aan toe komt de basis van LLBLGen 1 te gebruiken om de concrete delen van het framework te "laten" genereren. Nu gebruik ik een aangepaste LLBLGen voor 30% van het persistence raamwerk. Ik ben benieuwd wat LLBLGen 2 gaat brengen zullen we maar zeggen ;)

[ Voor 3% gewijzigd door Scare360 op 12-05-2003 15:02 ]


  • EfBe
  • Registratie: Januari 2000
  • Niet online
LLBLGen 2 probeert per entity class get methods voor alle entities die aan die entity zijn gerelateerd via 1:1, 1:m en m:n relaties te genereren. Daarnaast heb je op de collection classes het brede spectrum aan LLBLGen 1.x methods. Plus typed lists voor custom lists die je typed wilt gebruiken, plus een dynamic query engine die je in je code kunt gebruiken voor adhoc queries. Dat is het wel zo'n beetje. Een unitofwork unit die de huidige ConnectionProvider class gaat vervangen en transaction management op zich neemt en dus in feite de connection beheert naar de database toe wanneer je er 1 start, en zodra je entities of collectionclasses met opdrachten aan de unit of work toevoegt gaan die draaien in die transaction.

Ik ken visual assist niet, maar imho is het wel een stukje solide werk geweest om die templates te maken, wanneer je alle mogelijke relaties ook wilt embedden in code.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 20-08 18:43
EfBe schreef op 12 May 2003 @ 16:28:
LLBLGen 2 probeert per entity class get methods voor alle entities die aan die entity zijn gerelateerd via 1:1, 1:m en m:n relaties te genereren.
Per tabel een entity? Of kan ik bv kiezen om het adres niet als entity te zien, maar Medewerkers en Adressen samen in een medewerker entity proppen?

  • EfBe
  • Registratie: Januari 2000
  • Niet online
paulgielens schreef op 13 May 2003 @ 08:59:
[...]
Per tabel een entity? Of kan ik bv kiezen om het adres niet als entity te zien, maar Medewerkers en Adressen samen in een medewerker entity proppen?
Ja per tabel een entity. Je kunt geen tabellen samennemen in 1 entity, want dan krijg je problemen bij het creeeren van een nieuwe entity: als je gaat saven, hoe bepaal je dan de keys van de rows in de tables? Als je entities gaat samenstellen uit meerdere tables heb je ook het probleem dat je bv 2 velden uit table T bij andere velden voegt, maar bij wegschrijven heb je dan niet genoeg info om een row te vormen in tabel T.

Meerdere tables samennemen in 1 entity is alleen nuttig als je een 1:1 relatie hebt en dan kun je de table dus samenvoegen. Als adres is uitgenormaliseerd, bestaat er geen 1:1 relatie maar een 1:n relatie (adres wordt geshared door meerdere personen). En die kun je nooit samennemen in 1 entity.

Legacy databases die adres bv apart hebben opgeslagen zijn dan wat lastiger te hanteren als je adres niet samenvoegt met personen, terwijl er bv wel een 1:1 relatie is (m.a.w. men slaat ALTIJD het adres op van het persoon, kijkt niet of het adres al bestaat, dit is wat meestal gebeurt). Maar dat is dan jammer, maar helaas. Er zijn talloze crappy datamodellen denkbaar en het is echt de database bouwer die er een zootje van gemaakt heeft. Als je entiteiten opsplitst met een 1:1 relatie over meerdere tabellen dan ben je aan het knoeien en niet aan het datamodelleren.

Ik was van plan multi-table entities te bouwen, maar zoals ik je ook gemaild heb krijg je dan problemen bij het wegschrijven van een nieuwe entity: in welke tables moeten de waarden dan staan? Hetzelfde probleem is zichtbaar bij views in SQLServer en Oracle. Ook samengesteld uit meerdere tables maar je kunt maar 1 table updaten via een view, niet allemaal, en al helemaal geen nieuwe rows toevoegen.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • zoepercavia
  • Registratie: September 2001
  • Laatst online: 26-12-2025
Even een vraagje tussendoor: ik ben nog steeds met die lijsten aan het klooien en heb gekozen voor een combinatie van beide oplossingen. Althans ik maak het iets gemakkelijker voor de ontwikkelaar door collections direct aan entiteiten te hangen. Daarbij maak ik wel gebruik van lazy loading door middel van een thin wrapper. Dat ziet er als volgt uit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person : DomainObject
    {
        private string _name, _surname, _initials = "";
        private GroupCollection _groups = new GroupCollection();

        public GroupCollection Groups 
        {
            get 
            {
                if(_groups.IsGhost) 
                {
                    _groups.MarkLoading();
                    _groups = new GroupMapper().FindForPerson(this);
                    _groups.MarkLoaded();
                }
                return _groups;
            }
        }
//rest van spul


Nu mijn vraag: het werkt wel, maar is het ook netjes? Als dit namelijk goed is, dan ben ik weer een stapje verder :)

edit:

zucht, ik zie dat ik een erg slechte lezer ben :(
Ik heb zonet het wiel opnieuw uitgevonden door bijna precies wat Paul al bedacht had met IExpandable zelf te bedenken.
Vergeet dit ;)

[ Voor 11% gewijzigd door zoepercavia op 14-05-2003 19:43 ]

Panacea.NL als je geinteresserd bent in IT en Geneeskunde!

Pagina: 1