In RDMP DatabaseEntity
objects are class instances that map 1 to 1 with a row in a table in a database.
- The table will have the same name as the class
- The public properties map to columns of the same name in the table (unless they are marked
[NoMappingToDatabase]
) - There will be a public
ID
property on the class and anID
column which is identity autonum and primary key in the table
Often your class will have foreign key properties e.g. CatalogueItem.Catalogue_ID
. For these properties it is recommended to create a property Catalogue
which returns the corresponding object. For example
public Catalogue Catalogue
{
get { return Repository.GetObjectByID<Catalogue>(Catalogue_ID); }
}
Since this is a public property you will need to decorate it with [NoMappingToDatabase]
so that RDMP doesn't think that there should be a corresponding property in the database table.
There are some drawbacks to this:
- Every time the programmer references the property they will go to the database
- Since a new instance is fetched each time you have to copy the returned value to a local variable to edit it properly
- Any UI code that references it e.g. ToString etc will cause lots of redundant database calls
IInjectKnown<T>
is a structured way of handling the caching of known answers as well as injecting known answers in anticipation of someone asking one day.
It is intended to be a wrapper around Lazy<T>
that simplifies caching of these relationship properties as well as providing a single point for marking the cache invalid (e.g. when you change CatalogueItem.Catalogue_ID
int property).
Consider the imaginary class skeleton AggregateTopX
, this class will be dependent on a parent class AggregateConfiguration
public class AggregateTopX: DatabaseEntity
{
#region Database Properties
private int _aggregateConfiguration_ID;
#endregion
public int AggregateConfiguration_ID
{
get { return _aggregateConfiguration_ID;}
set { SetField(ref _aggregateConfiguration_ID, value);}
}
//constructor for creating new instances in memory/database simultaneously
public AggregateTopX(IRepository repository,AggregateConfiguration config)
{
repository.InsertAndHydrate(this,new Dictionary<string, object>()
{
{"AggregateConfiguration",config.ID}
});
if (ID == 0 || Repository != repository)
throw new ArgumentException("Repository failed to properly hydrate this class");
}
//internal constructor for creating instances out of the database
internal AggregateTopX(IRepository repository, DbDataReader r): base(repository, r)
{
AggregateConfiguration_ID = Convert.ToInt32(r["AggregateConfiguration_ID"]);
}
}
First create a relationship property with a getter that gets the parent object. This will go to the database every time.
#region Relationships
[NoMappingToDatabase]
public AggregateConfiguration AggregateConfiguration
{
get { return Repository.GetObjectByID<AggregateConfiguration>(AggregateConfiguration_ID); }
}
#endregion
To cache this result we should move the call to a Lazy<AggregateConfiguration>
field. We can initialize this field in a method SetupLazy
#region Relationships
Lazy<AggregateConfiguration> _knownAggregateConfiguration;
[NoMappingToDatabase]
public AggregateConfiguration AggregateConfiguration
{
get { return _knownAggregateConfiguration.Value; }
}
public void SetupLazy()
{
_knownAggregateConfiguration = new Lazy<AggregateConfiguration>(() => Repository.GetObjectByID<AggregateConfiguration>(AggregateConfiguration_ID));
}
#endregion
This is going to get out of hand fast, especially if we have multiple relationship properties etc. It also isn't exposed by any kind of interfaces. To deal with this we have the interface IInjectKnown<T>
.
Implement the interface IInjectKnown
for AggregateConfiguration
. Rename the SetupLazy
method to ClearAllInjections
.
public class AggregateTopX : DatabaseEntity, IInjectKnown<AggregateConfiguration>
{
#region Database Properties
private int _aggregateConfiguration_ID;
#endregion
#region Relationships
Lazy<AggregateConfiguration> _knownAggregateConfiguration;
[NoMappingToDatabase]
public AggregateConfiguration AggregateConfiguration
{
get { return _knownAggregateConfiguration.Value; }
}
#endregion
public int AggregateConfiguration_ID
{
get { return _aggregateConfiguration_ID; }
set { SetField(ref _aggregateConfiguration_ID, value); }
}
//Constructor that puts a new record into the database
public AggregateTopX(IRepository repository, AggregateConfiguration config)
{
repository.InsertAndHydrate(this, new Dictionary<string, object>()
{
{"AggregateConfiguration",config.ID}
});
if (ID == 0 || Repository != repository)
throw new ArgumentException("Repository failed to properly hydrate this class");
}
//Constructor for fetching existing instances out of the database
internal AggregateTopX(IRepository repository, DbDataReader r)
: base(repository, r)
{
AggregateConfiguration_ID = Convert.ToInt32(r["AggregateConfiguration_ID"]);
}
public void InjectKnown(AggregateConfiguration instance)
{
throw new NotImplementedException();
}
public void ClearAllInjections()
{
_knownAggregateConfiguration = new Lazy<AggregateConfiguration>(() => Repository.GetObjectByID<AggregateConfiguration>(AggregateConfiguration_ID));
}
}
Finally we need to handle InjectKnown
and make sure that ClearAllInjections
/InjectKnown
gets called in our constructors correctly (otherwise we will get a null reference exception when someone uses the AggregateConfiguration
property.
public void InjectKnown(AggregateConfiguration instance)
{
_knownAggregateConfiguration = new Lazy<AggregateConfiguration>(()=>instance);
}
//Constructor that puts a new record into the database
public AggregateTopX(IRepository repository, AggregateConfiguration config)
{
repository.InsertAndHydrate(this, new Dictionary<string, object>()
{
{"AggregateConfiguration",config.ID}
});
if (ID == 0 || Repository != repository)
throw new ArgumentException("Repository failed to properly hydrate this class");
ClearAllInjections();
}
//Constructor for fetching existing instances out of the database
internal AggregateTopX(IRepository repository, DbDataReader r)
: base(repository, r)
{
AggregateConfiguration_ID = Convert.ToInt32(r["AggregateConfiguration_ID"]);
ClearAllInjections();
}
Now we have an interface and standardised public methods we can do things like
- Fetch all
AggregateConfiguration
objects from the database as an array - Fetch all
AggregateTopX
objects from the database as an array- Foreach
AggregateTopX
callInjectKnown
with the requisiteAggregateConfiguration
object which is already in memory
- Foreach
This means that we have 2 database calls up front and never have to make another one since we did a fast operation in memory to tell all the AggregateTopX
what their associated AggregateConfiguration
are. This may not seem like must of a big deal but if you have thousands of objects it can really help prevent you from ddosing your metadata database.
Even if you never call InjectKnown
, having a standard interface method for caching relationship property objects and invalidating these caches (ClearAllInjections
) is still a big performance booster.