As you already know DataObjects.Net automatically generates database schema when you build a domain. You don’t need to create tables and columns manually, everything you need to do is just to create empty database and specify appropriate connection string in domain configuration.
Let us suppose we have created our model consisting of one class:
[HierarchyRoot]
public class Order : Entity
{
[Key, Field]
public int Id { get; private set;}
[Field]
public string ProductName { get; set; }
[Field]
public int Quantity { get; set; }
}
Then build domain:
var domainConfig = new DomainConfiguration("sqlserver://localhost/myDatabase");
domainConfig.Types.Register(myAssembly, "MyProduct.Model");
domainConfig.UpgradeMode = DomainUpgradeMode.Recreate;
var domain = Domain.Build(domainConfig);
When domain is built, database structure will look like this:
You can see that there is Order table corresponding to Order entity in our model and several additional tables.
You can also notice that in previous example we specified DomainUpgradeMode.Recreate as upgrade mode in domain configuration, it means that existing database schema and data should be completely cleared before new schema is generated. This mode is very useful when you want to create new database or perform unit tests, but when our application is in use we need to upgrade database schema from one version to another without loosing existing data, moreover sometimes existing data should be also modified, e.g. we may want to change some column type or move some data from one class to another.
There are several available values of DomainUpgradeMode :
In order to upgrade exiting database schema and save stored data we can use Perform or PerformSafely mode. In simple cases such upgrade can be performed fully automatically.
For example let’s create several orders and then change our model: Add Customer class and Customer property to Order class:
[HierarchyRoot]
public class Order : Entity
{
// ...
[Field]
public Customer Customer { get; set;}
}
[HierarchyRoot]
public class Customer : Entity
{
[Key, Field]
public int Id { get; private set; }
[Field]
public string Name { get; set; }
}
If we build our domain with Validate mode, we’ll get SchemaSynchronizationException in Build() method. If we build it with Recreate mode, it will create new database schema, but data will be lost. When we have our domain built with Perform or PerformSafely upgrade mode, we’ll see following database structure:
DataObjects.Net automatically added Customer table and Customer.Id column to Order table. New table Customer is empty and value of new field is null, but all existing orders are saved.
Automatic upgrade works perfectly when we add new classes and properties to our model, but sometimes we need to perform some specific schema modifications, for example we can rename some properties or classes.
Let’s rename Customer class to Person and, for example, Name property to FullName.
[HierarchyRoot]
public class Order : Entity
{
// ...
[Field]
public Person Customer { get; set;}
}
[HierarchyRoot]
public class Person : Entity
{
[Key, Field]
public int Id { get; private set; }
[Field]
public string FullName { get; set; }
}
To upgrade schema correctly, we should inform domain that Person class is renamed Customer, otherwise it will remove Customer table with all records and create new empty Person table. To do this we use so called upgrade handlers and upgrade hints.
Upgrade handler is class responsible for upgrade of persistent classes from one assembly. It must be inherited from UpgradeHandler or implement IUpgradeHandler interface, it also must have a parameterless constructor. Each assembly with persistent model should contain exactly one upgrade handler class, it will be automatically found when schema is being upgraded. UpgradeHandler class contains a set of methods and properties that can be overridden for customizing schema upgrade process.
public class Upgrader : UpgradeHandler
{
protected override void AddUpgradeHints(Xtensive.Collections.ISet<UpgradeHint> hints)
{
hints.Add(new RenameTypeHint("MyProduct.Model.Customer", typeof (Person)));
hints.Add(new RenameFieldHint(typeof (Person), "Name", "FullName"));
}
}
In our example we have overriden AddUpgradeHints method to add two hints. If we build domain with new model it will find our upgrade handler and will rename columns and tables instead of removing and creating new ones, so their data will be saved.
In order to understand meaning of remove hints let’s remember difference between Perform and PerformSafely upgrade modes. Second one requires all modifications leading to losing data to be explicitly permitted. For example, if we remove some persistent field or persistent class from our model, domain builder will remove appropriate database structure in Perform mode and will throw exception in PerformSafely mode. To perform such operations in safe mode we should use following hints:
For example, if you want to remove Quantity field from Order in PerformSafely mode, you would remove appropriate property from Order class and add RemoveFieldHint to hints collection in AddUpgradeHints method.
ChangeFieldTypeHint is used when you want to change type of some persistent column. For example you have Order class with Number property of int type, but new version of application should allow to store numbers with literal prefix, and we definitely want to change column type to string and convert all stored numbers. To do this we change type of property to string:
[Field(Length = 20)]
public string Number { get; set; }
And add ChangeFieldTypeHint in our custom upgrade handler:
hintSet.Add(new ChangeFieldTypeHint(typeof(Order), "Number"));
If hint is not added domain builder will create new column without saving data. In safe mode an exception will be thrown.
You should also use ChangeFieldTypeHint hint if you use PerformSafely mode change length of string field or precision of decimal field.
When column type is changed domain builder uses standard functions of RDBMS to convert values. In our example we changed int type to string and values were successfully converted. But if types are incompatible in particular RDBMS, we’ll get appropriate exception.
Imagine we have Order type inherited from Document, Order class has Number property. We want number to be a property of Document class. We can declare such property in Document and remove it from Order, but we also have to copy data from Order table to Document table before order’s column will be removed.
To do this we can use MoveFieldHint, that moves some field data to another class within single hierarchy:
hintSet.Add(new MoveFieldHint("MyProduct.Model.Order", "Number", typeof(Document)));
Whereas MoveFieldHint implies that original field is removed, CopyFieldHint don’t, it just copies. To implement our upgrade handler using CopyFieldHint we should add RemoveFieldHint.
hintSet.Add(new CopyFieldHint("Order", "Number", typeof(Order)));
hintSet.Add(new RemoveFieldHint("Order", "Number"));
There are some cases where automatic upgrade with upgrade hints is not enough to describe changes in our model. For example suppose we decided to extract product information from Order class to separate Product class. In original model we have Order.ProductName property of string type. In result model we have Order.Product property of Product type, which has Name property.
So we should enumerate all orders during upgrade process, create new product for each unique product name, and assign newly created products to Product property of each order. We can not do this when domain already built because column ProductName should be removed from Order table as a result of schema upgrade. Since it is rather complex operation, we will perform it manually.
First of all we should make necessary changes to our model – add new persistent class and persistent field. Then we mark old field ProductName as recycled, it is also recommended to mark it as obsolete:
[HierarchyRoot]
public class Order : Entity
{
// ...
[Field]
public Product Product { get; set; }
[Field, Obsolete, Recycled]
public string ProductName { get; set; }
}
[HierarchyRoot]
public class Product : Entity
{
[Key, Field]
public int Id { get; private set; }
[Field]
public string Name { get; set; }
}
Next thing we should do is to implement our custom upgrade handler for this upgrade:
public class Upgrader : UpgradeHandler
{
public override void OnUpgrade()
{
var session = Session.Demand();
foreach (var order in session.Query.All<Order>()) {
var product = session.Query.All<Product>()
.SingleOrDefault(p => p.Name==order.ProductName);
if (product==null)
product = new Product { Name = order.ProductName };
order.Product = product;
}
}
}
As you can see, we have overridden OnUpgrade() method of upgrade handler and placed our custom logic there. We manually query the orders and create new products. When this method is executed, both old and new fields are accessible. But when domain is already built, you can’t use recycled fields; moreover, their underlying tables and columns are removed at that moment.
In our example we used recycled fields, but it is also possible to use recycled persistent classes in exactly the same way. You need just mark types to be removed by [Recycled] attribute and recycle their data in OnUpgrade() method.
DataObjects.Net can be used for mapping of entities to existing database. There are two special upgrade modes for this:
Currently legacy mode has a set of limitations:
Rules to follow:
Here is an example of Legacy model mapping. Note that we are mapping 3 classes to the single table “Animals” and use ElementType property as type discriminator.
[HierarchyRoot(InheritanceSchema = InheritanceSchema.SingleTable)]
[TypeDiscriminatorValue("Animal", Default = true)]
[TableMapping("Animals")]
public class Animal : Entity
{
[Field, Key]
public Guid Id { get; private set; }
[Field(Length = 50), TypeDiscriminator]
[FieldMapping("Type")]
public string ElementType { get; private set; }
[Field(Length = 50)]
public string Name { get; set; }
[Field]
public int Age { get; set; }
[Field]
[FieldMapping("Owner")]
public Person Owner { get; set; }
}
[TypeDiscriminatorValue("Dog")]
public class Dog : Animal
{
}
[TypeDiscriminatorValue("Cat")]
public class Cat : Animal
{
}