Skip to content

crazyants/artisan-orm

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Artisan ORM — ADO.NET Micro-ORM to SQL Server

Artisan ОRМ was created to meet the following requirements:

  • interactions with database should mostly be made through stored procedures;
  • all calls to database should be encapsulated into repository methods;
  • a repository method should be able to read or save a complex object graph with one stored procedure;
  • it should work with the highest possible performance, even at the expense of the convenience and development time.

To achieve these goals Artisan ORM uses:

Artisan ОRМ is available as NuGet Package.

NuGet Logo

More information is available in Artisan ORM documentation Wiki.

Simple sample

Let's say we have database tables Users and UserRoles (TSQL):

create table dbo.Users
(
    Id       int           not null	  identity primary key clustered,
    [Login]  varchar(20)   not null	  ,
    Name     nvarchar(50)  not null	  ,
    Email    varchar(50)   not null
);

create table dbo.UserRoles
(
    UserId   in       not null   foreign key (UserId) references dbo.Users (Id),	
    RoleId   tinyint  not null   foreign key (RoleId) references dbo.Roles (Id),

    primary key clustered (UserId, RoleId)
);

// The Roles table is just a dictionary or lookup table and users don't edit it.

And we have a User class (C#):

public class User
{
    public Int32   Id       { get; set; }
    public String  Login    { get; set; }
    public String  Name     { get; set; }
    public String  Email    { get; set; }
    public Byte[]  RoleIds  { get; set; }
}

In order to read and save the User data in Artisan ORM way it is required to create:

  • mapper static vlass (C#)
  • user-defined table type (TSQL)
  • stored procedures (TSQL)

Mapper static class (C#) is decorated with MapperFor attribute and consists of four static methods with reserved names:

  • CreateObject
  • CreateObjectRow
  • CreateDataTable
  • CreateDataRow
[MapperFor(typeof(User)]
public static class UserMapper 
{
    public static User CreateObject(SqlDataReader dr)
    {
        var i = 0;

        return new User 
        {
            Id     =  dr.GetInt32(i++)  ,
            Login  =  dr.GetString(i++) ,
            Name   =  dr.GetString(i++) ,
            Email  =  dr.GetString(i++)
        };
    }

    public static ObjectRow CreateObjectRow(SqlDataReader dr)
    {
        var i = 0;

        return new ObjectRow(4)
        {
            /* 0 - Id      =  */  dr.GetInt32(i++)  ,
            /* 1 - Login   =  */  dr.GetString(i++) ,
            /* 2 - Name    =  */  dr.GetString(i++) ,
            /* 3 - Email   =  */  dr.GetString(i++)
        };
    }

    public static DataTable CreateDataTable()
    {
        var table = new DataTable("UserTableType");

        table.Columns.Add( "Id"    ,  typeof( Int32  ));
        table.Columns.Add( "Login" ,  typeof( String ));
        table.Columns.Add( "Name"  ,  typeof( String ));
        table.Columns.Add( "Email" ,  typeof( String ));

        return table;
    }

    public static Object[] CreateDataRow(User obj)
    {
        if (obj.Id == 0) 
            obj.Id = Int32NegativeIdentity.Next;

        return new object[]
        {
            obj.Id     ,
            obj.Login  ,
            obj.Name   ,
            obj.Email
        };
    }
}

User-defined table type (TSQL):

create type dbo.UserTableType as table
(
    Id       int           not null   primary key clustered,
    [Login]  varchar(20)   not null   ,
    Name     nvarchar(50)  not null   ,
    Email    varchar(50)   not null
);

Stored procedures (TSQL):

create procedure dbo.GetUserById
    @Id    int
as
begin
    set nocount on;

    -- read User

    select
        Id       ,
        [Login]  ,
        Name     ,
        Email
    from
        dbo.Users
    where
        Id = @Id;
        
    -- read User RoleIds
        
    select
        RoleId
    from
        dbo.UserRoles
    where
        UserId = @Id;
        
end;
create procedure dbo.SaveUser
    @User     dbo.UserTableType       readonly,
    @RoleIds  dbo.TinyIntIdTableType  readonly
as
begin
    set nocount on;
    
    declare @UserIds table ( InsertedId int primary key, ParamId int unique);

    begin -- save User

        merge into dbo.Users as target
            using 
            (
                select
                    Id       ,
                    [Login]  ,
                    Name     ,
                    Email
                from
                    @User
            ) 
            as source on source.Id = target.Id

        when matched then
            update set
                [Login]  =  source.[Login]  ,
                Name     =  source.Name     ,
                Email    =  source.Email    

        when not matched by target then                                                         
            insert (    
                [Login]  , 
                Name     ,
                Email    )
            values (
                source.[Login]  , 
                source.Name     , 
                source.Email    )
    
        output          inserted.Id , source.Id
        into @UserIds ( InsertedId  , ParamId   );

    end; 

    begin -- save UserRoles
            
        merge into dbo.UserRoles as target
            using 
            (
                select
                    UserId  =  ids.InsertedId,
                    RoleId  =  r.Id
                from 
                    @User u
                    inner join @UserIds ids on ids.ParamId = u.Id
                    cross join @RoleIds r
            )
            as source on source.UserId = target.UserId and source.RoleId = target.RoleId

        when not matched by target then                                                         
            insert (    
                UserId  ,
                RoleId  )
            values (
                source.UserId  ,
                source.RoleId  )

        when not matched by source and target.UserId in (select InsertedId from @UserIds) then    
            delete;
    end;

end;

Having prepared mapper, user-defined table type and stored procedures we can write the UserRepository (C#):

public class UserRepository: Artisan.Orm.RepositoryBase
{
    public User GetById(int id)
    {
        return GetByCommand(cmd =>
        {
            cmd.UseProcedure("dbo.GetUserById");

            cmd.AddIntParam("@Id", id);

            return cmd.GetByReader(reader =>
            {
                var user     = reader.ReadTo<User>();
                user.RoleIds = reader.ReadToArray<byte>();            
                
				return user;
            });

        });
    }

    public void Save(User user)
    {
        ExecuteCommand(cmd =>
        {
            cmd.UseProcedure("dbo.SaveUser");

            cmd.AddTableParam( "@User"    , user         );
            cmd.AddTableParam( "@RoleIds" , user.RoleIds );
        });
    }
}

More examples of the Artisan ORM usage are available in the Tests and Database projects.

For further reading please visit Artisan ORM documentation Wiki.

About

Yet another ADO.NET Micro-ORM to SQL Server

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 87.9%
  • PLpgSQL 5.9%
  • SQLPL 5.9%
  • PLSQL 0.3%