|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
PrefaceThere are many breaking changes in this version of the code. This version is not backward compatible with previous versions. The article text has been updated to reflect the changes and the code snippets provided here will work only with the latest version of Light. IntroductionThis article is about a small and simple ORM library. There are many good ORM solutions out there, so why did I decide to write another one? Well, the main reason is simple: I like to know exactly which code runs in my applications and what is going on in it. Moreover, if I get an exception I'd like to be able to pinpoint the location in the code where it could have originated without turning on the debugger. Other obvious reasons include me wanting to know how to write one of these and not having to code simple CRUD ADO.NET commands for every domain object. Purpose and GoalThe purpose of this library is to allow client code (user) to run basic database commands for domain objects. The assumption is that an object would represent a record in the database table. I think it is safe to say that most of us who write object-oriented code that deals with the database have these objects in some shape or form. So the goal was to create a small library that would allow me to reuse those objects and not constrain me to any inheritance or interface implementations. Also, I wanted to remain in control: I definitely did not want something to be generating the tables or classes for me. By the same token, I wanted to stay away from XML files for mapping information because this adds another place to maintain the code. I understand that it adds flexibility, but in my case it is not required. DesignOne of the things I wanted to accomplish was to leave the user in control of the database connection. The connection is the only resource that the user has to provide for this library to work. This ORM library ( Using the Code
There are two more attributes that aid with inheritance and interface implementation:
There is another attribute that helps with such things as object validation and management of related objects:
The most useful class of the A word about exceptions is in order. There are couple exceptions that can be thrown by Please note that both try {
T t = new T();
dao.Insert<T>(t);
}
catch(DbException e) {
SqlException sqle = (SqlException) e;
}
catch(DeclarationException e) {
...
}
catch(TriggerException e) {
...
}
catch(LightException e) {
if(e.InnerException != null) //then the following is always true
bool truth = e.Message.Equals(e.InnerException.Message);
}
You cannot create an instance of a All operations (except Note that for all of this to work, the create table dbo.person (
id int not null identity(1,1) primary key,
name varchar(30),
dob datetime
)
go
Now let's write some code. Note that this code has not been tested to compile; please use the demo project as a working sample: using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
using System.Collections.Generic;
using Light; // Light library namespace - this is all you need to use it.
//
// Defines a mapping of this interface type to the dbo.person table.
//
[Table("person", "dbo")]
public interface IPerson
{
[Column("id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
Id { get; set; }
[Column("name", DbType.AnsiString, 30)]
Name { get; set; }
[Column("dob", DbType.DateTime)]
Dob { get; set; }
}
//
// Says that when operating on type Mother the table definition from
// type IPerson should be used.
//
[TableRef(typeof(IPerson))]
public class Mother : IPerson
{
private int id;
private string name;
private DateTime dob;
public Mother() {}
public Mother(int id, string name, DateTime dob)
{
this.id = id;
this.name = name;
this.dob = dob;
}
public int Id
{
get { return id; }
set { id = value; }
}
public string Name
{
get { return name; }
set { name = value; }
}
public DateTime Dob
{
get { return dob; }
set { dob = value; }
}
}
//
// Notice that this class is identical to Mother but does not
// implement the IPerson interface, so it has to define its
// own mapping.
//
[Table("person", "dbo")]
public class Father
{
private int id;
private string name;
private DateTime dob;
public Father() {}
public Father(int id, string name, DateTime dob)
{
this.id = id;
this.name = name;
this.dob = dob;
}
[Column("id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
public int Id
{
get { return id; }
set { id = value; }
}
[Column("name", DbType.AnsiString, 30)]
public string Name
{
get { return name; }
set { name = value; }
}
[Column("dob", DbType.DateTime)]
public DateTime Dob
{
get { return dob; }
set { dob = value; }
}
}
//
// Same thing but using a struct.
//
[Table("person", "dbo")]
public struct Son
{
[Column("id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
public int Id;
[Column("name", DbType.AnsiString, 30)]
public string Name;
[Column("dob", DbType.DateTime)]
public DateTime Dob;
}
//
// Delegating with a struct.
//
[TableRef(typeof(IPerson))]
public struct Daughter : IPerson
{
private int id;
private string name;
private DateTime dob;
public int Id
{
get { return id; }
set { id = value; }
}
public string Name
{
get { return name; }
set { name = value; }
}
public DateTime Dob
{
get { return dob; }
set { dob = value; }
}
}
//
// Main.
//
public class Program
{
public static void Main(string[] args)
{
string s = "Server=.;Database=test;Uid=sa;Pwd=";
// We use a SqlConnection, but any IDbConnection should do the trick
// as long as you are using the correct Dao implementation to
// generate SQL statements.
SqlConnection cn = new SqlConnection(s);
// Here is the Data Access Object.
Dao dao = new SqlServerDao(cn);
// This would also work:
// Dao dao = new SqlServerDao();
// dao.Connection = cn;
try
{
// The connection must be opened before using the Dao object.
cn.Open();
Mother mother = new Mother(0, "Jane", DateTime.Today);
int x = dao.Insert(mother);
Console.WriteLine("Records affected: " + x.ToString());
Console.WriteLine("Mother ID: " + mother.Id.ToString());
Father father = new Father(0, "John", DateTime.Today);
x = dao.Insert(father);
Console.WriteLine("Father ID: " + father.Id.ToString());
// We can also force father to be treated as
// another type by the Dao.
// This is not limited to Insert, but the object and type
// MUST be compatible.
dao.Insert<IPerson>(father);
// This will also work.
dao.Insert(typeof(IPerson), father);
// We now have 3 fathers. Let's get rid of the last one.
// The 'father' variable has the last Father inserted because
// its Id was set to the last inserted identity.
x = dao.Delete(father);
// Now we have 2 fathers. Let's get them from the database.
IList<Father> fathers = dao.Select<Father>();
Console.WriteLine(fathers.Count);
// NOTICE: Dao.Select and Dao.Find methods instantiate objects
// internally so you cannot use an interface type
// as the type of objects to return. In other words,
// the runtime must be able to create instance of given type
// using reflection (Activator.CreateInstance method).
// The safest approach you can take is to make
// sure that every entity type has a default constructor
// (it could be private).
Son son;
son.Name = "Jimmy";
son.Dob = DateTime.Today;
dao.Insert(son);
// Daughter is a struct, so it cannot be null.
// If record with given id is not found and the type is a struct,
// then an empty struct of given type is returned.
// This, obviously, only works for the generic version
// of the Find method. The other version returns an object,
// so null will be returned.
// The following is usually not a good idea,
// but they are compatible by table definitions.
Daughter daughter = dao.Find<Daughter>(son.Id);
Console.WriteLine(daughter.Name); // should print "Jimmy"
daughter.Name = "Mary";
dao.Update(daughter);
// Refresh the son.
// Generics not used, so the return type is object,
// could be null if not found.
object obj = dao.Find(typeof(Son), son.Id);
if(obj != null)
{
son = (Son) obj;
Console.WriteLine(son.Name); // should print "Mary"
}
}
catch(LightException e)
{
Console.WriteLine(e.Message);
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
dao.Dispose();
try { cn.Close(); }
catch {}
}
}
}
Delegating table definition to another type was fairly easy, in my opinion. You simply apply using System;
using System.Data;
using Light;
public class AbstractPerson
{
protected int personId;
public int PersonId
{
get { return personId; }
set { personId = value; }
}
public abstract string Name { get; set; }
public abstract DateTime Dob { get; set; }
public abstract void Work();
}
//
// Maps the inherited property "PersonId".
//
[Table("person", "dbo")]
[Mapping("PersonId", "id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
public class Father : AbstractPerson
{
private string name;
private DateTime dob;
public Father() {}
[Column("name", DbType.AnsiString, 30)]
public override string Name
{
get { return name; }
set { name = value; }
}
[Column("dob", DbType.DateTime)]
public override DateTime Dob
{
get { return dob; }
set { dob = value; }
}
public override void Work()
{
// whatever he does at work...
}
}
//
// Maps the inherited protected field "personId".
//
[Table("person", "dbo")]
[Mapping("personId", "id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
public class Mother : AbstractPerson
{
[Column("name", DbType.AnsiString, 30)]
private string name;
[Column("dob", DbType.DateTime)]
private DateTime dob;
public Mother() {}
public override string Name
{
get { return name; }
set { name = value; }
}
public override DateTime Dob
{
get { return dob; }
set { dob = value; }
}
public override void Work()
{
// whatever she does at work...
}
}
Notice that Querying
The concept is identical to using a The Parameterized SQL statements are a recommended way of querying the database. It allows the database to cache the execution plan for later reuse. This means that the database does not have to parse your SQL statements each time they are executed, which definitely helps the performance. The //
// We will use the Son struct defined previously in the article.
// Assume we have a number of records in the dbo.person
// table to which Son maps.
//
using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections.Generic;
using Light;
IDbConnection cn = new SqlConnection(connectionString);
Dao dao = new SqlServerDao(cn);
cn.Open();
// This will return all Sons born in the last year
// sorted from youngest to oldest.
// We will use the chaining abilities of the Query and Parameter objects.
IList<Son> bornLastYear = dao.Select<Son>(
new Query("dob BETWEEN @a AND @b", "dob DESC")
.Add(
new Parameter()
.SetName("@a")
.SetDbType(DbType.DateTime)
.SetValue(DateTime.Today.AddYear(-1)),
new Parameter()
.SetName("@b")
.SetDbType(DbType.DateTime)
.SetValue(DateTime.Today)
)
);
// This will return all Sons named John - non-parameterized version.
IList<Son> johnsNoParam = dao.Select<Son>(new Query("name='John'"));
// This will do the same thing, but using parameters.
IList<Son> johnsParam = dao.Select<Son>(
new Query("name=@name", "dob ASC").Add(
new Parameter("@name", DbType.AnsiString, 30, "John")
));
// This will return all Sons whose name starts with letter J.
Query query = new Query("name like @name").Add(
new Parameter("@name", DbType.AnsiString, 30, "J%")
);
IList<Son> startsWithJ = dao.Select<Son>(query);
//
// We can use the same, previously defined, queries to delete records.
//
// This will delete all Sons whose name starts with letter J.
int affectedRecords = dao.Delete<Son>(query);
dao.Dispose();
cn.Close();
cn.Dispose();
The creation of Default Table and Column NamesYou can omit the name of the table in [Table] //same as [Table("Person")]
//[Table(Schema="dbo")] if you need to specify a schema.
public class Person
{
[Column(DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
// same as [Column("personId", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
private int personId;
private string myName;
[Column(DbType.AnsiString, 30)]
// same as [Column("Name", DbType.AnsiString, 30)]
public string Name
{
get { return myName; }
set { myName = value; }
}
}
TriggersThe concept of triggers comes from the database. A database trigger is a piece of code that is executed when a certain action occurs on a table, on which the trigger is defined. Trigger methods can be called before and/or after the So, the point here is that triggers are only called on existing objects. Hence, another caveat. When calling Here is some code demonstrating the use of triggers. The code has not been tested to compile or run. Assume we have the following table in our SQL Server database: create table parent (
parentid int not null identity(1,1) primary key,
name varchar(20)
)
go
create table child (
childid int not null identity(1,1) primary key,
parentid int not null foreign key references parent (parentid),
name varchar(20)
)
go
Here are C# classes defining this fake parent/child relationship: using System;
using System.Data;
using System.Collections.Generic;
using Light;
//
// Here is our parent.
//
[Table("parent")]
public class Parent
{
[Column("parentid", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
private int id = 0;
[Column("name", DbType.AnsiString, 20)]
private string name;
private IList<Child> children = new List<Child>();
public Parent()
{}
// No setter as it will be assigned by the database.
public int ParentId
{
get { return id; }
}
public string ParentName
{
get { return name; }
set { name = value; }
}
public int ChildCount
{
get { return children.Count; }
}
public Child GetChild(int index)
{
if(index > 0 && index < children.Count)
return children[index];
return null;
}
public void AddChild(Child child)
{
child.Parent = this;
children.Add(child);
}
public void RemoveChild(Child child)
{
if(children.Contains(child))
{
child.Parent = null;
children.Remove(child);
}
}
//
// Triggers
//
[Trigger(Actions.BeforeInsert | Actions.BeforeUpdate)]
private void BeforeInsUpd(TriggerContext context)
{
// We can do validation here!!!
// Let's say that the name cannot be empty.
if(string.IsNullOrEmpty(name))
{
// This will cause Dao to throw an exception
// and will abort the current transaction.
context.Fail("Parent's name cannot be empty.");
}
}
[Trigger(Actions.AfterInsert | Actions.AfterUpdate)]
private void AfterInsUpd(TriggerContext context)
{
// Let's save all the children. The database is ready
// for it because now this parent's id is in there
// and referential integrity will not break.
Dao dao = context.Dao;
if(context.TriggeringAction == Actions.AfterUpdate)
{
// There may have been children already saved
// so we need to delete them first.
dao.Delete<Child>(new Query("parentid=@id").Add(
new Parameter().SetName("@id").SetDbType(DbType.Int32)
.SetValue(this.id)
));
}
// And now we can insert the children.
dao.Insert<Child>(children);
}
[Trigger(Actions.AfterActivate)]
private void AfterActivate(TriggerContext context)
{
// Let's load all the children.
Dao dao = context.Dao;
children = dao.Select<Child>(new Query("parentid=@id").Add(
new Paremter().SetName("@id").SetDbType(DbType.Int32)
.SetValue(this.id)
));
foreach(Child child in children)
child.Parent = this;
}
}
[Table("child")]
public class Child
{
[Column("childid", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
private int id = 0;
[Column("name", DbType.AnsiString, 20)]
private string name;
private Parent parent;
public Child()
{}
public int ChildId
{
get { return id; }
}
public string Name
{
get { return name; }
set { name = value; }
}
public Parent Parent
{
get { return parent; }
set { parent = value; }
}
[Column("parentid", DbType.Int32)]
private int ParentId
{
get
{
if(parent != null)
return parent.Id;
return 0;
}
}
}
public class Program
{
public static void Main(string[] args)
{
SqlConnection cn = new SqlConnection("Server=.; Database=test; Uid=sa; Pwd=");
Dao dao = new SqlServerDao(cn);
cn.Open();
// Set up parent/child relationships.
Parent jack = new Parent();
jack.Name = "Parent Jack";
Child bob = new Child();
bob.Name = "Child Bob";
Child mary = new Child();
mary.Name = "Child Mary";
jack.AddChild(bob);
jack.AddChild(mary);
// When we save the parent, its children will also be saved.
dao.Insert<Parent>(jack);
// This id was assigned by the database.
int jacksId = jack.Id;
// Let's now pull jack from the database.
Parent jack2 = dao.Find<Parent>(jacksId);
// All Jack's children should be loaded by now.
Console.WriteLine("Jack's children are:");
for(int i = 0; i < jack2.ChildCount; i++)
Console.WriteLine(jack2.GetChild(i).Name);
dao.Dispose();
cn.Close();
}
}
Be careful not to create triggers that load objects in circles. For example, say we would add a trigger to the So, in a one-to-many relationship or cases where one object fully depends on another, triggers are very helpful. However, they will rarely be able to handle many-to-many relationships unless your code is disciplined enough to only access related objects from one side all the time. Of course, triggers don't solve all the issues of related objects, but in some cases they might help. Stored Procedures
Example: the SQLcreate table users (
userid int identity(1,1) primary key,
username varchar(30)
)
go
create table roles (
roleid int identity(1,1) primary key,
rolename varchar(30)
)
go
-- intermediate table: defines many-to-many relationship
create table userrole (
userid int foreign key references users(userid),
roleid int foreign key references roles(roleid),
constraint pk_userrole primary key(userid, roleid)
)
go
create procedure getroles(@userid int) as
begin
select roles.*
from roles join userrole on roles.roleid = userrole.roleid
where userrole.userid = @userid
end
go
Example: the C#using System;
using System.Data;
using System.Data.SqlClient;
using System.Collection.Generic;
using Light;
[Table("roles")]
public class Role
{
[Column(DbType.Int32, PrimaryKey=true, AutoIncrement=true)] private int roleid;
[Column(DbType.AnsiString, 30)] private string rolename;
public int Id {
get { return roleid; }
set { roleid = value; }
}
public string Name {
get { return rolename; }
set { rolename = value; }
}
}
[Table("users")]
public class User
{
[Column(DbType.Int32, PrimaryKey=true, AutoIncrement=true)] private int userid;
[Column(DbType.AnsiString, 30)] private string username;
private IList<Role> roles = new List<Role>();
public int Id {
get { return userid; }
set { userid = value; }
}
public string Name {
get { return username; }
set { username = value; }
}
public IList<Role> Roles {
get { return roles; }
}
[Trigger(Actions.AfterConstruct)]
private void T1(TriggerContext ctx)
{
// Notice that we are not using the UserRole objects
// here to pull the list of Role objects.
Dao dao = ctx.Dao;
roles = dao.Call<Role>(
new Procedure("getroles").Add(
new Parameter("@userid", DbType.Int32, this.userid)
)
);
}
}
[Table]
public class UserRole
{
[Column(DbType.Int32, PrimaryKey=true)] private int userid;
[Column(DbType.Int32, PrimaryKey=true)] private int roleid;
public int UserId {
get { return userid; }
set { userid = value; }
}
public int RoleId {
get { return roleid; }
set { roleid = value; }
}
}
public class Program
{
public static void Main(string[] args)
{
SqlConnection cn = new SqlConnection("Server=.; Database=test; Uid=sa; Pwd=");
cn.Open();
Dao dao = new SqlServerDao(cn);
// add new user
User user1 = new User();
user1.Name = "john";
dao.Insert(user1);
// add some roles
for(int i = 0; i < 3; i++)
{
// create role
Role role = new Role();
role.Name = "role " + (i+1).ToString();
dao.Insert(role);
// associate with user1
UserRole userrole = new UserRole();
userrole.UserId = user1.Id;
userrole.RoleId = role.Id;
dao.Insert(userrole);
}
// let's select the only user from the database
// it should have all roles in its Roles property
User user2 = dao.Find<User>(user1.Id);
Console.WriteLine("Roles of " + user2.Name + ":");
foreach(Role role in user2.Roles)
{
Console.WriteLine(role.Name);
}
dao.Dispose();
cn.Close();
}
}
Performance
ConclusionThe demo project provided is not really a demo project. It is just a bunch of NUnit tests that I ran against an SQL Server 2005 database. So, if you want to run the demo project, you will need to reference (or re-reference) the NUnit DLL that is on your system. Also, you will need to compile the source code and reference it from the demo project. No binaries are provided in the downloads, only source code. You don't need Visual Studio to use these projects; you can use a freely available SharpDevelop IDE (which was used to develop Also included is an extension project by Jordan Marr. His code adds support for concurrency and introduces a useful business framework structure. It keeps track of object properties that were changed and only updates objects if anything was changed. This reduces the load on the database. The business framework also allows you to add validation rules to your objects. The code is fully commented, so you may find some more useful information there. I hope this was, is or will be useful to somebody in some way... CreditsMany thanks to Jordan Marr for his contribution, feedback, ideas and the extension project. History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||