web-dev-qa-db-ja.com

C#でSQLクエリの結果からクラスにデータを入力するにはどうすればよいですか?

私はこのようなクラスを持っています:

public class Product
{
    public int ProductId { get; private set; }
    public int SupplierId { get; private set; }

    public string Name { get; private set; }
    public decimal Price { get; private set; }
    public int Stock { get; private set; }
    public int PendingStock { get; private set; }
}

これらの詳細は、次のようにデータベースから取得できます。

SELECT product_id, supplier_id, name, price, total_stock, pending_stock 
FROM products
WHERE product_id = ?

値を設定するためにDataSetまたはDataTableを手動で実行する必要はありません。

ある種のバインディング/マッピングメカニズムを使用してクラスにデータを設定する方法があると確信していますが、私が見つけたのは、winformsコンポーネントへのバインディングまたはXAMLの使用だけでした。

プロパティ/クラスに適用して、クエリ行からクラスに自動的にデータを入力できるような属性はありますか?

12
Polynomial

私は別の回答を提案することにしました。これは実際にはAlexによって提供された回答の拡張です(つまり、すべてのクレジットが彼にあります)が、列名2プロパティ名のマッピングのために属性が導入されています。

まず、列名を保持するためのカスタム属性が必要です。

[AttributeUsage(AttributeTargets.Property, Inherited = true)]
[Serializable]
public class MappingAttribute : Attribute
{
    public string ColumnName = null;
}

この属性は、データベース行から入力されるクラスのプロパティに適用する必要があります。

public class Product
{
    [Mapping(ColumnName = "product_id")]
    public int ProductId { get; private set; }

    [Mapping(ColumnName = "supplier_id")]
    public int SupplierId { get; private set; }

    [Mapping(ColumnName = "name")]
    public string Name { get; private set; }
    [Mapping(ColumnName = "price")]
    public decimal Price { get; private set; }
    [Mapping(ColumnName = "total_stock")]
    public int Stock { get; private set; }
    [Mapping(ColumnName = "pending_stock")]
    public int PendingStock { get; private set; }
}

そして、属性が列名を取得するために使用されることを除いて、残りはAlexが提案したとおりに進みます。

T MapToClass<T>(SqlDataReader reader) where T : class
{
        T returnedObject = Activator.CreateInstance<T>();
        PropertyInfo[] modelProperties = returnedObject.GetType().GetProperties();
        for (int i = 0; i < modelProperties.Length; i++)
        {
            MappingAttribute[] attributes = modelProperties[i].GetCustomAttributes<MappingAttribute>(true).ToArray();

            if (attributes.Length > 0 && attributes[0].ColumnName != null)
                modelProperties[i].SetValue(returnedObject, Convert.ChangeType(reader[attributes[0].ColumnName], modelProperties[i].PropertyType), null);
        }
        return returnedObject;
}
13
Kuba Wyrostek

プロパティを自分でマップするか、ORM(オブジェクトリレーショナルマッパー)を使用する必要があります。

Microsoftは Entity Framework を提供していますが、 Dapper はオーバーヘッドが少なくて済み、要件によっては実行可能なオプションになる場合があります。

あなたの場合、Dapperコードは次のようになります。

var query = @"SELECT product_id, supplier_id, name, price, total_stock, pending_stock 
FROM products
WHERE product_id = @id";

var product = connection.Query<Product>(query, new { id = 23 });

完全を期すために、ここでDapperについて話していることを指摘することが重要です。これは、SQLの結果をオブジェクトにマッピングすることに関する質問だからです。 EFとLinqto SQLもこれを実行しますが、LinqクエリをSQLステートメントに変換するなど、追加の処理も実行します。これも役立つ場合があります。

12
madd0

ORMフレームワーク(Entity Frameworkなど)を活用したくない場合は、手動で行うことができます。

T MapToClass<T>(SqlDataReader reader) where T : class
{
        T returnedObject = Activator.CreateInstance<T>();
        List<PropertyInfo> modelProperties = returnedObject.GetType().GetProperties().OrderBy(p => p.MetadataToken).ToList();
        for (int i = 0; i < modelProperties.Count; i++)
            modelProperties[i].SetValue(returnedObject, Convert.ChangeType(reader.GetValue(i), modelProperties[i].PropertyType), null);
        return returnedObject;
}

あなたはそれをこのように使います:

Product P = new Product(); // as per your example
using(SqlDataReader reader = ...)
{
while(reader.Read()) { P = MapToClass<Product(reader); /* then you use P */ }
}

注意すべきことは、クエリ内のフィールドの順序だけです(クラスで定義されているプロパティの順序と一致する必要があります)。

あなたがする必要があるのは、クラスを構築し、クエリを書くことだけです、そしてそれは「マッピング」の面倒を見ます。

[〜#〜] warning [〜#〜]私はこのメソッドを頻繁に使用し、問題は発生しませんでしたが、部分クラスでは正しく機能しません。部分的なモデルになると、とにかくORMフレームワークを使用する方がはるかに良いでしょう。

4
Alex

Linq to SQLを使用して、次のようにします。

public class Product
{
    public int ProductId { get; private set; }
    public int SupplierId { get; private set; }
    public string Name { get; private set; }
    public decimal Price { get; private set; }
    public int Stock { get; private set; }
    public int PendingStock { get; private set; }

    public Product(int id)
    {
        using(var db = new MainContext())
        {
            var q = (from c in product where c.ProductID = id select c).SingleOrDefault();
            if(q!=null)
                LoadByRec(q);           
        }
    }
    public Product(product rec)
    {
        LoadByRec(q);
    }
    public void LoadByRec(product rec)
    {
        ProductId = rec.product_id;
        SupplierID = rec.supplier_id;
        Name = rec.name;
        Price = rec.price;
        Stock = rec.total_stock;
        PendingStock = rec.pending_stock;
    }
}
2
Tom Gullen

Raw .NET Frameworkには、デフォルトではそのような機能はありません。 Entity Frameworkを使用することもできますが、それが適切なソリューションでない場合は、代わりにリフレクションメカニズムを使用します。

  1. クラスの各パブリックプロパティの列名を保持できるカスタム属性クラスを作成します。

  2. データベースからレコードを取得した後、Productクラスのオブジェクトをインスタンス化し、プロパティを列挙します。カスタム属性を持つプロパティごとに、SetValuePropertyInfoを使用して、カスタム属性で定義された列名に従って値を変更します。

次の点を考慮してください。

  • 解決策は、単純な割り当てのオーバーヘッドです。 Productのような多くのテーブルと多くのクラスがあり、それらすべてを自動的に初期化する1つのコードを記述したい場合にのみ意味があります。
  • リフレクション自体はオーバーヘッドであるため、長期的にはキャッシュが必要になります
1
Kuba Wyrostek

1つの可能な解決策:

SQLクエリでは、 "PATH Mode with FOR XML" 句を使用できます。

クエリの結果はXMLになります。これは C#オブジェクトに直接逆シリアル化 できます。

また、ネストされた大規模なSQLクエリでも非常にうまく機能します。

1
Miro Malek

次に、EntityFrameworkまたはLinqTo SQLを使用する必要があります。それを使用したくない場合は、自分でマップ/入力する必要があります。

entity Frameworkの詳細 http://msdn.Microsoft.com/en-us/data/ef.aspx

linq to SQLの詳細 http://msdn.Microsoft.com/en-us/library/bb386976.aspx

0
JohnnBlade