คำสั่งและ / หรือข้อกำหนดการสืบค้นที่ออกแบบมาอย่างดี


92

ฉันได้ค้นหาวิธีแก้ปัญหาที่ดีสำหรับปัญหาที่นำเสนอโดยรูปแบบพื้นที่เก็บข้อมูลทั่วไปมาระยะหนึ่งแล้ว (รายการวิธีการเพิ่มขึ้นสำหรับการสืบค้นเฉพาะทาง ฯลฯ ดู: http://ayende.com/blog/3955/repository- is-the-new-singleton )

ฉันชอบแนวคิดในการใช้การสืบค้นคำสั่งโดยเฉพาะอย่างยิ่งการใช้รูปแบบข้อมูลจำเพาะ อย่างไรก็ตามปัญหาของฉันเกี่ยวกับข้อกำหนดคือมันเกี่ยวข้องกับเกณฑ์ของการเลือกอย่างง่ายเท่านั้น (โดยทั่วไปคือ where clause) และไม่ได้จัดการกับปัญหาอื่น ๆ ของการสืบค้นเช่นการเข้าร่วมการจัดกลุ่มการเลือกส่วนย่อยหรือการฉายภาพเป็นต้น โดยพื้นฐานแล้วการค้นหาเพิ่มเติมทั้งหมดจะต้องผ่านการค้นหาจำนวนมากเพื่อให้ได้ชุดข้อมูลที่ถูกต้อง

(หมายเหตุ: ฉันใช้คำว่า "คำสั่ง" ในรูปแบบคำสั่งหรือที่เรียกว่าวัตถุแบบสอบถามฉันไม่ได้พูดถึงคำสั่งเช่นเดียวกับในการแยกคำสั่ง / คิวรีที่มีความแตกต่างระหว่างแบบสอบถามและคำสั่ง (อัปเดตลบ แทรก))

ดังนั้นฉันกำลังมองหาทางเลือกอื่นที่ห่อหุ้มข้อความค้นหาทั้งหมด แต่ก็ยังมีความยืดหยุ่นเพียงพอที่คุณจะไม่เพียง แต่แลกเปลี่ยนที่เก็บสปาเก็ตตี้เพื่อเพิ่มคลาสคำสั่ง

ฉันเคยใช้เช่น Linqspecs และในขณะที่ฉันพบคุณค่าบางอย่างในการกำหนดชื่อที่มีความหมายให้กับเกณฑ์การคัดเลือก แต่ก็ไม่เพียงพอ บางทีฉันอาจกำลังมองหาวิธีการแก้ปัญหาแบบผสมผสานที่ผสมผสานหลายวิธีเข้าด้วยกัน

ฉันกำลังมองหาวิธีแก้ปัญหาที่ผู้อื่นอาจพัฒนาขึ้นเพื่อแก้ไขปัญหานี้หรือแก้ไขปัญหาอื่น แต่ยังคงเป็นไปตามข้อกำหนดเหล่านี้ ในบทความที่เชื่อมโยง Ayende แนะนำให้ใช้บริบท nHibernate โดยตรง แต่ฉันรู้สึกว่าส่วนใหญ่ทำให้ชั้นธุรกิจของคุณซับซ้อนเพราะตอนนี้ต้องมีข้อมูลการสืบค้นด้วย

ฉันจะมอบรางวัลให้กับสิ่งนี้ทันทีที่ระยะเวลารอผ่านพ้นไป ดังนั้นโปรดทำให้โซลูชันของคุณมีค่าพร้อมคำอธิบายที่ดีและฉันจะเลือกทางออกที่ดีที่สุดและเพิ่มคะแนนให้กับนักวิ่ง

หมายเหตุ: ฉันกำลังมองหาสิ่งที่เป็นไปตาม ORM ไม่จำเป็นต้องเป็น EF หรือ nHibernate อย่างชัดเจน แต่สิ่งเหล่านี้เป็นสิ่งที่พบได้บ่อยที่สุดและเหมาะสมที่สุด หากสามารถปรับให้เข้ากับ ORM อื่น ๆ ได้อย่างง่ายดายนั่นจะเป็นโบนัส เข้ากันได้กับ Linq ก็จะดี

UPDATE: ฉันแปลกใจจริงๆที่ไม่มีคำแนะนำดีๆมากมายที่นี่ ดูเหมือนว่าผู้คนจะเป็น CQRS ทั้งหมดหรืออยู่ในค่าย Repository อย่างสมบูรณ์ แอพส่วนใหญ่ของฉันไม่ซับซ้อนพอที่จะรับประกัน CQRS (บางอย่างที่ผู้สนับสนุน CQRS ส่วนใหญ่บอกว่าคุณไม่ควรใช้เพื่อ)

UPDATE: ดูเหมือนจะมีความสับสนเล็กน้อยที่นี่ ฉันไม่ได้มองหาเทคโนโลยีการเข้าถึงข้อมูลใหม่ ๆ แต่เป็นอินเทอร์เฟซที่ออกแบบมาอย่างดีพอสมควรระหว่างธุรกิจและข้อมูล

ตามหลักการแล้วสิ่งที่ฉันกำลังมองหาคือการผสมข้ามระหว่างวัตถุแบบสอบถามรูปแบบข้อมูลจำเพาะและที่เก็บ ดังที่ฉันได้กล่าวไว้ข้างต้นรูปแบบข้อมูลจำเพาะจะเกี่ยวข้องกับด้านที่ของส่วนคำสั่งเท่านั้นไม่ใช่ด้านอื่น ๆ ของแบบสอบถามเช่นการรวมการเลือกย่อย ฯลฯ ที่เก็บจัดการกับข้อความค้นหาทั้งหมด แต่ไม่สามารถใช้งานได้ในขณะที่ . ออบเจ็กต์แบบสอบถามยังจัดการกับข้อความค้นหาทั้งหมด แต่ฉันไม่ต้องการเพียงแค่แทนที่ที่เก็บด้วยการระเบิดของวัตถุแบบสอบถาม


5
คำถามที่ยอดเยี่ยม ฉันก็อยากเห็นสิ่งที่ผู้คนมีประสบการณ์มากกว่าที่ฉันแนะนำเช่นกัน ฉันกำลังทำงานกับฐานรหัสในขณะที่ที่เก็บข้อมูลทั่วไปยังมีโอเวอร์โหลดสำหรับวัตถุคำสั่งหรือวัตถุแบบสอบถามโครงสร้างของใครคล้ายคลึงกับที่ Ayende อธิบายในบล็อกของเขา PS: สิ่งนี้อาจดึงดูดความสนใจของโปรแกรมเมอร์ด้วยเช่นกัน SE
Simon Whitehead

ทำไมไม่ใช้พื้นที่เก็บข้อมูลที่เปิดเผย IQueryable ถ้าคุณไม่สนใจการพึ่งพา LINQ? วิธีการทั่วไปคือที่เก็บข้อมูลทั่วไปจากนั้นเมื่อคุณต้องการตรรกะที่ใช้ซ้ำได้ข้างต้นคุณจะสร้างประเภทที่เก็บที่ได้รับด้วยวิธีการเพิ่มเติม
devdigital

@devdigital - การพึ่งพา Linq ไม่เหมือนกับการพึ่งพาการนำข้อมูลไปใช้ ฉันต้องการใช้ Linq กับออบเจ็กต์ดังนั้นฉันจึงสามารถจัดเรียงหรือใช้ฟังก์ชันชั้นธุรกิจอื่น ๆ ได้ แต่นั่นไม่ได้หมายความว่าฉันต้องการการอ้างอิงในการนำโมเดลข้อมูลไปใช้ สิ่งที่ฉันกำลังพูดถึงที่นี่คืออินเทอร์เฟซเลเยอร์ / ชั้น ตัวอย่างเช่นฉันต้องการที่จะสามารถเปลี่ยนข้อความค้นหาและไม่ต้องเปลี่ยนใน 200 ตำแหน่งซึ่งจะเกิดอะไรขึ้นถ้าคุณผลัก IQueryable เข้าสู่โมเดลธุรกิจโดยตรง
Erik Funkenbusch

1
@devdigital - ซึ่งโดยพื้นฐานแล้วเพียงแค่ย้ายปัญหาด้วยที่เก็บไปยังชั้นธุรกิจของคุณ คุณแค่สับปัญหาไปรอบ ๆ
Erik Funkenbusch

คำตอบ:


95

คำเตือน:เนื่องจากยังไม่มีคำตอบที่ดีฉันจึงตัดสินใจโพสต์ส่วนหนึ่งจากบล็อกโพสต์ที่ยอดเยี่ยมที่ฉันอ่านเมื่อสักครู่ที่ผ่านมาโดยคัดลอกเกือบทุกคำ คุณสามารถค้นหาโพสต์บล็อกเต็มรูปแบบที่นี่ นี่คือ:


เราสามารถกำหนดสองอินเทอร์เฟซต่อไปนี้:

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

IQuery<TResult>ระบุข้อความที่กำหนดแบบสอบถามที่เฉพาะเจาะจงกับข้อมูลที่จะส่งกลับมาใช้TResultประเภททั่วไป ด้วยอินเทอร์เฟซที่กำหนดไว้ก่อนหน้านี้เราสามารถกำหนดข้อความค้นหาดังนี้:

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}

คลาสนี้กำหนดการดำเนินการเคียวรีด้วยพารามิเตอร์สองตัวซึ่งจะทำให้เกิดอาร์เรย์ของอUserอบเจ็กต์ คลาสที่จัดการกับข้อความนี้สามารถกำหนดได้ดังนี้:

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly NorthwindUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
    }
}

ตอนนี้เราสามารถให้ผู้บริโภคพึ่งพาIQueryHandlerอินเทอร์เฟซทั่วไปได้:

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
    {
        this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.findUsersBySearchTextHandler.Handle(query);    
        return View(users);
    }
}

UserControllerทันทีแบบนี้จะช่วยให้เรามีความยืดหยุ่นมากเพราะตอนนี้เราสามารถตัดสินใจว่าจะฉีดเข้าไปใน เราสามารถฉีดการนำไปใช้งานที่แตกต่างไปจากเดิมอย่างสิ้นเชิงหรือการใช้งานที่ครอบคลุมการนำไปใช้งานจริงโดยไม่ต้องทำการเปลี่ยนแปลงUserController(และผู้บริโภคอื่น ๆ ทั้งหมดของอินเทอร์เฟซนั้น)

IQuery<TResult>อินเตอร์เฟซที่ช่วยให้เราสนับสนุนเวลารวบรวมเมื่อระบุหรือฉีดIQueryHandlersในรหัสของเรา เมื่อเราเปลี่ยนเป็นการFindUsersBySearchTextQueryส่งคืนUserInfo[]แทน (โดยการใช้งานIQuery<UserInfo[]>) UserControllerจะไม่สามารถรวบรวมได้เนื่องจากข้อ จำกัด ประเภททั่วไปในIQueryHandler<TQuery, TResult>จะไม่สามารถที่จะแมปไปFindUsersBySearchTextQueryUser[]

การฉีด IQueryHandlerอินเทอร์เฟซเข้าสู่ผู้บริโภคมีปัญหาที่ชัดเจนน้อยกว่าที่ยังต้องได้รับการแก้ไข จำนวนการพึ่งพาของผู้บริโภคของเราอาจใหญ่เกินไปและอาจนำไปสู่การสร้างตัวสร้างมากเกินไป - เมื่อตัวสร้างรับข้อโต้แย้งมากเกินไป จำนวนคิวรีที่คลาสรันสามารถเปลี่ยนแปลงได้บ่อยซึ่งจะต้องมีการเปลี่ยนแปลงจำนวนอาร์กิวเมนต์คอนสตรัคเตอร์อย่างต่อเนื่อง

เราสามารถแก้ไขปัญหาที่ต้องฉีดIQueryHandlersนามธรรมเพิ่มอีกชั้นมากเกินไป เราสร้างคนกลางที่อยู่ระหว่างผู้บริโภคและตัวจัดการแบบสอบถาม:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

IQueryProcessorเป็นอินเตอร์เฟซที่ไม่ทั่วไปด้วยวิธีการทั่วไปหนึ่ง ดังที่คุณเห็นในนิยามอินเตอร์เฟสIQueryProcessorขึ้นอยู่กับIQuery<TResult>อินเทอร์เฟซ IQueryProcessorนี้ช่วยให้เราได้รับการสนับสนุนรวบรวมเวลาในการบริโภคของเราที่ขึ้นอยู่กับ มาเขียนใหม่UserControllerเพื่อใช้ใหม่IQueryProcessor:

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Process(query);

        return this.View(users);
    }
}

UserControllerตอนนี้ขึ้นอยู่กับIQueryProcessorว่าสามารถจัดการทั้งหมดของคำสั่งของเรา UserController's SearchUsersวิธีการเรียกIQueryProcessor.Processวิธีการผ่านในวัตถุแบบสอบถามที่เริ่ม เนื่องจากFindUsersBySearchTextQueryใช้IQuery<User[]>อินเทอร์เฟซเราสามารถส่งต่อไปยังทั่วไปได้Execute<TResult>(IQuery<TResult> query)วิธีการได้ ด้วยการอนุมานประเภท C # คอมไพเลอร์สามารถกำหนดประเภททั่วไปได้และช่วยให้เราไม่ต้องระบุประเภทอย่างชัดเจน ชนิดของProcessวิธีการส่งคืนยังเป็นที่รู้จัก

ตอนนี้มันเป็นความรับผิดชอบของการดำเนินการที่จะหาที่เหมาะสมIQueryProcessor IQueryHandlerสิ่งนี้ต้องใช้การพิมพ์แบบไดนามิกและอาจเลือกใช้เฟรมเวิร์ก Dependency Injection และสามารถทำได้ด้วยโค้ดเพียงไม่กี่บรรทัด:

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Process<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

QueryProcessorระดับโครงสร้างที่เฉพาะเจาะจงIQueryHandler<TQuery, TResult>ประเภทขึ้นอยู่กับชนิดของอินสแตนซ์แบบสอบถามที่จัดมาให้ ประเภทนี้ใช้เพื่อขอให้คลาสคอนเทนเนอร์ที่ให้มาเพื่อรับอินสแตนซ์ของประเภทนั้น น่าเสียดายที่เราจำเป็นต้องเรียกHandleใช้เมธอดโดยใช้การสะท้อน (โดยใช้คีย์เวิร์ด dymamic C # 4.0 ในกรณีนี้) เนื่องจาก ณ จุดนี้เป็นไปไม่ได้ที่จะส่งอินสแตนซ์ตัวจัดการเนื่องจากTQueryอาร์กิวเมนต์ทั่วไปไม่สามารถใช้งานได้ในเวลาคอมไพล์ อย่างไรก็ตามเว้นแต่ไฟล์Handleวิธีการเปลี่ยนชื่อหรือได้รับอาร์กิวเมนต์อื่นการเรียกนี้จะไม่ล้มเหลวและหากคุณต้องการการเขียนแบบทดสอบหน่วยสำหรับคลาสนี้ทำได้ง่ายมาก การใช้เงาสะท้อนจะทำให้ลดลงเล็กน้อย แต่ก็ไม่มีอะไรน่าเป็นห่วง


เพื่อตอบข้อสงสัยของคุณ:

ดังนั้นฉันกำลังมองหาทางเลือกอื่นที่ห่อหุ้มข้อความค้นหาทั้งหมด แต่ก็ยังมีความยืดหยุ่นเพียงพอที่คุณจะไม่เพียง แต่แลกเปลี่ยนที่เก็บสปาเก็ตตี้เพื่อเพิ่มคลาสคำสั่ง

ผลที่ตามมาของการใช้การออกแบบนี้คือในระบบจะมีคลาสขนาดเล็กจำนวนมาก แต่การมีคลาสขนาดเล็ก / เน้นจำนวนมาก (ที่มีชื่อชัดเจน) เป็นสิ่งที่ดี วิธีนี้ดีกว่าอย่างเห็นได้ชัดเมื่อมีการโอเวอร์โหลดจำนวนมากพร้อมกับพารามิเตอร์ที่แตกต่างกันสำหรับวิธีการเดียวกันในที่เก็บเนื่องจากคุณสามารถจัดกลุ่มสิ่งเหล่านั้นในคลาสเคียวรีเดียวได้ ดังนั้นคุณยังคงได้รับคลาสเคียวรีน้อยกว่าเมธอดในที่เก็บ


2
ดูเหมือนว่าคุณจะได้รับรางวัล ฉันชอบแนวคิดฉันแค่หวังให้ใครบางคนนำเสนอสิ่งที่แตกต่างอย่างแท้จริง ยินดีด้วย.
Erik Funkenbusch

1
@FuriCuri ชั้นเดียวต้องการ 5 แบบสอบถามจริงหรือ? บางทีคุณอาจมองว่าเป็นชั้นเรียนที่มีความรับผิดชอบมากเกินไป หรืออีกวิธีหนึ่งหากมีการรวบรวมคำค้นหาบางทีอาจเป็นแบบสอบถามเดียว แน่นอนว่านี่เป็นเพียงคำแนะนำเท่านั้น
แซม

1
@stakx คุณพูดถูกจริงๆที่ในตัวอย่างเริ่มต้นของฉันTResultพารามิเตอร์ทั่วไปของIQueryอินเทอร์เฟซไม่มีประโยชน์ อย่างไรก็ตามในการตอบกลับที่อัปเดตของฉันTResultพารามิเตอร์จะถูกใช้โดยProcessวิธีการIQueryProcessorเพื่อแก้ไขIQueryHandlerที่รันไทม์
david.s

1
ฉันยังมีบล็อกที่มีการใช้งานที่คล้ายกันมากซึ่งทำให้ฉันคิดว่าฉันมาถูกทางแล้วนี่คือลิงค์ jupaol.blogspot.mx/2012/11/…และฉันใช้มันมาระยะหนึ่งแล้วในแอปพลิเคชัน PROD แต่ฉันมีปัญหากับวิธีนี้ การเชื่อมโยงและการนำการสืบค้นกลับมาใช้ใหม่สมมติว่าฉันมีแบบสอบถามเล็ก ๆ หลายรายการที่ต้องรวมกันเพื่อสร้างการสืบค้นที่ซับซ้อนมากขึ้นฉันเพิ่งทำซ้ำโค้ด แต่ฉันกำลังมองหาแนวทางที่ดีกว่าและสะอาดกว่า ความคิดใด ๆ ?
Jupaol

4
@Cemre ฉันสรุปการสืบค้นของฉันในวิธีการส่วนขยายที่ส่งคืนIQueryableและตรวจสอบให้แน่ใจว่าไม่ได้ระบุคอลเล็กชันจากนั้นQueryHandlerฉันเพิ่งเรียก / เชื่อมโยงการสืบค้น สิ่งนี้ทำให้ฉันมีความยืดหยุ่นในการทดสอบหน่วยการสืบค้นของฉันและเชื่อมโยงพวกเขา ฉันมีบริการแอปพลิเคชันอยู่ด้านบนของฉันQueryHandlerและผู้ควบคุมของฉันรับผิดชอบในการพูดคุยโดยตรงกับบริการแทนตัวจัดการ
Jupaol

4

วิธีการจัดการกับสิ่งนั้นง่ายและไม่เชื่อเรื่องพระเจ้า มุมมองของฉันสำหรับที่เก็บคือสิ่งนี้: หน้าที่ของที่เก็บคือการจัดหาโมเดลที่จำเป็นสำหรับบริบทให้แอปดังนั้นแอปจึงขอ repo ถึงสิ่งที่ต้องการ แต่ไม่ได้บอกวิธีรับ

ฉันจัดหาวิธีการจัดเก็บด้วยเกณฑ์ (ใช่สไตล์ DDD) ซึ่ง repo จะใช้ในการสร้างแบบสอบถาม (หรืออะไรก็ตามที่จำเป็น - อาจเป็นคำขอบริการเว็บ) การเข้าร่วมและกลุ่ม imho เป็นรายละเอียดของวิธีการไม่ใช่สิ่งที่และเกณฑ์ควรเป็นเพียงฐานในการสร้างประโยค where

Model = อ็อบเจ็กต์สุดท้ายหรือโครงสร้างข้อมูลที่แอปต้องการ

public class MyCriteria
{
   public Guid Id {get;set;}
   public string Name {get;set;}
    //etc
 }

 public interface Repository
  {
       MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria);
   }

คุณอาจใช้เกณฑ์ ORM (Nhibernate) ได้โดยตรงหากต้องการ การใช้งานที่เก็บควรรู้วิธีใช้เกณฑ์กับหน่วยเก็บข้อมูลพื้นฐานหรือ DAO

ฉันไม่ทราบโดเมนของคุณและข้อกำหนดของโมเดล แต่มันจะแปลกถ้าวิธีที่ดีที่สุดคือแอปจะสร้างแบบสอบถามเอง โมเดลเปลี่ยนแปลงไปมากจนคุณไม่สามารถกำหนดสิ่งที่มั่นคงได้?

โซลูชันนี้ต้องการรหัสเพิ่มเติมอย่างชัดเจน แต่ไม่ได้เชื่อมโยงส่วนที่เหลือกับ ORM หรืออะไรก็ตามที่คุณใช้เพื่อเข้าถึงที่เก็บข้อมูล พื้นที่เก็บข้อมูลทำหน้าที่เป็นส่วนหน้าและ IMO สะอาดและโค้ด 'การแปลเกณฑ์' สามารถใช้ซ้ำได้


สิ่งนี้ไม่ได้ช่วยแก้ปัญหาการเติบโตของพื้นที่เก็บข้อมูลและการมีรายการวิธีการที่เพิ่มมากขึ้นในการส่งคืนข้อมูลประเภทต่างๆ ฉันเข้าใจว่าคุณอาจไม่เห็นปัญหาในเรื่องนี้ (หลายคนไม่เห็น) แต่คนอื่น ๆ เห็นต่างออกไป (ฉันขอแนะนำให้อ่านบทความที่ฉันเชื่อมโยงไปมีคนอื่น ๆ อีกมากมายที่มีความคิดเห็นคล้ายกัน)
Erik Funkenbusch

1
ฉันพูดถึงมันเพราะเกณฑ์ทำให้ไม่จำเป็นต้องใช้วิธีการมากมาย แน่นอนว่าไม่ใช่ทั้งหมดที่ฉันพูดมากไม่ได้โดยไม่รู้อะไรเลยเกี่ยวกับสิ่งที่คุณต้องการ ฉันอยู่ภายใต้ความประทับใจแม้ว่าคุณต้องการสอบถามฐานข้อมูลโดยตรงดังนั้นอาจเป็นที่เก็บข้อมูล หากคุณต้องการทำงานที่น่ากลัวกับความสัมพันธ์เชิงสัมพันธ์ให้ไปหามันโดยตรงไม่จำเป็นต้องมีที่เก็บ และตามหมายเหตุมันน่ารำคาญที่มีคนอ้าง Ayende ในโพสต์นั้น ฉันไม่เห็นด้วยกับมันและฉันคิดว่านักพัฒนาหลายคนแค่ใช้รูปแบบในทางที่ผิด
MikeSW

1
อาจลดปัญหาได้บ้าง แต่ด้วยแอปพลิเคชั่นที่ใหญ่พอมันจะยังคงสร้างที่เก็บมอนสเตอร์ ฉันไม่เห็นด้วยกับโซลูชันของ Ayende ในการใช้ nHibernate โดยตรงในตรรกะหลัก แต่ฉันเห็นด้วยกับเขาเกี่ยวกับความไร้สาระของการเติบโตของพื้นที่เก็บข้อมูลที่ไม่สามารถควบคุมได้ ฉันไม่ต้องการสอบถามฐานข้อมูลโดยตรง แต่ฉันไม่เพียงต้องการย้ายปัญหาจากที่เก็บไปยังการระเบิดของวัตถุแบบสอบถาม
Erik Funkenbusch

2

ฉันได้ทำสิ่งนี้สนับสนุนสิ่งนี้และเลิกทำสิ่งนี้แล้ว

ปัญหาสำคัญคือไม่ว่าคุณจะทำอย่างไรสิ่งที่เป็นนามธรรมที่เพิ่มเข้ามาจะไม่ทำให้คุณเป็นอิสระ มันจะรั่วตามนิยาม โดยพื้นฐานแล้วคุณกำลังประดิษฐ์เลเยอร์ทั้งหมดเพียงเพื่อให้โค้ดของคุณดูน่ารัก ... แต่มันไม่ได้ลดการบำรุงรักษาปรับปรุงความสามารถในการอ่านหรือทำให้คุณได้รับโมเดลที่ไม่เชื่อเรื่องพระเจ้าใด ๆ

ส่วนที่สนุกคือคุณตอบคำถามของคุณเองเพื่อตอบสนองต่อคำตอบของ Olivier: "นี่เป็นการทำซ้ำฟังก์ชันการทำงานของ Linq โดยไม่ได้รับประโยชน์ทั้งหมดที่คุณจะได้รับจาก Linq"

ถามตัวเองว่าเป็นไปไม่ได้อย่างไร?


ฉันประสบปัญหาในการรวม Linq เข้ากับชั้นธุรกิจของคุณอย่างแน่นอน มันมีประสิทธิภาพมาก แต่เมื่อเราทำการเปลี่ยนแปลงโมเดลข้อมูลมันเป็นฝันร้าย สิ่งต่างๆได้รับการปรับปรุงด้วยที่เก็บเนื่องจากฉันสามารถทำการเปลี่ยนแปลงในสถานที่ที่แปลเป็นภาษาท้องถิ่นได้โดยไม่ส่งผลกระทบต่อชั้นธุรกิจมากนัก (นอกเหนือจากเวลาที่คุณต้องเปลี่ยนชั้นธุรกิจเพื่อรองรับการเปลี่ยนแปลงด้วย) แต่ที่เก็บกลายเป็นชั้นป่องเหล่านี้ซึ่งละเมิด SRP อย่างมาก ฉันเข้าใจประเด็นของคุณ แต่ก็ไม่ได้ช่วยแก้ปัญหาใด ๆ เช่นกัน
Erik Funkenbusch

หากชั้นข้อมูลของคุณใช้ LINQ และการเปลี่ยนแปลงโมเดลข้อมูลจำเป็นต้องมีการเปลี่ยนแปลงในชั้นธุรกิจของคุณ ... คุณจะจัดเลเยอร์ไม่ถูกต้อง
สตู

ฉันคิดว่าคุณกำลังบอกว่าคุณไม่ได้เพิ่มเลเยอร์นั้นแล้ว เมื่อคุณพูดว่าสิ่งที่เป็นนามธรรมที่เพิ่มเข้ามานั้นไม่ได้ประโยชน์อะไรเลยนั่นแสดงว่าคุณเห็นด้วยกับ Ayende เกี่ยวกับการส่งเซสชัน nHibernate (หรือบริบท EF) ไปยังชั้นธุรกิจของคุณโดยตรง
Erik Funkenbusch

1

คุณสามารถใช้อินเทอร์เฟซที่คล่องแคล่ว แนวคิดพื้นฐานคือวิธีการของคลาสจะส่งคืนอินสแตนซ์ปัจจุบันคลาสนี้หลังจากดำเนินการบางอย่างแล้ว สิ่งนี้ช่วยให้คุณสามารถเชื่อมโยงการเรียกวิธีการ

ด้วยการสร้างลำดับชั้นของคลาสที่เหมาะสมคุณสามารถสร้างโฟลว์ตรรกะของวิธีการที่สามารถเข้าถึงได้

public class FinalQuery
{
    protected string _table;
    protected string[] _selectFields;
    protected string _where;
    protected string[] _groupBy;
    protected string _having;
    protected string[] _orderByDescending;
    protected string[] _orderBy;

    protected FinalQuery()
    {
    }

    public override string ToString()
    {
        var sb = new StringBuilder("SELECT ");
        AppendFields(sb, _selectFields);
        sb.AppendLine();

        sb.Append("FROM ");
        sb.Append("[").Append(_table).AppendLine("]");

        if (_where != null) {
            sb.Append("WHERE").AppendLine(_where);
        }

        if (_groupBy != null) {
            sb.Append("GROUP BY ");
            AppendFields(sb, _groupBy);
            sb.AppendLine();
        }

        if (_having != null) {
            sb.Append("HAVING").AppendLine(_having);
        }

        if (_orderBy != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderBy);
            sb.AppendLine();
        } else if (_orderByDescending != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderByDescending);
            sb.Append(" DESC").AppendLine();
        }

        return sb.ToString();
    }

    private static void AppendFields(StringBuilder sb, string[] fields)
    {
        foreach (string field in fields) {
            sb.Append(field).Append(", ");
        }
        sb.Length -= 2;
    }
}

public class GroupedQuery : FinalQuery
{
    protected GroupedQuery()
    {
    }

    public GroupedQuery Having(string condition)
    {
        if (_groupBy == null) {
            throw new InvalidOperationException("HAVING clause without GROUP BY clause");
        }
        if (_having == null) {
            _having = " (" + condition + ")";
        } else {
            _having += " AND (" + condition + ")";
        }
        return this;
    }

    public FinalQuery OrderBy(params string[] fields)
    {
        _orderBy = fields;
        return this;
    }

    public FinalQuery OrderByDescending(params string[] fields)
    {
        _orderByDescending = fields;
        return this;
    }
}

public class Query : GroupedQuery
{
    public Query(string table, params string[] selectFields)
    {
        _table = table;
        _selectFields = selectFields;
    }

    public Query Where(string condition)
    {
        if (_where == null) {
            _where = " (" + condition + ")";
        } else {
            _where += " AND (" + condition + ")";
        }
        return this;
    }

    public GroupedQuery GroupBy(params string[] fields)
    {
        _groupBy = fields;
        return this;
    }
}

คุณจะเรียกแบบนี้

string query = new Query("myTable", "name", "SUM(amount) AS total")
    .Where("name LIKE 'A%'")
    .GroupBy("name")
    .Having("COUNT(*) > 2")
    .OrderBy("name")
    .ToString();

คุณสามารถสร้างอินสแตนซ์ใหม่ของQueryไฟล์. คลาสอื่น ๆ มีตัวสร้างที่ได้รับการป้องกัน จุดสำคัญของลำดับชั้นคือการ "ปิดใช้งาน" เมธอด ตัวอย่างเช่นGroupByวิธีการส่งคืนGroupedQueryซึ่งเป็นคลาสฐานของQueryและไม่มีWhereเมธอด (ซึ่งเป็นวิธีการที่ประกาศในQuery) ดังนั้นจึงเป็นไปไม่ได้ที่จะเรียกWhereหลังจากGroupByหลังจาก

อย่างไรก็ตามมันไม่สมบูรณ์แบบ ด้วยลำดับชั้นของคลาสนี้คุณสามารถซ่อนสมาชิกได้อย่างต่อเนื่อง แต่ไม่สามารถแสดงสมาชิกใหม่ได้ ดังนั้นจึงHavingแสดงข้อยกเว้นเมื่อมีการเรียกใช้ก่อนหน้านี้GroupByนี้

โปรดทราบว่าสามารถโทรได้Whereหลายครั้ง สิ่งนี้จะเพิ่มเงื่อนไขใหม่พร้อมกับเงื่อนไขANDที่มีอยู่ ทำให้ง่ายต่อการสร้างตัวกรองโดยใช้โปรแกรมจากเงื่อนไขเดียว เช่นเดียวกันกับHaving .

params string[] fieldsวิธีการที่ยอมรับรายการฟิลด์มีพารามิเตอร์ ช่วยให้คุณสามารถส่งผ่านชื่อเขตข้อมูลเดียวหรืออาร์เรย์สตริง


อินเทอร์เฟซที่คล่องแคล่วมีความยืดหยุ่นมากและไม่จำเป็นต้องให้คุณสร้างเมธอดจำนวนมากเกินพิกัดด้วยชุดพารามิเตอร์ที่แตกต่างกัน ตัวอย่างของฉันใช้งานได้กับสตริงอย่างไรก็ตามวิธีนี้สามารถขยายไปยังประเภทอื่นได้ คุณยังสามารถประกาศวิธีการที่กำหนดไว้ล่วงหน้าสำหรับกรณีพิเศษหรือวิธีการยอมรับประเภทที่กำหนดเอง นอกจากนี้คุณยังสามารถเพิ่มวิธีการเช่นหรือExecuteReader ExceuteScalar<T>ซึ่งจะช่วยให้คุณสามารถกำหนดแบบสอบถามเช่นนี้ได้

var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
    .Where(new CurrentMonthCondition())
    .Where(new DivisionCondition{ DivisionType = DivisionType.Production})
    .OrderBy(new StandardMonthlyReportSorting())
    .ExecuteReader();

แม้แต่คำสั่ง SQL ที่สร้างด้วยวิธีนี้ก็สามารถมีพารามิเตอร์คำสั่งและหลีกเลี่ยงปัญหาการแทรก SQL และในขณะเดียวกันก็อนุญาตให้แคชคำสั่งโดยเซิร์ฟเวอร์ฐานข้อมูล นี่ไม่ใช่การแทนที่ O / R-mapper แต่สามารถช่วยได้ในสถานการณ์ที่คุณจะสร้างคำสั่งโดยใช้การต่อสตริงแบบธรรมดา


3
อืม .. น่าสนใจ แต่โซลูชันของคุณดูเหมือนจะมีปัญหากับความเป็นไปได้ของ SQL Injection และไม่ได้สร้างคำสั่งที่เตรียมไว้สำหรับการประมวลผลล่วงหน้า (จึงทำงานได้ช้ากว่า) มันอาจจะดัดแปลงเพื่อแก้ไขปัญหาเหล่านั้นได้ แต่แล้วเราก็ติดอยู่กับผลลัพธ์ของชุดข้อมูลที่ไม่ใช่ประเภทปลอดภัยและสิ่งที่ไม่เป็นเช่นนั้น ฉันต้องการโซลูชันที่ใช้ ORM และบางทีฉันควรระบุอย่างชัดเจน นี่เป็นการทำซ้ำฟังก์ชันการทำงานของ Linq โดยไม่ได้รับประโยชน์ทั้งหมดที่คุณได้รับจาก Linq
Erik Funkenbusch

ฉันตระหนักถึงปัญหาเหล่านี้ นี่เป็นเพียงวิธีแก้ปัญหาที่รวดเร็วและสกปรกซึ่งแสดงให้เห็นว่าสามารถสร้างอินเทอร์เฟซที่คล่องแคล่วได้อย่างไร ในการแก้ปัญหาในโลกแห่งความเป็นจริงคุณอาจ“ อบ” แนวทางที่มีอยู่ของคุณให้กลายเป็นอินเทอร์เฟซที่คล่องแคล่วซึ่งปรับให้เข้ากับความต้องการของคุณ
Olivier Jacot-Descombes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.