Multi-Mapper để tạo hệ thống phân cấp đối tượng


82

Tôi đã chơi với điều này một chút, vì nó có vẻ giống như ví dụ về bài đăng / người dùng được ghi lại , nhưng nó hơi khác và không phù hợp với tôi.

Giả sử thiết lập đơn giản sau (một liên hệ có nhiều số điện thoại):

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; }
}

Tôi muốn kết thúc với một cái gì đó trả về một Liên hệ với nhiều đối tượng Điện thoại. Theo cách đó, nếu tôi có 2 địa chỉ liên hệ, mỗi liên hệ với 2 điện thoại, thì SQL của tôi sẽ trả về một liên hệ trong số đó do kết quả được đặt với tổng số hàng là 4. Sau đó, Dapper sẽ bật ra 2 đối tượng liên lạc với hai điện thoại mỗi đối tượng.

Đây là SQL trong thủ tục được lưu trữ:

SELECT *
FROM Contacts
    LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1

Tôi đã thử điều này, nhưng cuối cùng chỉ có 4 Tuples (điều đó không sao, nhưng không phải là những gì tôi hy vọng ... nó chỉ có nghĩa là tôi vẫn phải chuẩn hóa lại kết quả):

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);

và khi tôi thử một phương pháp khác (bên dưới), tôi nhận được một ngoại lệ là "Không thể ép kiểu đối tượng 'System.Int32' thành '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);

Tôi chỉ làm điều gì đó sai? Nó có vẻ giống như ví dụ về bài đăng / chủ sở hữu, ngoại trừ việc tôi sẽ chuyển từ cha mẹ sang con thay vì từ con đến cha mẹ.

Cảm ơn trước

Câu trả lời:


69

Bạn không làm gì sai, đó không phải là cách API được thiết kế. Tất cả các QueryAPI sẽ luôn trả về một đối tượng trên mỗi hàng cơ sở dữ liệu.

Vì vậy, điều này hoạt động tốt trên nhiều -> một hướng, nhưng kém hơn đối với một -> nhiều bản đồ đa.

Có 2 vấn đề ở đây:

  1. Nếu chúng tôi giới thiệu một trình liên kết tích hợp hoạt động với truy vấn của bạn, chúng tôi dự kiến ​​sẽ "loại bỏ" dữ liệu trùng lặp. (Danh bạ. * Được sao chép trong truy vấn của bạn)

  2. Nếu chúng ta thiết kế nó để làm việc với một -> nhiều cặp, chúng ta sẽ cần một số loại bản đồ nhận dạng. Điều này làm tăng thêm sự phức tạp.


Lấy ví dụ: truy vấn này hiệu quả nếu bạn chỉ cần kéo một số lượng bản ghi hạn chế, nếu bạn đẩy điều này lên đến một triệu nội dung sẽ phức tạp hơn, khiến bạn cần phải phát trực tuyến và không thể tải mọi thứ vào bộ nhớ:

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)"

Những gì bạn có thể làm là mở rộng GridReaderđể cho phép ánh xạ lại:

var mapped = cnn.QueryMultiple(sql)
   .Map<Contact,Phone, int>
    (
       contact => contact.ContactID, 
       phone => phone.ContactID,
       (contact, phones) => { contact.Phones = phones };  
    );

Giả sử bạn mở rộng GridReader của mình và bằng trình ánh xạ:

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;
}

Vì điều này là một chút khó khăn và phức tạp, hãy lưu ý. Tôi không nghiêng về việc đưa điều này vào cốt lõi.


Rất tuyệt. Thứ này có khá nhiều sức mạnh ... Tôi đoán nó chỉ quen với cách sử dụng nó. Tôi sẽ xem xét khối lượng truy vấn của mình và xem bộ kết quả lớn đến mức nào và xem liệu chúng ta có đủ khả năng để có nhiều truy vấn và ánh xạ chúng với nhau hay không.
Jorin

@Jorin, tùy chọn khác của bạn sẽ là sắp xếp nhiều kết nối và kết hợp các kết quả. Nó phức tạp hơn một chút.
Sam Saffron

1
Tôi cũng sẽ thêm một cái khác sau if (childMap.TryGetvalue (..)) để tập hợp con được khởi tạo mặc định thành tập hợp rỗng thay vì NULL nếu không có mục con. Như thế này: else {addChildren (item, new TChild [] {}); }
Marius

1
@SamSaffron Tôi yêu Dapper. Cảm ơn bạn. Tôi có một câu hỏi mặc dù. Một-nhiều là một sự xuất hiện phổ biến trong các truy vấn SQL. Trong thiết kế, bạn đã lưu ý điều gì để người thực hiện sử dụng? Tôi muốn làm theo cách Dapper, nhưng hiện tại tôi đang làm theo cách SQL. Làm thế nào để tôi nghĩ về điều này đến từ SQL nơi Một bên thường là "trình điều khiển". Tại sao Bên nhiều lại có trong Dapper? Có phải điểm để chúng ta lấy đối tượng và thực hiện phân tích cú pháp sau thực tế? Cảm ơn vì thư viện tuyệt vời.
johnny

2
Đảm bảo rằng bạn đang sử dụng công cụ phù hợp cho công việc. Nếu bạn không có các yêu cầu lớn về hiệu suất cơ sở dữ liệu, hoặc chưa chuẩn hóa hệ thống của mình, bạn đã lãng phí hàng giờ hoặc có thể là vài ngày trong đời khi sử dụng Dapper.
Aluan Haddad

32

FYI - Tôi đã nhận được câu trả lời của Sam hoạt động bằng cách làm như sau:

Đầu tiên, tôi đã thêm một tệp lớp có tên "Extensions.cs". Tôi đã phải thay đổi từ khóa "this" thành "reader" ở hai nơi:

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;
        }
    }
}

Thứ hai, tôi đã thêm phương thức sau, sửa đổi tham số cuối cùng:

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;
    }
}

24

Hãy xem https://www.tritac.com/blog/dappernet-by-example/ Bạn có thể làm điều gì đó như sau:

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;

Tôi nhận được điều này từ các bài kiểm tra dapper.net: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343


2
Chà! Đối với tôi, tôi thấy đây là giải pháp dễ dàng nhất. Được cho, đối với một-> nhiều, (giả sử có hai bảng), tôi sẽ chọn hai lần. Tuy nhiên, trong trường hợp của tôi, tôi có một-> một-> nhiều và điều này hoạt động tuyệt vời. Bây giờ, nó mang lại rất nhiều dữ liệu dư thừa nhưng đối với trường hợp của tôi, sự dư thừa này tương đối nhỏ - tốt nhất là 10 hàng.
code5

Điều này hoạt động tốt cho hai cấp độ, nhưng sẽ phức tạp hơn khi bạn có nhiều hơn.
Samir Aguiar

1
Nếu không có dữ liệu con, mã (s, a) sẽ được gọi với a = null và Tài khoản sẽ chứa danh sách có mục nhập null thay vì trống. Bạn cần thêm "if (a! = Null)" trước "shop.Accounts.Add (a)"
Etienne Charland

12

Hỗ trợ nhiều bộ kết quả

Trong trường hợp của bạn, sẽ tốt hơn nhiều (và cũng dễ dàng hơn) nếu có một truy vấn tập hợp nhiều kết quả. Điều này đơn giản có nghĩa là bạn nên viết hai câu lệnh chọn:

  1. Một trả về danh bạ
  2. Và một trong đó trả về số điện thoại của họ

Bằng cách này, các đối tượng của bạn sẽ là duy nhất và không trùng lặp.


1
Trong khi các câu trả lời khác có thể thanh lịch theo cách riêng của chúng, tôi có xu hướng thích câu này vì mã dễ suy luận hơn. Tôi có thể xây dựng một hệ thống phân cấp sâu vài cấp với một số ít các câu lệnh chọn lọc và khoảng 30 dòng mã foreach / linq. Điều này có thể bị hỏng với các bộ kết quả lớn, nhưng may mắn là tôi chưa gặp vấn đề đó (chưa).
Sam Storie

10

Đây là một giải pháp có thể tái sử dụng khá dễ sử dụng. Nó là một sửa đổi nhỏ của câu trả lời của Andrews .

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;
}

Ví dụ sử dụng

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");

7

Dựa trên cách tiếp cận của Sam Saffron (và của Mike Gleason), đây là một giải pháp cho phép nhiều trẻ em và nhiều cấp độ.

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;
        }
    }
}

Sau đó, bạn có thể để nó đọc bên ngoài chức năng.

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;
}

Sau đó, hàm bản đồ có thể được gọi lại cho đối tượng con tiếp theo bằng cách sử dụng cùng một đối tượng mẹ. Bạn cũng có thể thực hiện phân tách trên các câu lệnh đọc cha hoặc con độc lập với hàm bản đồ.

Đây là một phương pháp mở rộng bổ sung 'đơn đến 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;
    }

2
Cảm ơn vì điều này - giải pháp tuyệt vời. đã loại bỏ câu lệnh if để thay vì không gọi addChilder trên không có con nào, hàm gọi có thể xử lý các giá trị null. Bằng cách đó, tôi có thể thêm các danh sách trống dễ làm việc hơn nhiều.
Mladen Mihajlovic

1
Đây là một giải pháp tuyệt vời. Tôi gặp một số vấn đề với "tìm kiếm động". Điều đó có thể được giải quyết với contactList = multi.MapChild <Contact, Phone, int> (/ * mã giống như trên đây * /
granadaCoder

4

Sau khi chúng tôi quyết định chuyển DataAccessLayer của mình sang các thủ tục được lưu trữ và các thủ tục này thường trả về nhiều kết quả được liên kết (ví dụ bên dưới).

Chà, cách tiếp cận của tôi gần như giống nhau, nhưng có thể thoải mái hơn một chút.

Đây là cách mã của bạn có thể trông như thế nào:

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();
}


Hãy phá vỡ nó ...

Sự mở rộng:

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 );
    }
}

Người vẽ bản đồ:

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 );
}

Đó là tất cả, nhưng nếu bạn muốn kiểm tra nhanh, đây là mô hình và quy trình dành cho bạn:

Mô hình:

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

1
Tôi không biết tại sao cách tiếp cận này không nhận được sự chú ý hoặc bình luận cho đến nay, nhưng tôi thấy nó rất thú vị và có cấu trúc logic. Cám ơn vì đã chia sẻ. Tôi nghĩ bạn có thể áp dụng cách tiếp cận này cho các hàm có giá trị bảng hoặc thậm chí cả các chuỗi SQL - chúng chỉ khác nhau về kiểu lệnh. Chỉ một số phần mở rộng / quá tải và điều này sẽ hoạt động cho tất cả các loại truy vấn phổ biến.
Grimm

để đảm bảo tôi đang đọc quyền này, điều này yêu cầu người dùng biết chính xác loại thứ tự mà thủ tục sẽ trả về kết quả, đúng không? Nếu bạn hoán đổi Ví dụ: Bao gồm <Sách> và Bao gồm <Khóa>, điều này sẽ xảy ra?
cubesnyc

@cubesnyc Tôi không nhớ nó có ném hay không, nhưng có, người dùng phải biết thứ tự
Sam Sch

2

Tôi muốn chia sẻ giải pháp của mình cho vấn đề này và xem liệu có ai có bất kỳ phản hồi mang tính xây dựng nào về phương pháp tôi đã sử dụng không?

Tôi có một số yêu cầu trong dự án mà tôi đang thực hiện mà tôi cần giải thích trước:

  1. Tôi phải giữ cho POCO của mình sạch sẽ nhất có thể vì các lớp này sẽ được chia sẻ công khai trong một trình bao bọc API.
  2. POCO của tôi nằm trong một Thư viện lớp riêng biệt vì yêu cầu trên
  3. Sẽ có nhiều cấp độ phân cấp đối tượng sẽ khác nhau tùy thuộc vào dữ liệu (vì vậy tôi không thể sử dụng Trình lập bản đồ loại chung hoặc tôi sẽ phải viết hàng tấn chúng để phục vụ cho tất cả các trường hợp có thể xảy ra)

Vì vậy, những gì tôi đã làm là để SQL xử lý thứ bậc thứ 2 - thứ n bằng cách trả về một chuỗi JSON Đơn dưới dạng một cột trên hàng ban đầu như sau ( loại bỏ các cột / thuộc tính khác, v.v. để minh họa ):

Id  AttributeJson
4   [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]

Sau đó, POCO của tôi được xây dựng như sau:

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; }
}

Nơi kế thừa của POCO từ BaseEntity. (Để minh họa, tôi đã chọn một hệ thống cấp duy nhất, khá đơn giản như được hiển thị bởi thuộc tính "Thuộc tính" của đối tượng khách hàng.)

Sau đó, tôi có trong Lớp dữ liệu của mình "Lớp dữ liệu" sau đây kế thừa từ POCO Client.

internal class dataClient : Client
{
    public string AttributeJson
    {
        set
        {
            Attributes = value.FromJson<List<ClientAttribute>>();
        }
    }
}

Như bạn có thể thấy ở trên, điều gì đang xảy ra là SQL đang trả về một cột có tên "AttributeJson" được ánh xạ tới thuộc tính AttributeJsontrong lớp dataClient. Điều này chỉ có một bộ định danh giúp mô tả JSON thành thuộc Attributestính trên Clientlớp kế thừa . Lớp dataClient là Lớp internaltruy cập dữ liệu và ClientProvider(nhà máy dữ liệu của tôi) trả về POCO của Ứng dụng khách ban đầu cho Ứng dụng / Thư viện đang gọi như sau:

var clients = _conn.Get<dataClient>();
return clients.OfType<Client>().ToList();

Lưu ý rằng tôi đang sử dụng Dapper.Contrib và đã thêm một Get<T>Phương thức mới trả vềIEnumerable<T>

Có một số điều cần lưu ý với giải pháp này:

  1. Có một sự đánh đổi hiệu suất rõ ràng với tuần tự hóa JSON - tôi đã đánh giá tiêu chuẩn này so với 1050 hàng với 2 List<T>thuộc tính phụ , mỗi thuộc tính có 2 thực thể trong danh sách và nó đạt tốc độ 279ms - điều này có thể chấp nhận được cho nhu cầu dự án của tôi - điều này cũng với KHÔNG tối ưu hóa về mặt SQL của mọi thứ, vì vậy tôi sẽ có thể cạo một vài mili giây ở đó.

  2. Điều đó có nghĩa là các truy vấn SQL bổ sung được yêu cầu để xây dựng JSON cho mỗi thuộc tính bắt buộc List<T>, nhưng một lần nữa, điều này phù hợp với tôi vì tôi biết SQL khá tốt và không thông thạo về động học / phản xạ, v.v. vì vậy theo cách này tôi cảm thấy như mình đã kiểm soát nhiều hơn mọi thứ vì tôi thực sự hiểu những gì đang xảy ra dưới mui xe :-)

Có thể có một giải pháp tốt hơn giải pháp này và nếu có, tôi thực sự đánh giá cao việc lắng nghe suy nghĩ của bạn - đây chỉ là giải pháp mà tôi đưa ra cho đến nay phù hợp với nhu cầu của tôi cho dự án này (mặc dù đây là giải pháp thử nghiệm ở giai đoạn đăng bài ).


Hay đấy. Bất kỳ cơ hội nào bạn có thể chia sẻ phần SQL?
WhiteRuski
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.