The Gemli Project - Wiki

Gemli.Data

Modified: 2009/09/26 18:00 by Stimpy77 - Categorized as: Gemli.Data
Gemli.Data


Gemli.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;

Edit

Data 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.

Edit

Initializing A Data Provider

Edit

Gemli.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);

Edit

Gemli.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);

Edit

One-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;
Edit

DataModel 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.

Edit

Gemli.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.

Edit

Gemli.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);

Edit

Saving 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.

Edit

DataModel.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

Edit

DataProvider.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.

Edit

Loading 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.

Edit

Queries

To create a Query<T> object, you can invoke the static .NewQuery() method on the DataModel<T> type.

var customerQuery = DataModel<Customer>.NewQuery();

Edit

Query 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.

Edit

Requesting Data

Edit

Single 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);

Edit

Multiple 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.

Edit

Transactions

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.

Edit

Creating 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.

Edit

Rolling 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.

Edit

Loading 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.

Edit

Relationships (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.

Edit

Executing 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.

Edit

DataProviderBase.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.

Edit

DataProviderBase.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.

Edit

DataProviderBase.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.

Edit

DataProviderBase.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.

Edit

DataModel.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.

Edit

Declaring Relationships

There are two ways to create a relationship between two entities with Gemli.Data: declaritive models (ForeignDataModelAttribute) and decarative keys (ForeignKeyAttribute).

Edit

ForeignDataModelAttribute

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.

Edit

ForeignKeyAttribute

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.

Edit

Many-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.

Edit

Loading 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>

Edit

Customizing 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));

Edit

Table 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: DataModelTableAttribute

Edit

Column 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: DataModelColumnAttribute

Edit

Foreign 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: ForeignDataModelAttribute

Edit

Iterating 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;

ScrewTurn Wiki version 2.0.36. Some of the icons created by FamFamFam.