文書化された投稿/ユーザーの例 によく似ているように見えるので、私はこれで少し遊んでいますが、わずかに異なっており、私にとっては機能していません。
次の簡単なセットアップを想定しています(連絡先には複数の電話番号があります):
public class Contact
{
public int ContactID { get; set; }
public string ContactName { get; set; }
public IEnumerable<Phone> Phones { get; set; }
}
public class Phone
{
public int PhoneId { get; set; }
public int ContactID { get; set; } // foreign key
public string Number { get; set; }
public string Type { get; set; }
public bool IsActive { get; set; }
}
複数のPhoneオブジェクトを持つContactを返すものになりたいです。そのように、2つの連絡先があり、それぞれ2つの電話がある場合、SQLは合計4行の結果セットとしてそれらの結合を返します。次に、Dapperは、それぞれ2台の電話を持つ2つの連絡先オブジェクトをポップします。
ストアドプロシージャのSQLは次のとおりです。
SELECT *
FROM Contacts
LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1
私はこれを試しましたが、4つのタプルになりました(これは大丈夫ですが、私が望んでいたものではありません...それは単に結果を再正規化する必要があることを意味します):
var x = cn.Query<Contact, Phone, Tuple<Contact, Phone>>("sproc_Contacts_SelectByClient",
(co, ph) => Tuple.Create(co, ph),
splitOn: "PhoneId", param: p,
commandType: CommandType.StoredProcedure);
そして、別のメソッド(下記)を試すと、「タイプ 'System.Int32'のオブジェクトをタイプ 'System.Collections.Generic.IEnumerable`1 [Phone]'にキャストできません」という例外が発生します。
var x = cn.Query<Contact, IEnumerable<Phone>, Contact>("sproc_Contacts_SelectByClient",
(co, ph) => { co.Phones = ph; return co; },
splitOn: "PhoneId", param: p,
commandType: CommandType.StoredProcedure);
私は何か間違ったことをしていますか?投稿/所有者の例のように見えますが、私は子供から親へではなく、親から子供へ行きます。
前もって感謝します
あなたは何も悪いことをしていない、それはAPIが設計された方法だけではありません。すべてのQuery
APIはalwaysデータベース行ごとにオブジェクトを返します。
したがって、これは多方向->一方向ではうまく機能しますが、一方向->多方向ではうまく機能しません。
ここには2つの問題があります。
クエリで機能する組み込みのマッパーを導入すると、重複データを「破棄」することになります。 (Contacts。*はクエリで複製されます)
1->多くのペアで動作するように設計する場合、何らかのIDマップが必要になります。これにより複雑さが増します。
たとえば、限られた数のレコードをプルする必要がある場合に効率的であるこのクエリを考えてください。
var sql = "set nocount on
DECLARE @t TABLE(ContactID int, ContactName nvarchar(100))
INSERT @t
SELECT *
FROM Contacts
WHERE clientid=1
set nocount off
SELECT * FROM @t
SELECT * FROM Phone where ContactId in (select t.ContactId from @t t)"
GridReader
を拡張して再マッピングできるようにすることができます。
var mapped = cnn.QueryMultiple(sql)
.Map<Contact,Phone, int>
(
contact => contact.ContactID,
phone => phone.ContactID,
(contact, phones) => { contact.Phones = phones };
);
GridReaderを拡張し、マッパーを使用すると仮定します。
public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
(
this GridReader reader,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
var first = reader.Read<TFirst>().ToList();
var childMap = reader
.Read<TSecond>()
.GroupBy(s => secondKey(s))
.ToDictionary(g => g.Key, g => g.AsEnumerable());
foreach (var item in first)
{
IEnumerable<TSecond> children;
if(childMap.TryGetValue(firstKey(item), out children))
{
addChildren(item,children);
}
}
return first;
}
これは少し注意が必要で複雑なため、注意が必要です。私はこれをコアに含めるつもりはありません。
参考までに、次の操作を行うことで、サムの答えが得られました。
最初に、「Extensions.cs」というクラスファイルを追加しました。 2つの場所で「this」キーワードを「reader」に変更する必要がありました。
using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;
namespace TestMySQL.Helpers
{
public static class Extensions
{
public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
(
this Dapper.SqlMapper.GridReader reader,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
var first = reader.Read<TFirst>().ToList();
var childMap = reader
.Read<TSecond>()
.GroupBy(s => secondKey(s))
.ToDictionary(g => g.Key, g => g.AsEnumerable());
foreach (var item in first)
{
IEnumerable<TSecond> children;
if (childMap.TryGetValue(firstKey(item), out children))
{
addChildren(item, children);
}
}
return first;
}
}
}
次に、次のメソッドを追加し、最後のパラメーターを変更しました。
public IEnumerable<Contact> GetContactsAndPhoneNumbers()
{
var sql = @"
SELECT * FROM Contacts WHERE clientid=1
SELECT * FROM Phone where ContactId in (select ContactId FROM Contacts WHERE clientid=1)";
using (var connection = GetOpenConnection())
{
var mapped = connection.QueryMultiple(sql)
.Map<Contact,Phone, int> (
contact => contact.ContactID,
phone => phone.ContactID,
(contact, phones) => { contact.Phones = phones; }
);
return mapped;
}
}
チェックアウト https://www.tritac.com/blog/dappernet-by-example/ 次のようなことができます:
public class Shop {
public int? Id {get;set;}
public string Name {get;set;}
public string Url {get;set;}
public IList<Account> Accounts {get;set;}
}
public class Account {
public int? Id {get;set;}
public string Name {get;set;}
public string Address {get;set;}
public string Country {get;set;}
public int ShopId {get;set;}
}
var lookup = new Dictionary<int, Shop>()
conn.Query<Shop, Account, Shop>(@"
SELECT s.*, a.*
FROM Shop s
INNER JOIN Account a ON s.ShopId = a.ShopId
", (s, a) => {
Shop shop;
if (!lookup.TryGetValue(s.Id, out shop)) {
lookup.Add(s.Id, shop = s);
}
shop.Accounts.Add(a);
return shop;
},
).AsQueryable();
var resultList = lookup.Values;
これはdapper.netテストから取得しました: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#134
あなたの場合、複数の結果セットのクエリを使用する方がはるかに優れています(同様に簡単です)。これは、単に2つのselectステートメントを記述する必要があることを意味します。
これにより、オブジェクトは一意になり、複製されません。
これは非常に使いやすい再利用可能なソリューションです。 Andrews answer のわずかな変更です。
public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
this IDbConnection connection,
string sql,
Func<TParent, TParentKey> parentKeySelector,
Func<TParent, IList<TChild>> childSelector,
dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();
connection.Query<TParent, TChild, TParent>(
sql,
(parent, child) =>
{
if (!cache.ContainsKey(parentKeySelector(parent)))
{
cache.Add(parentKeySelector(parent), parent);
}
TParent cachedParent = cache[parentKeySelector(parent)];
IList<TChild> children = childSelector(cachedParent);
children.Add(child);
return cachedParent;
},
param as object, transaction, buffered, splitOn, commandTimeout, commandType);
return cache.Values;
}
使用例
public class Contact
{
public int ContactID { get; set; }
public string ContactName { get; set; }
public List<Phone> Phones { get; set; } // must be IList
public Contact()
{
this.Phones = new List<Phone>(); // POCO is responsible for instantiating child list
}
}
public class Phone
{
public int PhoneID { get; set; }
public int ContactID { get; set; } // foreign key
public string Number { get; set; }
public string Type { get; set; }
public bool IsActive { get; set; }
}
conn.QueryParentChild<Contact, Phone, int>(
"SELECT * FROM Contact LEFT OUTER JOIN Phone ON Contact.ContactID = Phone.ContactID",
contact => contact.ContactID,
contact => contact.Phones,
splitOn: "PhoneId");
Sam Saffron(およびMike Gleason)のアプローチに基づいて、複数の子と複数のレベルを可能にするソリューションがあります。
using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;
namespace TestMySQL.Helpers
{
public static class Extensions
{
public static IEnumerable<TFirst> MapChild<TFirst, TSecond, TKey>
(
this SqlMapper.GridReader reader,
List<TFirst> parent,
List<TSecond> child,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
var childMap = child
.GroupBy(secondKey)
.ToDictionary(g => g.Key, g => g.AsEnumerable());
foreach (var item in parent)
{
IEnumerable<TSecond> children;
if (childMap.TryGetValue(firstKey(item), out children))
{
addChildren(item, children);
}
}
return parent;
}
}
}
その後、関数の外部で読み取ることができます。
using (var multi = conn.QueryMultiple(sql))
{
var contactList = multi.Read<Contact>().ToList();
var phoneList = multi.Read<Phone>().ToList;
contactList = multi.MapChild
(
contactList,
phoneList,
contact => contact.Id,
phone => phone.ContactId,
(contact, phone) => {contact.Phone = phone;}
).ToList();
return contactList;
}
その後、同じ親オブジェクトを使用して、次の子オブジェクトに対してマップ関数を再度呼び出すことができます。また、map関数とは無関係に、親または子のreadステートメントに splits を実装することもできます。
「single to N」追加の拡張方法を次に示します
public static TFirst MapChildren<TFirst, TSecond, TKey>
(
this SqlMapper.GridReader reader,
TFirst parent,
IEnumerable<TSecond> children,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
if (parent == null || children == null || !children.Any())
{
return parent;
}
Dictionary<TKey, IEnumerable<TSecond>> childMap = children
.GroupBy(secondKey)
.ToDictionary(g => g.Key, g => g.AsEnumerable());
if (childMap.TryGetValue(firstKey(parent), out IEnumerable<TSecond> foundChildren))
{
addChildren(parent, foundChildren);
}
return parent;
}
この問題に対するソリューションを共有し、使用したアプローチについて建設的なフィードバックがあるかどうかを確認したかったのですか?
私が取り組んでいるプロジェクトには、最初に説明する必要があるいくつかの要件があります。
だから、私がやったことは、次のように元の行の列として単一のJSON文字列を返すことで、2番目からn番目のレベルの階層を処理するSQLを取得することです(他の列/プロパティなどを削除して =):
Id AttributeJson
4 [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]
その後、私のPOCOは以下のように構築されます。
public abstract class BaseEntity
{
[KeyAttribute]
public int Id { get; set; }
}
public class Client : BaseEntity
{
public List<ClientAttribute> Attributes{ get; set; }
}
public class ClientAttribute : BaseEntity
{
public string Name { get; set; }
public string Value { get; set; }
}
POCOがBaseEntityを継承する場所。 (説明のために、クライアントオブジェクトの "Attributes"プロパティで示されるように、かなり単純な単一レベルの階層を選択しました。
その後、POCO Client
を継承する次の「データクラス」をデータレイヤーに保持します。
internal class dataClient : Client
{
public string AttributeJson
{
set
{
Attributes = value.FromJson<List<ClientAttribute>>();
}
}
}
上記を見るとわかるように、SQLはdataClientクラスのAttributeJson
プロパティにマップされている「AttributeJson」という列を返しています。これには、JSONを継承されたAttributes
クラスのClient
プロパティにデシリアライズするセッターのみがあります。 dataClientクラスはデータアクセスレイヤーに対してinternal
であり、ClientProvider
(私のデータファクトリー)は元のクライアントPOCOを呼び出し元のアプリケーション/ライブラリに返します。
var clients = _conn.Get<dataClient>();
return clients.OfType<Client>().ToList();
Dapper.Contribを使用しており、Get<T>
を返す新しいIEnumerable<T>
メソッドを追加していることに注意してください。
このソリューションには、注意すべきことがいくつかあります。
JSONのシリアル化には明らかなパフォーマンスのトレードオフがあります-2つのList<T>
プロパティを持つ1050行に対してこれをベンチマークしました。各プロパティにはリストに2つのエンティティがあり、279msで動作します。 -これは物事のSQL側のZERO最適化でもあるので、そこで数ミリ秒シェービングできるはずです。
必要なList<T>
プロパティごとにJSONを構築するために追加のSQLクエリが必要であることを意味しますが、SQLを十分に知っており、ダイナミクス/リフレクションなどにそれほど流fluentではないので、これは私に適しています私は実際にフードの下で何が起こっているのかを理解しているので、物事をより細かく制御できるように感じます:-)
これよりも良い解決策があるかもしれませんが、あなたの考えを聞いて本当に感謝します-これは私がこれまでに思いついた解決策であり、これはこのプロジェクトの私のニーズに合っています(ただし、これは投稿の段階で実験的です)。
DataAccessLayerをストアドプロシージャに移動すると、これらのプロシージャは複数のリンクされた結果を返すことがよくあります(下の例を参照)。
まあ、私のアプローチはほとんど同じですが、多分もう少し快適です。
コードは次のようになります。
using ( var conn = GetConn() )
{
var res = await conn
.StoredProc<Person>( procName, procParams )
.Include<Book>( ( p, b ) => p.Books = b.Where( x => x.PersonId == p.Id ).ToList() )
.Include<Course>( ( p, c ) => p.Courses = c.Where( x => x.PersonId == p.Id ).ToList() )
.Include<Course, Mark>( ( c, m ) => c.Marks = m.Where( x => x.CourseId == c.Id ).ToList() )
.Execute();
}
拡張子:
public static class SqlExtensions
{
public static StoredProcMapper<T> StoredProc<T>( this SqlConnection conn, string procName, object procParams )
{
return StoredProcMapper<T>
.Create( conn )
.Call( procName, procParams );
}
}
マッパー:
public class StoredProcMapper<T>
{
public static StoredProcMapper<T> Create( SqlConnection conn )
{
return new StoredProcMapper<T>( conn );
}
private List<MergeInfo> _merges = new List<MergeInfo>();
public SqlConnection Connection { get; }
public string ProcName { get; private set; }
public object Parameters { get; private set; }
private StoredProcMapper( SqlConnection conn )
{
Connection = conn;
_merges.Add( new MergeInfo( typeof( T ) ) );
}
public StoredProcMapper<T> Call( object procName, object parameters )
{
ProcName = procName.ToString();
Parameters = parameters;
return this;
}
public StoredProcMapper<T> Include<TChild>( MergeDelegate<T, TChild> mapper )
{
return Include<T, TChild>( mapper );
}
public StoredProcMapper<T> Include<TParent, TChild>( MergeDelegate<TParent, TChild> mapper )
{
_merges.Add( new MergeInfo<TParent, TChild>( mapper ) );
return this;
}
public async Task<List<T>> Execute()
{
if ( string.IsNullOrEmpty( ProcName ) )
throw new Exception( $"Procedure name not specified! Please use '{nameof(Call)}' method before '{nameof( Execute )}'" );
var gridReader = await Connection.QueryMultipleAsync(
ProcName, Parameters, commandType: CommandType.StoredProcedure );
foreach ( var merge in _merges )
{
merge.Result = gridReader
.Read( merge.Type )
.ToList();
}
foreach ( var merge in _merges )
{
if ( merge.ParentType == null )
continue;
var parentMerge = _merges.FirstOrDefault( x => x.Type == merge.ParentType );
if ( parentMerge == null )
throw new Exception( $"Wrong parent type '{merge.ParentType.FullName}' for type '{merge.Type.FullName}'." );
foreach ( var parent in parentMerge.Result )
{
merge.Merge( parent, merge.Result );
}
}
return _merges
.First()
.Result
.Cast<T>()
.ToList();
}
private class MergeInfo
{
public Type Type { get; }
public Type ParentType { get; }
public IEnumerable Result { get; set; }
public MergeInfo( Type type, Type parentType = null )
{
Type = type;
ParentType = parentType;
}
public void Merge( object parent, IEnumerable children )
{
MergeInternal( parent, children );
}
public virtual void MergeInternal( object parent, IEnumerable children )
{
}
}
private class MergeInfo<TParent, TChild> : MergeInfo
{
public MergeDelegate<TParent, TChild> Action { get; }
public MergeInfo( MergeDelegate<TParent, TChild> mergeAction )
: base( typeof( TChild ), typeof( TParent ) )
{
Action = mergeAction;
}
public override void MergeInternal( object parent, IEnumerable children )
{
Action( (TParent)parent, children.Cast<TChild>() );
}
}
public delegate void MergeDelegate<TParent, TChild>( TParent parent, IEnumerable<TChild> children );
}
それだけですが、簡単なテストを行いたい場合は、ここにモデルと手順があります:
モデル:
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
public List<Course> Courses { get; set; }
public List<Book> Books { get; set; }
public override string ToString() => Name;
}
public class Book
{
public Guid Id { get; set; }
public Guid PersonId { get; set; }
public string Name { get; set; }
public override string ToString() => Name;
}
public class Course
{
public Guid Id { get; set; }
public Guid PersonId { get; set; }
public string Name { get; set; }
public List<Mark> Marks { get; set; }
public override string ToString() => Name;
}
public class Mark
{
public Guid Id { get; set; }
public Guid CourseId { get; set; }
public int Value { get; set; }
public override string ToString() => Value.ToString();
}
SP:
if exists (
select *
from sysobjects
where
id = object_id(N'dbo.MultiTest')
and ObjectProperty( id, N'IsProcedure' ) = 1 )
begin
drop procedure dbo.MultiTest
end
go
create procedure dbo.MultiTest
@PersonId UniqueIdentifier
as
begin
declare @tmpPersons table
(
Id UniqueIdentifier,
Name nvarchar(50)
);
declare @tmpBooks table
(
Id UniqueIdentifier,
PersonId UniqueIdentifier,
Name nvarchar(50)
)
declare @tmpCourses table
(
Id UniqueIdentifier,
PersonId UniqueIdentifier,
Name nvarchar(50)
)
declare @tmpMarks table
(
Id UniqueIdentifier,
CourseId UniqueIdentifier,
Value int
)
--------------------------------------------------
insert into @tmpPersons
values
( '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Иван' ),
( '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Василий' ),
( '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Алефтина' )
insert into @tmpBooks
values
( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Математика' ),
( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Физика' ),
( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Геометрия' ),
( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Биология' ),
( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Химия' ),
( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга История' ),
( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Литература' ),
( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Древне-шумерский диалект иврита' )
insert into @tmpCourses
values
( '30945b68-a6ef-4da8-9a35-d3b2845e7de3', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Математика' ),
( '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Физика' ),
( '92bbefd1-9fec-4dc7-bb58-986eadb105c8', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Геометрия' ),
( '923a2f0c-c5c7-4394-847c-c5028fe14711', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Биология' ),
( 'ace50388-eb05-4c46-82a9-5836cf0c988c', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Химия' ),
( '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'История' ),
( '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Литература' ),
( '73ac366d-c7c2-4480-9513-28c17967db1a', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Древне-шумерский диалект иврита' )
insert into @tmpMarks
values
( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 98 ),
( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 87 ),
( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 76 ),
( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 89 ),
( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 78 ),
( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 67 ),
( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 79 ),
( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 68 ),
( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 75 ),
----------
( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 198 ),
( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 187 ),
( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 176 ),
( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 189 ),
( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 178 ),
( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 167 ),
----------
( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 8 ),
( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 7 ),
( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 6 ),
( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 9 ),
( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 8 ),
( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 7 ),
( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 9 ),
( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 8 ),
( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 5 )
--------------------------------------------------
select * from @tmpPersons
select * from @tmpBooks
select * from @tmpCourses
select * from @tmpMarks
end
go