First of all, you must have in mind that ActiveRecord uses [NHibernate], so it's a good idea to at least have a look at how NHibernate works. Their documentation
is very good, and the mappings section
is something that you must read.
Mappings
In this section we're going to talk more about how to perform proper mappings. However we won't talk about every single property and enum value. Please check the API section for further detailed information.
Simple mappings
Class
Every ActiveRecord class must extends ActiveRecordBase and must use the ActiveRecordAttribute.
[ActiveRecord("blogtable")] public class Blog : ActiveRecordBase { }
You can omit the table name if the class name has the same name as the table.
[ActiveRecord()]
public class Blog : ActiveRecordBase
{
}
Primary key
[ActiveRecord()] public class Blog : ActiveRecordBase { private int id; [PrimaryKey] public int Id { get { return id; } set { id = value; } } }
Which is equivalent to use:
[PrimaryKey(PrimaryKeyType.Native)] public int Id { get { return id; } set { id = value; } }
You can use sequences as well:
[PrimaryKey(PrimaryKeyType.Sequence, SequenceName="myseqname")] public int Id { get { return id; } set { id = value; } }
Composite Keys
To use composite keys, all the work is in the composite key itself. Just use a PrimaryKey attribute as you normally would, with the type of the property being your composite key class.
[PrimaryKey] public MyCompositeKey ID { get { return _key; } set { _key = value; } }
... and the composite key class ...
[CompositeKey, Serializable] public class MyCompositeKey { private string _keyA; private string _keyB; [KeyProperty] public virtual string KeyA { get { return _keyA; } set { _keyA = value; } } [KeyProperty] public virtual string KeyB { get { return _keyB; } set { _keyB = value; } } public override string ToString() { return string.Join( ":", new string[] { _keyA, _keyB } ); } public override bool Equals( object obj ) { if( obj == this ) return true; if( obj == null || obj.GetType() != this.GetType() ) return false; MyCompositeKey test = ( MyCompositeKey ) obj; return ( _keyA == test.KeyA || (_keyA != null && _keyA.Equals( test.KeyA ) ) ) && ( _keyB == test.KeyB || ( _keyB != null && _keyB.Equals( test.KeyB ) ) ); } public override int GetHashCode() { return _keyA.GetHashCode() ^ _keyB.GetHashCode(); } }
A composite key class must be Serializable, and also implement both Equals and GetHashCode. The class must also have two or more properties marked with the KeyProperty attribute.
Properties
To map an ordinary property where its name is the same as the column, use:
[Property] public String Name { get { return name; } set { name = value; } }
Or you can specify the column name:
[Property("customer_name")] public String Name { get { return name; } set { name = value; } }
You can also set more detailed information:
[Property(Length=10, NotNull=true, Unique=true)] public String Name { get { return name; } set { name = value; } }
Fields
You can also map a field directly, using the [Field] Attribute:
[Field] private String _name;
Like with a property, you can specify the column name:
[Field("customer_name")] private String _name;
You can specify detailed information on a Field the same way you do it on a property. The field can be of any visibility.
Using is a field is usually recommended when the field data is an internal class implementation (type of a strategy to use in a certain case, which is never exposed to the clients of the class, etc). In general, it's preferable to use properties.
Nested
You can use an aggregate class to map data on the same table. For example:
[ActiveRecord] public class Company : ActiveRecordBase { private PostalAddress _address; [Nested] public PostalAddress Address { get { return _address; } set { _address = value; } } } public class PostalAddress { private String _address; private String _city; private String _state; private String _zipcode; public PostalAddress() { } public PostalAddress(String address, String city, String state, String zipcode) { _address = address; _city = city; _state = state; _zipcode = zipcode; } [Property] public String Address { get { return _address; } set { _address = value; } } [Property] public String City { get { return _city; } set { _city = value;} } [Property] public String State { get { return _state; } set { _state = value; } } [Property] public String ZipCode { get { return _zipcode; } set { _zipcode = value; } } }
Version/Timestamp
The NHibernate's Version and Timestamp feature can be used on ActiveRecord as well:
[Version("customer_version")] public Int32 Version { get { return version; } set { version = value; } }
And
[Timestamp("customer_timestamp")] public Int32 Timestamp { get { return ts; } set { ts = value; } }
BelongsTo
The BelongsTo maps a many to one association, like Post belongs to Blog:
[ActiveRecord] public class Post : ActiveRecordBase { private Blog blog; [BelongsTo("post_blogid")] public Blog Blog { get { return blog; } set { blog = value; } } }
You can also specify the Cascade behavior, enable insert and update and so forth:
[BelongsTo("post_blog_id", Cascade=CascadeEnum.All, Unique=true)] public Blog Blog { get { return blog; } set { blog = value; } }
HasMany
The HasMany maps an one to many association, like Blog has many Post.
[ActiveRecord("Blogs")] public class Blog : ActiveRecordBase { private IList _posts; [HasMany(typeof(Post), Table="posts", ColumnKey="post_blogid")] public IList Posts { get { return _posts; } set { _posts = value; } } }
| You can be explicit on the mapping information or you can omit it if the target class has a BelongsTo mapping back to the source class. |
You can also specify the cascade behavior, order by, where, turn on the lazy. Also you can use IList or ISet to hold the collection items. If you need to delete child objects when deleting a parent (e.g. when calling Blog.Delete() should also delete all related Post records from the database), you can specify Inverse=true. See NHibernate's Parent/Child documentation
for more examples.
HasAndBelongsToMany
The HasAndBelongsToMany maps a many to many association using an separated association table.
Consider the following tables:
CREATE TABLE [dbo].[companies] (
[id] [int] IDENTITY (1, 1) NOT NULL ,
[client_of] [int] NULL ,
[name] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[type] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
CREATE TABLE [dbo].[people] (
[id] [int] IDENTITY (1, 1) NOT NULL ,
[name] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
CREATE TABLE [dbo].[people_companies] (
[person_id] [int] NOT NULL ,
[company_id] [int] NOT NULL
) ON [PRIMARY]
As you see a company has many people and a "person" has many companies. This kind of mapping can be achieved using the attribute HasAndBelongsToMany:
[ActiveRecord("companies")] public class Company : ActiveRecordBase { private int id; private String name; private IList _people; public Company() { } public Company(string name) { this.name = name; } [PrimaryKey] public int Id { get { return id; } set { id = value; } } [Property] public String Name { get { return name; } set { name = value; } } [HasAndBelongsToMany( typeof(Person), Table="people_companies", ColumnRef="person_id", ColumnKey="company_id" )] public IList People { get { return _people; } set { _people = value; } } } [ActiveRecord("people")] public class Person : ActiveRecordBase { private int _id; private String _name; private IList _companies; public Person() { _companies = new ArrayList(); } [PrimaryKey] public int Id { get { return _id; } set { _id = value; } } [Property] public string Name { get { return _name; } set { _name = value; } } [HasAndBelongsToMany( typeof(Company), Table="people_companies", ColumnRef="company_id", ColumnKey="person_id" )] public IList Companies { get { return _companies; } set { _companies = value; } } }
| You must specify the association table and the ref and key columns. |
OneToOne
A One to one associates a foreign table where the current class and the target class share their primary key.
This is usefull when you are using Class Table Inheritance![]()
Glitches
The primary key of the table which isn't autogenerated must be declared using PrimaryKeyType.Foreign:
[ActiveRecord("Customer")] public class Customer : ActiveRecordBase { int _custID; string name; CustomerAddress _addr; [PrimaryKey] public int CustomerID { get { return _custID; } set { _custID = value; } } [OneToOne] public CustomerAddress CustomerAddress { get { return _addr; } set { _addr = value; } } [Property] public string Name { get { return _name; } set { _name = value; } } } [ActiveRecord("CustomerAddress")] public class CustomerAddress : ActiveRecordBase { int _custID; string address; Customer _cust; [PrimaryKey(PrimaryKeyType.Foreign)] public int CustomerID { get { return _custID; } set { _custID = value; } } [OneToOne] public Customer Customer { get { return _cust; } set { _cust = value; } } [Property] public string Address { get { return _addr; } set { _addr = value; } } }
Any
There are certain cases when you need to make an association from an entity to a range of possible objects that doesn't necessarily share a common base class.
| This is a fairly advanced scenario, if you can, find a simpler solution. |
A simple example may be a payment method in an Order class, where the choices are either a bank account or a credit card, like this:
[ActiveRecord("CreditCards")] public class CreditCard : ActiveRecordBase, IPaymentMethod { ... } [ActiveRecord("BankAccounts")] public class BankAccount : ActiveRecordBase, IPaymentMethod { ... }
As you can see, when I try to write my Order class, I do not know how to map those two options. They are not part of any hierarchy (either in the object model or an ActiveRecord one).
The solution is to map them to this schema:
CREATEA TABLE Orders ( Id int not null identity(1,1), ... Billing_Details_Id int not null, Billing_Details_Type nvarchar not null, )
Together, BillingDetailsId and BillingDetailsType points to the correct account or credit card that should pay for the order. Here is the attributes declarations.
| Unlike most other attributes, here you need to specify a few attributes. |
[Any(typeof(int), MetaType=typeof(string), TypeColumn="Billing_Details_Type", IdColumn="Billing_Details_Id", Cascade=CascadeEnum.SaveUpdate)] [Any.MetaValue("CREDIT_CARD", typeof(CreditCard))] [Any.MetaValue("BANK_ACCOUNT", typeof(BankAccount))] public IPaymentMethod PaymentMethod { get { ... } set { ... } }
The first parameter is the type of the Id column (in this case "BillingDetailsId"), the second is the MetaType definition, which in this case mean the type of the the field that defines the type of the id.
Next we have the TypeColumn and IdColumn, which match Billing_Details_Type and Billing_Details_Id.
The interesting part is the Any.MetaValue() attribute. Here, we define that when the value in the Billing_Details_Type column is "CREDIT_CARD", the value in the Billing_Details_Id column is the primary key of a CreditCard, and when the Billing_Details_Type is BANK_ACCOUNT, then the value in Billing_Details_Id should be interpreted as the primary key of a BankAccount class.
| The type of the property should be of a common type or interface that all the possible objects share (worst case is to make it of type System.Object). |
HasManyToAny
A natural extention of Any, the HasManyToAny provides the same functionality for collections. Here is an example of a class that needs a set of payment methods:
[HasManyToAny(typeof (IPayment), "pay_id", "payments_table", typeof (int), "Billing_Details_Type", "Billing_Details_Id", MetaType=typeof (string))] [Any.MetaValue("CREDIT_CARD", typeof (CreditCard))] [Any.MetaValue("BANK_ACCOUNT", typeof (BankAccount))] public ISet PaymentMethod { get { ... } set { ... } }
The parameters for HasManyToAny are (in order of apperances in the constructor):
- typeof(IPayment) - the type of the objects in this collection
- pay_id - the key column that maps the values in this collection to this object
- payment_table - the table for this collection
- typeof(int) - the type of the id column - identical to the first parameter of [Any]
- Billing_Details_Type - identical in function to the Billing_Details_Type mentioned above
- Billing_Details_Id - identical to the Billing_Details_Id mentioned above
- MetaType=typeof(string - the type of the type column - identical to the one above
