Gemli.DataGemli.Data is the O/RM component of the Gemli Project and is the beginning and core of its evolution. It aims to meet the following objectives:
- Database O/RM to provide lightweight persistence support for C# projects with *no* code generators required, *no* XML-based mappings required, *no* attributes required, and *no* code-based setup required in basic scenarios where inferences cannot be made by looking at the POCO with .NET reflection.
- Of course, it also aims to support and work well with code generators, support XML-based mappings, work best by default with attributes-based mapping details, and allow for relatively easy manipulation of mappings in explicit code.
- So, essentially, the mappings are reflection-driven, but can instead be XML-driven or code invocation driven.
- All of the reflection-driven (attributes-driven) mappings are cached to optimized CLR objects in the same way an XML-driven implementation should be.
- You do not have to choose one mapping paradigm or another. XML mappings can override attributes/reflection, and manual code can override further.
- Complete persistence capacity with O/RM-neutral POCO objects with as minimal code or learning curve required as feasibly possible.
- Two or three lines of code to wrap a POCO object, reference a database provider, and persist the POCO object as a DB record.
- Reference field mappings by CLR property/field or by DB column name, whichever is convenient.
- Example:
myQuery.WhereProperty["ID"].IsEqualTo(2) might be the same as myQuery.WhereColumn["mytable_id"].IsEqualTo(2)
- Support for SQL stored procedures based CRUD operations
- Support one-to-one relationships, one-to-many relationships, many-to-many relationships, and many-to-one relationships
- with n-level or infinite deep-loading
- including with SQL stored procedures for per-table CRUD operations (using client-side joins)
- Support all ADO.NET compatible (requires DbFactory) database providers that speak ANSI SQL and support bi-directional parameters
- Expose a basic query mechanism for loading filtered collections of data entities.
- Does not replace the usefulness of other O/RM solutions or raw ADO.NET in scenarios that have complex functional or performance requirements
- If you need significantly more detailed control of your mappings or mapping behavior than the basics, or you need the raw performance of a highly optimized code generator, Gemli.Data probably isn't for you
- Not a do-it-all solution like nHibernate tries to be, only a do-the-obvious data persistence solution.
- No support for aggregate functions (i.e. SUM) or for GROUP BY, or similar advanced queries. There are always workarounds using raw ADO.NET and the load-by-DataRow feature of the DataModel class. ;)
- Probably the biggest down-side of Gemli.Data at this point is that only the Table/Class maps are strongly typed when declaring queries, whereas field names in query conditions must be identified with strings. This can't change with C# 3.0 or earlier without a code generator (or lots of manual code). This might change in the future with C# 4.0 dynamic objects, otherwise it might evolve with some Visual Studio integration to gain some optional "live code generation" that might pre-compile as you are writing code and/or saving changes so that you get the full IntelliSense of a field mapping. All of this is speculative for now.
- Minor data utilities
- Example: Convert a POCO object to a DataRow with one line of code:
var dr = new DataModel<MyPoco>(myObject).Convert.ToDataRow();
This page breaks down the various aspects of working with data-mapped CLR objects. You can always access the generated Help documentation at
http://www.gemli-project.org/Help/.
Simple of Gemli.Data code:
// attributes only used where the schema is not inferred
// inferred: [DataModelTable(Schema = "dbo", Table = "SamplePoco")]
public class SamplePoco
{
// inferred: [DataModelColumn("ID", IsPrimaryKey = true, IsIdentity = true,
// IsNullable = false, DataType = DbType.Int32)] // note: DbType.Int32 is SQL type: int
public int ID { get; set; }
// inferred: [DataModelColumn("SampleStringValue", IsNullable = true,
// DataType = DbType.String)] // note: DbType.String is SQL type: nvarchar
public string SampleStringValue { get; set; }
// inferred: [DataModelColumn("SampleDecimalValue", IsNullable = true,
// DataType = DbType.Decimal)] // note: DbType.Decimal is SQL type: money
public decimal? SampleDecimalValue { get; set; }
}
[TestMethod]
public void CreateAndDeleteEntityTest()
{
var sqlFactory = System.Data.SqlClient.SqlClientFactory.Instance;
var dbProvider = new DbDataProvider(sqlFactory, TestSqlConnectionString);
// create my poco
var poco = new SamplePoco { SampleStringValue = "abc" };
// wrap and auto-inspect my poco
var dew = new DataModel<SamplePoco>(poco); // data entity wrapper
// save my poco
dew.DataProvider = dbProvider;
dew.Save(); // auto-synchronizes ID
// or...
//dbProvider.SaveModel(dew);
//dew.SynchronizeFields(SyncTo.ClrMembers); // manually sync ID
// now let's load it and validate that it was saved
var mySampleQuery = DataModel<SamplePoco>.NewQuery()
.WhereProperty["ID"] == poco.ID; // poco.ID was inferred as IsIdentity so we auto-returned it on Save()
var data = dbProvider.LoadModel(mySampleQuery);
Assert.IsNotNull(data); // success!
// by the way, you can go back to the POCO type, too
SamplePoco poco2 = data.Entity; // no typecast nor "as" statement
Assert.IsNotNull(poco2);
Assert.IsTrue(poco2.ID > 0);
Assert.IsTrue(poco2.SampleStringValue == "abc");
// test passed, let's delete the test record
data.MarkDeleted = true;
data.Save();
// ... and make sure that it has been deleted
data = dbProvider.LoadModel(mySampleQuery);
Assert.IsNull(data);
}
And by the way, if you want your POCO objects to inherit DataModel instead of wrapping your POCO objects with DataModel<T>, that works too, but you'll need to always update your inner data dictionary on all of your property getters and setters. The downside of working this way is obvious: you have more code you have to maintain, and it gets more difficult to maintain changes to fields accurately. But the biggest advantage of working this way is performance. So if you use Gemli.Data with a code generator, this might work better for you.
Gemli.Data supports strongly typed collections and multiple records, too, of course.
var mySampleQuery = DataModel<SamplePoco>.NewQuery()
.WhereColumn["SampleStringValue"].IsLike("%bc");
var models = dbProvider.LoadModels(mySampleQuery);
SamplePoco theFirstSamplePocoEntity = models.Unwrap<SamplePoco>()[0];
// or.. SamplePoco theFirstSamplePocoEntity = models[0].Entity;
EditData Providers
All data that is synchronized to or from a database or other persistence mechanism works through
Gemli.Data.Providers.DataProviderBase, which is the abstract base class for implementing the required behavior for saving and loading data. Data does not have to be persisted to a database. It can be persisted in other ways as well. However, the database is the obvious choice for most typical application scenarios.
EditInitializing A Data Provider
EditGemli.Data.Providers.DbDataProvider
To work with a data provider such as SQL Server, you can start with
Gemli.Data.Provider.DbDataProvider which is a general-purpose, vendor-neutral database provider that constructs ANSI SQL statements and uses bi-directional parameters to perform CRUD operations for your data. You only need to construct it with the
System.Data.DbFactory instance provided by the database flavor of your choice. The
DbDataProvider class can also be inherited to override specific functions to better support a particular database vendor or platform.
Note: At the time this wiki document was created, the only database tested with DbDataProvider was SQL Server Express Edition with an ad hoc .mdf file reference.
To instantiate the data provider (EXAMPLE):
var sqlFactory = System.Data.SqlClient.SqlClientFactory.Instance;
var dbProvider = new Gemli.Data.Providers.DbDataProvider(sqlFactory, AppSqlConnectionString);
EditGemli.Data.Providers.MemoryDataProvider
The unit tests in the Gemli project source code began with
Gemli.Data.Providers.MemoryDataProvider before
DbDataProvider was introduced. It uses ADO.NET
DataTables to store data records in-memory.
DataTable supports subsets of SQL clauses to extract filtered data.
var memProvider = new MemoryDataProvider();
// add a table
var dt = new System.Data.DataTable("Company");
var companyIdCol = new DataColumn("company_id", typeof (int));
dt.Columns.Add(companyIdCol);
var companyNameCol = new DataColumn("company_name", typeof (string));
dt.Columns.Add(companyNameCol);
memProvider.AddTable(dt);
EditOne-Time Referencing Of The Initialized Provider
An application can have a data provider specified as the "default" data provider instance. This allows the application code to avoid having to manually retain a reference to the data provider instance, and objects that rely on a data provider can automatically assume to use this "default" provider instance.
var sqlFactory = System.Data.SqlClient.SqlClientFactory.Instance;
var dbProvider = new Gemli.Data.Providers.DbDataProvider(sqlFactory, AppSqlConnectionString);
// Assign as application default provider
Gemli.Data.Providers.ProviderDefaults.AppProvider = dbProvider;
EditDataModel and DataModel<T>
To store CLR objects to a database, it must either be converted to a DataModel (using
Gemli.Data.DataModel<T>) or else already be a
Gemli.Data.DataModel (using inheritance of
Gemli.Data.DataModel). There is no one-size-fit-all best practice as to which one to always use, it depends.
EditGemli.Data.DataModel (inheriting)
A CLR object that inherits
DataModel is optimized for performance. However, this comes with support overhead. You must manually populate the inner data dictionary.
// A basic DataModel class.
public class Customer : DataModel
{
[DataModelColumn("customer_id")]
public virtual int ID
{
get { return (int)base["ID"]; }
set { base["ID"] = value; }
}
[DataModelColumn("customer_name")]
public virtual string Name
{
get { return (string) base["Name"]; }
set { base["Name"] = value; }
}
}
As demonstrated, the getters and setters of each property manually retrieve from and update to the inner dictionary of
DataModel. This is necessary for tracking field changes (managing dirty state) as well as to give the data provider a single point of reference for retrieving and populating the data.
If you are already using a code generator, this might be the preferred way to manage data entities, although it does also expose extra data management related properties to the entity's public interface that might look "messy" to object consumers.
EditGemli.Data.DataModel<TEntity> (wrapping)
Gemli.Data.DataModel<TEntity> inherits
Gemli.Data.DataModel but adds a strong type reference to any CLR object that it wraps. Wrapping a CLR object allows the object to inherit its own base objects. Both
DataModel and
DataModel<TEntity> work with
Gemli.Data.DataModelMap which uses the
System.Reflection namespace to introspect type
TEntity (your business entity) using reflection. Much is inferred by default about a CLR type and how it would map to a database table, but you can manage these details using C# attributes with the class and property declarations.
IMPORTANT NOTE: If the members of a class are not attributed, inferences will be made on behalf of all of them, but if one or more members are attributed, inferences will not be made of non-attributed members. In other words, you must specify attributes on all of your database mapped properties (explicit) or else on none of them (inferred), you cannot have one property be inferred and one property be explicit.
You can override this by setting the PropertyLoadBehavior property in the [DataModelTable] class attribute to InferProperties.NotIgnored. This indicates that all properties of the class, whether attributed or not, should be picked up as a mapped column, unless [DataModelIgnore] is used on the property.
Inferences are also ONLY made on properties that would map to a database primitive value (int, nvarchar, money, tinyint, etc). Properties having complex types would require a foreign key relationship and as such must be mapped explicitly as a foreign key or as a ForeignDataModel.
public class Customer // : object, or your own base class
{
[DataModelColumn("customer_id")]
public virtual int ID { get; set; }
[DataModelColumn("customer_name")]
public virtual string Name { get; set ]
}
...
var customer = new Customer {Name = "Jon"};
DataModel customerDataModel = new DataModel<Customer>(customer);
EditSaving Data
A
DataModel must have change state (IsNew = true, IsDirty = true, or MarkDeleted = true) before it can be saved through a Gemli data povider. To save a
DataModel, you can do so in one of three ways:
- Execute its
.Save() method. If the DataModel instance was not already loaded from the database, you will need to first set its .DataProvider property. - Invoke the data provider's
.SaveModel(dataModel) method. - If the
DataModel is in a Gemli.Data.DataModelCollection, save the collection.
EditDataModel.Save()
The recommended way to save a
DataModel is to invoke its own
Save(..) method. Doing so requires: a) a default setting of the appication data provider, b) it was created from a prior
LoadDataModel(..) invocation, or c) it was created as a new record or in a custom load manner, and its
DataProvider property has been manually set.
Example:
var sqlFactory = System.Data.SqlClient.SqlClientFactory.Instance;
var dbProvider = new Gemli.Data.Providers.DbDataProvider(sqlFactory, AppSqlConnectionString);
Gemli.Data.Providers.ProviderDefaults.AppProvider = dbProvider;
var contact = new Contact();
contact.Name = "Bob Smith";
var contactModel = new DataModel<Contact>(myContactObject);
contactModel.Save();
Example 2:
var contact = new Contact();
contact.Name = "Bob Smith";
var contactModel = new DataModel<Contact>(myContactObject);
var sqlFactory = System.Data.SqlClient.SqlClientFactory.Instance;
var dbProvider = new Gemli.Data.Providers.DbDataProvider(sqlFactory, AppSqlConnectionString);
contactModel.DataProvider = dbProvider;
contactModel.Save();
Example 3:
var sqlFactory = System.Data.SqlClient.SqlClientFactory.Instance;
var dbProvider = new Gemli.Data.Providers.DbDataProvider(sqlFactory, AppSqlConnectionString);
var contactQuery = DataModel<Contact>.NewQuery()
.WhereProperty["Name"] == "Bob Smith";
var contactModel = dbProvider.LoadModel(contactQuery);
contactModel.Entity.Salary += 10000m; // give Bob a raise
contactModel.Save(); // contactModel already knows its DataProvider
EditDataProvider.SaveModel()
You can also invoke
SaveModel(..) on the DataProvider object. If you invoke
DataProvider.SaveModel(), you
should also invoke on the dataModel:
dataModel.SynchronizeFields(SyncTo.FieldMappedData);
before it is invoked, and
dataModel.SynchronizeFields(SyncTo.ClrMembers);
after, resulting in ..
dataModel.SynchronizeFields(SyncTo.FieldMappedData);
dataProvider.SaveModel(dataModel);
dataModel.SynchronizeFields(SyncTo.ClrMembers);
The reason for this is because when
DataModel.Save() is invoked, it executes these synchronization methods to ensure that if the
DataModel is actually a
DataModel<T> wrapper, the data in the CLR properties are propagated to the
DataModel's inner data dictionary so that the data provider can read it out, and then the second synchronization is to put any data returned from the database (such as the new identity column value in a new record) gets propagated back to the CLR object's properties.
This synchroniztion could be moved to the data provider, but it is not the responsibility of the data provider to ensure that the model and the model's referenced entity (if any) are syncroninzed. The role of the data provider is to synchronize between the
DataModel and the database itself.
EditLoading Data
Data can be loaded directly into a single
DataModel (including
DataModel<T>), or it can be loaded into a
Gemli.Data.DataModelCollection<T>.
In either case, you must use a
Gemli.Data.Query<T> object to perform such a load.
EditQueries
To create a
Query<T> object, you can invoke the static
.NewQuery() method on the
DataModel<T> type.
var customerQuery = DataModel<Customer>.NewQuery();
EditQuery Conditions
Queries support a chain syntax to define multiple conditions without terminating the statement. The conditions are field value boolean evaluations that use the data mappings to create a standard WHERE clause in SQL (assuming that the data provider is a SQL-compatible database).
var customerQuery = DataModel<Customer>.NewQuery()
.WhereProperty["salary"].IsGreaterThan(60000m)
.WhereProperty["department"].IsEqualTo("Sales");If you only have a single condition, you can use a binary comparison operator instead of a comparison method, i.e. instead of using
.IsEqualTo(value) you can use
== value. It will still return the query object (not true/false).
Note: At the time of this wiki page's entry, nested clauses and AND vs OR are only partially implemented and not fully supported yet.
Note: At the time of this wiki page's entry, more advanced SQL query features such as aggregate functions (SUM) or GROUP BY clauses are not supported yet.
Note: DataModels can be mapped to SQL stored procedures instead of generated ad hoc SQL. In this scenario, there are strict limitations on query conditions, and generally in such case only .IsEqualTo() is allowed, as this would be propagated as a procedure parameter and parameter value.
EditRequesting Data
EditSingle Record
Once a query is created, you can load the model from one of four interfaces:
- From a
DataModelQuery<TModel> object you can invoke SelectFirst(). - From the
DataModel/DataModel<T> classes you can statically load a single record with a DataModelQuery<T> using the static .Load() method. - From a data provider, you can invoke
LoadModel(). - From a
DataModel/DataModel<T> object you can load a single record as an instance of the DataModel/DataModel<T> using .Load(), passing in an ADO.NET DataRow or IDataReader.
var sqlFactory = SqlClientFactory.Instance;
var dbProvider = new DbDataProvider(sqlFactory, TestSqlConnection);
var myCustomerQuery = DataModel<Customer>.NewQuery()
.WhereColumn["customer_id"].IsEqualTo(153);
DataModel<Customer> customer = myCustomerQuery.SelectFirst();
var sqlFactory = SqlClientFactory.Instance;
var dbProvider = new DbDataProvider(sqlFactory, TestSqlConnection);
var myCustomerQuery = DataModel<Customer>.NewQuery()
.WhereColumn["customer_id"].IsEqualTo(153);
DataModel<Customer> customer = DataModel<Customer>.Load(myCustomerQuery);
var sqlFactory = SqlClientFactory.Instance;
var dbProvider = new DbDataProvider(sqlFactory, TestSqlConnection);
var myCustomerQuery = DataModel<Customer>.NewQuery()
.WhereColumn["customer_id"].IsEqualTo(153);
DataModel<Customer> customer = dbProvider.LoadModel(myCustomerQuery);
EditMultiple Records
You can also load multiple records as models into a
DataModelCollection<T> using one of three interfaces:
- From a
DataModelQuery<TModel> object you can invoke SelectFirst() or SelectMany(). - From the
DataModel/DataModel<T> class's LoadMany() method. - From the data provider's
LoadModels() (plural) method.
var customersQuery = DataModel<Customer>.NewQuery()
.WhereProperty["IsActive"].IsEqualTo(true);
var customers = customersQuery.SelectMany();
var customersQuery = DataModel<Customer>.NewQuery()
.WhereProperty["IsActive"].IsEqualTo(true);
var customers = DataModel<Customer>.LoadMany(customersQuery);
var customersQuery = DataModel<Customer>.NewQuery()
.WhereProperty["IsActive"].IsEqualTo(true);
var customers = dbProvider.LoadModels(customersQuery);This returns a collection object that also has
Save() and
SyncronizeFields() methods for performing these tasks on behalf of all of the items.
EditTransactions
All of the
Save() and
Load() related methods on
DataModel and
DataProviderBase have parameter overloads for accepting a
System.Data.DbTransaction context.
DbDataProvider implements this parameter and passes it on to the ADO.NET
DbCommand object that gets generated by the
DbFactory.
EditCreating A Transaction
If your data provider's
SupportsTransactions property returns true, you can create a new transaction using the data provider's
BeginTransaction or
BeginTransaction(IsolationLevel) method.
EditRolling Back
The DbTransaction object should be able to perform a database-level rollback of changes made within a transaction context.
There is no implicit rollback support on the entities and data models if a transaction has been rolled back. Should this situation occur, you should treat the entities/models on which changes were made as stale data records that must be destroyed and/or reloaded.
However,
DataModel has a
Reset(ResetMode) method that effectively implements "Undo" functionality.
Rollback support can be otherwise easily applied to the DataModel manually, since the DataModel objects are aware of both the
OriginalData and
ModifiedProperties of the data passed in for a modified record, and their
IsNew and
MarkDeleted states. So, in the event of a rollback, one can examine these properties and implement their own "undo" functionality since all the data is available to do so.
EditLoading and Saving With Stored Procedures
The reality of many teams is that table-reading/writing ad hoc SQL, including generated ad hoc SQL, is not allowed to touch a database, and all CRUD (Create, Read, Update, and Delete) operations must execute through stored procedures. Gemli.Data supports routing all CRUD operations through stored procedures. This comes with a few caveats, most notably the DataModelQuery will be limited to IsEqualTo() (or the '==' operator) comparisons which would become mapped as stored procedure parameters. For example, .WhereColumn["ID"] == 23 would be mapped as parameter @ID=23.
Because deep loads and deep saves are implemented with client-side join behavior by default, stored procedure based CRUD operations work the same with relationship-constrained data as generated ad hoc SQL—one record operation at a time.
To assign stored procedures to
DataModels' CRUD operations, specify the
InsertProcedure,
SelectProcedure,
SelectManyProcedure,
UpdateProcedure, and/or
DeleteProcedure propert(ies) of the
[DataModelTable] attribute or TableMapping of the DataModelMap for your entity or
DataModel. These properties should be the same names as the stored procedures that should execute the operation.
The parameters of each CRUD operation will be inferred by the
ColumnName properties of the model's mappings; however, the parametrization behavior can be modified on the
[DataModelColumn] attribute or ColumnMapping of the DataModelMap.
- ReturnOnInsert - If set to true, this field is included in INSERTs to the database as a return value. This property defaults to the value of the property IsIdentity. Note that setting this to true forces the IncludeOnInsert value to be false.
- IncludeOnInsert - If set to false, indicates that this field is excluded from the parameters list. This property defaults to the opposite value of the property IsIdentity. Note that setting this to true forces the ReturnOnInsert value to be false.
EditRelationships (Deep Loads and Deep Saves)
Note: At the time of this wiki page's entry, deep loads and deep saves are joined on the client side. That is, this feature is not optimized. Optimizing the deep loads and saves is on the to-do list but will require vendor-specific implementations of the data provider.
Gemli.Data supports n-level or infinite-depth deep loads and deep saves. The client-side management of this functionality is in
Gemli.Data.DataProviderBase, so the only requirement of an implementation of a data provider is to load/save one record or to load/save multiple records from one table for one
DataModel.
EditExecuting Deep Loads and Deep Saves
You can always lazy-load a model using manual loading of each nested entity. However, the deep loading feature allows you to perform a deep load with a single invocation.
EditDataProviderBase.DeepLoadModel()
The
DataProviderBase class, which is abstract and is implemented by, for example,
DbDataProvider, implements the method
DeepLoadModel(...). This method is the same as
LoadModel(...) but performs a deep load instead of a shallow load of a
DataModel.
n-Level Depth
The method is also overloaded with a
depth parameter which specifies how many levels deep to load. If this value is
null or not provided, an infinite-depth load will be used (not literally, of course, but recursion without limits will be used).
Recursion Upon Same Records
The
DataProviderBase class's recursive deep loader is able to automatically identify previously loaded
TModels deriving from equal primary keys, and will not loop infinitely into a stack overflow. The first loaded
TModel from the same recursive
DeepLoad invocation will be referenced instead and recursion in that object is cancelled.
EditDataProviderBase.DeepLoadModels()
You can also perform a deep-load of many results from a single query. The
DeepLoadModels(...) method is the same as
DeepLoadModel(...) but returns a
DataModelCollection<TModel>. This method supports
n-level depth.
EditDataProviderBase.DeepSaveModel()
The
DataProviderBase class also has a
DeepSaveModel(...) method which performs similarly to the
DeepLoadModel(...) method but performs a deep
save of an existing
DataModel rather than a deep load of a new
DataModel. This method supports
n-level depth.
EditDataProviderBase.DeepSaveModels()
Likewise, he
DataProviderBase class also has a
DeepSaveModels(...) method which performs similarly to the
DeepSaveModel(...) method but performs the deep save on behalf of a
DataModelCollection<TModel> instead of a single
DataModel. This method supports
n-level depth.
EditDataModel.Save(deep, ...)
When saving a
DataModel with its
Save(..) method, you can also pass a boolean switch as a parameter to indicate that you want to perform a deep save. This method
does not currently support
n-level depth.
EditDeclaring Relationships
There are two ways to create a relationship between two entities with Gemli.Data: declaritive models (
ForeignDataModelAttribute) and decarative keys (
ForeignKeyAttribute).
EditForeignDataModelAttribute
The recommended approach to creating a foreign key reference is by using the foreign entity's target CLR type as a property and marking it with a
ForeignDataModel attribute.
Example:
[DataModel("customer")]
public class Customer
{
[DataModelColumn("customer_id")]
public int ID { get; set; }
[ForeignDataModel(
ColumnName = "billing_address_id",
ForeignColumnName = "address_id",
Relationship = Relationship.OneToOne]
public Address MailingAddress { get; set; }
}In this example, Gemli.Data is instructed to load/save the MailingAddress property as an Address entity. The Address entity's class will have its own mapping metadata, but the
ColumnName and
ForeignColumnName indicates explicitly how the JOIN should be made.
EditForeignKeyAttribute
An alternative way to create a foreign referece is by declaring the foreign key property explicitly and using the
[ForeignKey] attribute. This attribute allows you to specify the target property that should be populated in a deep load and saved with a deep save, using its
AssignToMember property.
Example:
[DataModel("customer")]
public class Customer
{
[DataModelColumn("customer_id")]
public int ID { get; set; }
public Address MailingAddress { get; set; }
[ForeignKey(
Relationship = Relationship.OneToOne,
RelatesTo = typeof(Address),
ForeignColumn = "address_id",
AssignToMember = "MailingAddress"
)]
public int MailingAdressID { get; set; }
}Note: The ForeignDataModelAttribute approach uses ForeignKeyAttribute under the covers to do its work.
EditMany-To-Many Scenarios
Gemli.Data uses a mapping table inference for many-to-many scenarios. Under the covers, when performing a deep load / deep save, Gemli.Data will first join on the inferred mapping table before it joins on the target related table. This type of relationship is only supported by the
ForeignDataModel attribute on a collection typed property of a class.
Example (T-SQL):
-- A simple User table, doesn't know Groups
CREATE TABLE [dbo].[User](
[user_id] [uniqueidentifier] NOT NULL,
[user_name] [varchar](250) NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
[user_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
-- A simple Group table, doesn't know Users
CREATE TABLE [dbo].[Group](
[group_id] [uniqueidentifier] NOT NULL,
[group_name] [varchar](250) NULL,
CONSTRAINT [PK_Group] PRIMARY KEY CLUSTERED
(
[group_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
-- A Group-User mapping table, allows a user to have many groups,
-- allows a group to have many users.
CREATE TABLE [dbo].[GroupUser](
[groupuser_id] [bigint] IDENTITY(1,1) NOT NULL,
[user_id] [uniqueidentifier] NOT NULL,
[group_id] [uniqueidentifier] NOT NULL,
CONSTRAINT [PK_GroupUser] PRIMARY KEY CLUSTERED
(
[groupuser_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[GroupUser] WITH CHECK ADD CONSTRAINT [FK_GroupUser_Group] FOREIGN KEY([group_id])
REFERENCES [dbo].[Group] ([group_id])
GO
ALTER TABLE [dbo].[GroupUser] CHECK CONSTRAINT [FK_GroupUser_Group]
GO
ALTER TABLE [dbo].[GroupUser] WITH CHECK ADD CONSTRAINT [FK_GroupUser_User] FOREIGN KEY([user_id])
REFERENCES [dbo].[User] ([user_id])
GO
ALTER TABLE [dbo].[GroupUser] CHECK CONSTRAINT [FK_GroupUser_User]
GO
Example (C#):
public class Group
{
[DataModelColumn("group_id", IsPrimaryKey = true)]
public Guid ID { get; set; }
[DataModelColumn("group_name")]
public string Name { get; set; }
[ForeignDataModel(Relationship = Relationship.ManyToMany,
MappingTable = "GroupUser")]
public List<User> Users { get; set; }
}
public class User
{
[DataModelColumn("user_id", IsPrimaryKey = true)]
public Guid ID { get; set; }
[DataModelColumn("user_name")]
public string Name { get; set; }
[ForeignDataModel(Relationship = Relationship.ManyToMany,
MappingTable = "GroupUser")]
public List<Group> Groups { get; set; }
}
As illustrated in the above example, the mapping table is handled automatically. Its name is declared by the
MappingTable property of the
ForeignDataModel attribute.
EditLoading Mappings From XML
DataModel mappings can be loaded from XML files. To load an XML mapping, execute
DataModelMap.LoadMappings(..) with the file path to your XML file as the parameter.
The following is a sample XML mapping file:
<?xml version="1.0"?>
<mappings>
<mappingItem>
<class>Tests.Gemli.Data.MemoryDataProviderTest+MockPoco, Tests.Gemli.Data</class>
<dataModel>
<entityType>Tests.Gemli.Data.MemoryDataProviderTest+MockPoco</entityType>
<tableMapping>
<table>mock_table</table>
<schema>dbo</schema>
</tableMapping>
<columnMappings>
<column>
<!--
The ownerType element should be the same as the class element above;
unfortunately this is not optional in the current build.
-->
<ownerType>Tests.Gemli.Data.MemoryDataProviderTest+MockPoco</ownerType>
<memberName>ID</memberName>
<columnName>customentity_id</columnName>
<sqlType>Int</sqlType>
<isNullable>false</isNullable>
<isPrimaryKey>true</isPrimaryKey>
<isIdentity>true</isIdentity>
<defaultValue>0</defaultValue>
</column>
<column>
<ownerType>Tests.Gemli.Data.MemoryDataProviderTest+MockPoco</ownerType>
<memberName>SomeValue</memberName>
<columnName>some_value</columnName>
<sqlType>NVarChar</sqlType>
<isNullable>true</isNullable>
<isPrimaryKey>false</isPrimaryKey>
<isIdentity>false</isIdentity>
</column>
</columnMappings>
</dataModel>
</mappingItem>
</mappings>
EditCustomizing Mappings At Runtime
You can always modify the mappings at runtime. The key interface is
DataModelMap and its static method
DataModelMap.GetEntityMapping(typeof(YOUR_TYPE)) which returns a
DataModelMap instance that defines the mapping details for the type
YOUR_TYPE.
DataModelMap mapping = DataModelMap.GetEntityMapping(typeof(YOUR_TYPE));
EditTable Mapping
The
TableMapping property of
DataModelMap is a
DataModelTableAttribute object, which is the same attribute type that decorates a class declaration. Most of the properties of this object are modifiable at runtime. This is documented here:
DataModelTableAttributeEditColumn Mappings
The
ColumnMappings property of
DataModelMap is a case-insensitive dictionary of
DataModelColumnAttribute objects, which are the same attribute type that decorate a class's properties as column mappings. Most of the properties of this object are modifiable at runtime. This is documented here:
DataModelColumnAttributeEditForeign Mappings
The
ForeignModelMappings property of
DataModelMap is a case-insensitive dictionary of
ForeignDataModelAttribute objects, which are the same attribute type that decorate a class's property as a deep loadable complex type. Most of the properties of this object are modifiable at runtime. This is documented here:
ForeignDataModelAttributeEditIterating And Modifying The DataModelMap Items
You can iterate through or even completely reset a DataModelMap at runtime by accessing the
DataModelMap.MapItems dictionary, which is keyed by the
Type hashcode.
var oldMapping = DataModelMap.GetEntityMapping(typeof(MyClass));
var newMapping = new DataModelMap(typeof(MyClass));
newMapping.TableMapping.Table = "MyOtherTable";
DataModelMap.MapItems[typeof(MyClass)] = newMapping;