読者です 読者をやめる 読者になる 読者になる

なんちゃってActiveRecord

ActiveRecordって何?

ActiveRecordデザインパターンの一種です。

データベーステーブルまたはビューの行をラップし、データベースアクセスをカプセル化して
データにドメインロジックを追加するオブジェクト。

PofEAA's Wiki - ActiveRecord

ActiveRecordC# で楽に使いたい!

Ruby on RailsActiveRecord::Base みたいなベースクラスを作ってみました。マッピング用に新しく属性を作成したくなかったので、LINQ to SQL のものを利用しています。

public abstract class ActiveRecord<T>
    where T : class
{
    protected ActiveRecord()
    {
    }

    public static List<T> FindAll()
    {
        using (var context = CreateDataContext())
        {
            return context.GetTable<T>().ToList();
        }
    }

    public static List<T> Find(Func<T, bool> predicate)
    {
        using (var context = CreateDataContext())
        {
            return context.GetTable<T>().Where(predicate).ToList();
        }
    }

    public static T Get(Func<T, bool> predicate)
    {
        using (var context = CreateDataContext())
        {
            return context.GetTable<T>().Single(predicate);
        }
    }

    public void Create()
    {
        using (var context = CreateDataContext())
        using (TransactionScope ts = new TransactionScope())
        {
            context.GetTable(typeof(T)).InsertOnSubmit(this);
            context.SubmitChanges();
            ts.Complete();
        }
    }

    public void Delete()
    {
        using (var context = CreateDataContext())
        using (TransactionScope ts = new TransactionScope())
        {
            var table = context.GetTable(typeof(T));
            table.Attach(this);
            table.DeleteOnSubmit(this);
            context.SubmitChanges();
            ts.Complete();
        }
    }

    public void Update()
    {
        using (var context = CreateDataContext())
        using (TransactionScope ts = new TransactionScope())
        {
            var table = context.GetTable(typeof(T));
            table.Attach(this, true);
            table.Context.SubmitChanges();
            ts.Complete();
        }
    }

    // 接続文字列の指定方法が課題
    public static string ConnectionString { get; set; }

    private static DataContext CreateDataContext()
    {
        return new DataContext(ConnectionString);
    }
}

接続文字列の指定方法がイマイチですね。構成ファイルから取得した方がよかったかな。

さっそく使ってみます!

まず LINQ to SQL クラスを用意します。Visual Studio でサクっと作成。

[Table(Name="dbo.Customer")]
public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
{
	
	private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
	
	private string _Code;
	
	private string _Name;
	
	private System.Data.Linq.Binary _UpdatedAt;
	
#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnCodeChanging(string value);
partial void OnCodeChanged();
partial void OnNameChanging(string value);
partial void OnNameChanged();
partial void OnUpdatedAtChanging(System.Data.Linq.Binary value);
partial void OnUpdatedAtChanged();
#endregion
	
	public Customer()
	{
		OnCreated();
	}
	
	[Column(Storage="_Code", DbType="NVarChar(50) NOT NULL", CanBeNull=false, IsPrimaryKey=true, UpdateCheck=UpdateCheck.Never)]
	public string Code
	{
		get
		{
			return this._Code;
		}
		set
		{
			if ((this._Code != value))
			{
				this.OnCodeChanging(value);
				this.SendPropertyChanging();
				this._Code = value;
				this.SendPropertyChanged("Code");
				this.OnCodeChanged();
			}
		}
	}
	
	[Column(Storage="_Name", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false, UpdateCheck=UpdateCheck.Never)]
	public string Name
	{
		get
		{
			return this._Name;
		}
		set
		{
			if ((this._Name != value))
			{
				this.OnNameChanging(value);
				this.SendPropertyChanging();
				this._Name = value;
				this.SendPropertyChanged("Name");
				this.OnNameChanged();
			}
		}
	}
	
	[Column(Storage="_UpdatedAt", AutoSync=AutoSync.Always, DbType="rowversion", IsDbGenerated=true, IsVersion=true, UpdateCheck=UpdateCheck.Never)]
	public System.Data.Linq.Binary UpdatedAt
	{
		get
		{
			return this._UpdatedAt;
		}
		set
		{
			if ((this._UpdatedAt != value))
			{
				this.OnUpdatedAtChanging(value);
				this.SendPropertyChanging();
				this._UpdatedAt = value;
				this.SendPropertyChanged("UpdatedAt");
				this.OnUpdatedAtChanged();
			}
		}
	}
	
	public event PropertyChangingEventHandler PropertyChanging;
	
	public event PropertyChangedEventHandler PropertyChanged;
	
	protected virtual void SendPropertyChanging()
	{
		if ((this.PropertyChanging != null))
		{
			this.PropertyChanging(this, emptyChangingEventArgs);
		}
	}
	
	protected virtual void SendPropertyChanged(String propertyName)
	{
		if ((this.PropertyChanged != null))
		{
			this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}
	}
}

このクラスのベースクラスに、先ほど作成した ActiveRecord クラスを指定します。生成されたファイルとは別のファイルに記述しておいた方がいいですね。

public partial class Customer : ActiveRecord<Customer>
{
    // ドメインロジックをなんか書く
}

ドメインロジックは省略。これで準備終了です。

実際にメソッドを利用したのがこちら。

// 接続文字列を指定
// 接続文字列の指定方法もちゃんと考えないとな〜
SqlConnectionStringBuilder builder
    = new SqlConnectionStringBuilder();
builder.DataSource = "(local)";
builder.InitialCatalog = "Test";
builder.IntegratedSecurity = true;
Customer.ConnectionString = builder.ToString();

// 作成
Customer customer = new Customer();
customer.Code = "0001";
customer.Name = "Foo";
customer.Create();

// 全件取得
List<Customer> customers = Customer.FindAll();
Console.WriteLine(customers.Count); //=> 1

// 更新
customer = Customer.Get(c => c.Code == "0001");
customer.Name = "Bar";
customer.Update();

// 削除
customer = Customer.Find(c => c.Name == "Bar").First();
Console.WriteLine(customer.Name);   //=> Bar
customer.Delete();

結構いいかも。

ただし問題があります

内部で LINQ to SQL を使っているため、LINQ to SQL で作成・更新・削除の操作が出来ないテーブルでは使えません。
例えば、主キーが無いと作成・削除ができないですし、行のバージョンを判断する情報が無いと更新ができません。

実は既にActiveRecord のライブラリはあったりします!

Castle Project の中に ActiveRecord があります。

NHibernate をベースにしているみたいですが、まだ使ったことはありません。LINQ to SQLADO.NET Entity Framework と、どちらが便利ですかね。