잘 설계된 쿼리 명령 및 / 또는 사양
나는 전형적인 리포지토리 패턴 (특화된 질의를위한 메소드의 증가하는 목록 등)이 제시하는 문제에 대한 좋은 해결책을 꽤 오랫동안 찾고 있었다. http://ayende.com/blog/3955/repository- is-the-new-singleton ).
특히 사양 패턴을 사용하여 명령 쿼리를 사용하는 아이디어가 정말 마음에 듭니다. 그러나 사양에 대한 내 문제는 단순 선택 기준 (기본적으로 where 절)에만 관련되며 조인, 그룹화, 하위 집합 선택 또는 투영 등과 같은 쿼리의 다른 문제는 처리하지 않는다는 것입니다. 기본적으로 올바른 데이터 집합을 얻기 위해 많은 쿼리가 거쳐야하는 모든 추가 작업이 있습니다.
(참고 : 쿼리 개체라고도하는 명령 패턴에서 "명령"이라는 용어를 사용합니다. 쿼리와 명령 (업데이트, 삭제, 삭제)이 구분되는 명령 / 쿼리 분리에서 명령에 대해 말하는 것이 아닙니다. 끼워 넣다))
그래서 저는 전체 쿼리를 캡슐화하는 대안을 찾고 있지만, 명령 클래스의 폭발적인 증가를 위해 스파게티 리포지토리를 교체하는 것이 아니라 여전히 충분히 유연합니다.
예를 들어 Linqspecs를 사용했으며 선택 기준에 의미있는 이름을 할당 할 수 있다는 점에서 가치를 발견했지만 충분하지 않습니다. 아마도 여러 접근 방식을 결합한 혼합 솔루션을 찾고 있습니다.
다른 사람들이이 문제를 해결하거나 다른 문제를 해결하기 위해 개발했을 수있는 솔루션을 찾고 있지만 여전히 이러한 요구 사항을 충족합니다. 링크 된 기사에서 Ayende는 nHibernate 컨텍스트를 직접 사용하도록 제안했지만 이제 쿼리 정보도 포함해야하기 때문에 비즈니스 계층이 크게 복잡해집니다.
대기 기간이 지나는대로 현상금을 지급하겠습니다. 따라서 좋은 설명과 함께 귀하의 솔루션을 바운티에 합당하게 만드십시오. 최선의 솔루션을 선택하고 준우승자를 찬성하겠습니다.
참고 : ORM 기반의 것을 찾고 있습니다. 명시 적으로 EF 또는 nHibernate 일 필요는 없지만 가장 일반적이며 가장 적합합니다. 다른 ORM에 쉽게 적용 할 수 있다면 보너스가 될 것입니다. Linq 호환도 좋을 것입니다.
업데이트 : 여기에 좋은 제안이 많지 않다는 사실에 정말 놀랐습니다. 사람들은 완전히 CQRS이거나 완전히 Repository 캠프에있는 것 같습니다. 내 앱의 대부분은 CQRS를 보증 할만큼 충분히 복잡하지 않습니다 (대부분의 CQRS 옹호자들은이를 사용해서는 안된다고 쉽게 말합니다).
업데이트 : 여기에 약간의 혼란이있는 것 같습니다. 저는 새로운 데이터 액세스 기술을 찾고있는 것이 아니라 비즈니스와 데이터 간의 합리적으로 잘 설계된 인터페이스를 찾고 있습니다.
이상적으로 내가 찾고있는 것은 쿼리 개체, 사양 패턴 및 저장소 간의 일종의 교차입니다. 위에서 말했듯이 사양 패턴은 조인, 하위 선택 등과 같은 쿼리의 다른 측면이 아닌 where 절 측면 만 처리합니다. 리포지토리는 전체 쿼리를 처리하지만 잠시 후 손을 뗍니다. . 쿼리 개체도 전체 쿼리를 처리하지만 단순히 리포지토리를 쿼리 개체의 폭발로 바꾸고 싶지는 않습니다.
면책 조항 : 아직 훌륭한 답변이 없기 때문에 얼마 전에 읽은 훌륭한 블로그 게시물의 일부를 거의 그대로 복사하기로 결정했습니다. 여기 에서 전체 블로그 게시물을 찾을 수 있습니다 . 그래서 여기 있습니다 :
다음 두 가지 인터페이스를 정의 할 수 있습니다.
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>
매핑 할 수 없습니다 FindUsersBySearchTextQuery
에 User[]
.
IQueryHandler
그러나 소비자에게 인터페이스를 주입하는 것은 여전히 해결해야 할 몇 가지 덜 분명한 문제를 가지고 있습니다. 소비자의 종속성 수가 너무 커져 생성자가 너무 많은 인수를 사용하는 경우 생성자 과잉 주입으로 이어질 수 있습니다. 클래스가 실행하는 쿼리 수는 자주 변경 될 수 있으므로 생성자 인수 수를 지속적으로 변경해야합니다.
IQueryHandlers
추가 추상화 계층으로 너무 많이 주입해야하는 문제를 해결할 수 있습니다 . 소비자와 쿼리 처리기 사이에있는 중재자를 만듭니다.
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
은 IQueryProcessor
하나 개의 일반적인 방법이 아닌 일반적인 인터페이스입니다. 인터페이스 정의에서 볼 수 있듯이 인터페이스에 IQueryProcessor
따라 다릅니다 IQuery<TResult>
. 이를 통해 .NET Framework에 의존하는 소비자에서 컴파일 시간을 지원할 수 있습니다 IQueryProcessor
. UserController
new를 사용 하도록를 다시 작성해 보겠습니다 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
의 SearchUsers
방법은 호출 IQueryProcessor.Process
초기화 된 질의 객체를 전달 방법. 인터페이스를 FindUsersBySearchTextQuery
구현 하므로 IQuery<User[]>
일반 Execute<TResult>(IQuery<TResult> query)
메서드에 전달할 수 있습니다 . C # 유형 추론 덕분에 컴파일러는 제네릭 유형을 결정할 수 있으므로 명시 적으로 유형을 명시 할 필요가 없습니다. Process
메서드 의 반환 유형 도 알려져 있습니다.
이제의 구현 IQueryProcessor
에서 올바른을 찾는 책임이 IQueryHandler
있습니다. 이를 위해서는 약간의 동적 타이핑과 선택적으로 의존성 주입 프레임 워크의 사용이 필요하며 몇 줄의 코드로 모두 수행 할 수 있습니다.
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
리플렉션을 사용하여 메서드 를 호출해야합니다 (이 경우 C # 4.0 동적 키워드 TQuery
사용). 컴파일 타임에 제네릭 인수를 사용할 수 없기 때문에이 시점에서 핸들러 인스턴스를 캐스팅 할 수 없기 때문 입니다. 그러나 Handle
메서드의 이름을 바꾸거나 다른 인수를 가져 오지 않는 한이 호출은 실패하지 않으며 원하는 경우이 클래스에 대한 단위 테스트를 작성하는 것이 매우 쉽습니다. 리플렉션을 사용하면 약간의 하락이 발생하지만 실제로 걱정할 필요는 없습니다.
우려 사항 중 하나에 대답하려면 :
그래서 저는 전체 쿼리를 캡슐화하는 대안을 찾고 있지만, 명령 클래스의 폭발적인 증가를 위해 스파게티 리포지토리를 교체하는 것이 아니라 여전히 충분히 유연합니다.
이 디자인을 사용한 결과는 시스템에 작은 클래스가 많이 있지만 작은 / 집중된 클래스 (명확한 이름 포함)가 많은 것은 좋은 일입니다. 이 접근 방식은 하나의 쿼리 클래스로 그룹화 할 수 있으므로 저장소의 동일한 메서드에 대해 다른 매개 변수를 사용하는 많은 오버로드를 갖는 것보다 훨씬 낫습니다. 따라서 여전히 저장소의 메서드보다 쿼리 클래스가 훨씬 적습니다.
이를 처리하는 내 방식은 실제로 단순하고 ORM에 구애받지 않습니다. 저장소에 대한 내 견해는 다음과 같습니다. 저장소의 역할은 컨텍스트에 필요한 모델을 앱에 제공하는 것이므로 앱은 원하는 것을 저장소에 요청 하지만 가져 오는 방법 을 알려주지 않습니다 .
리포지토리 메서드에 Criteria (예, DDD 스타일)를 제공합니다. 이는 리포지토리에서 쿼리를 생성하는 데 사용할 것입니다 (또는 필요한 것은 무엇이든-웹 서비스 요청 일 수 있음). 조인 및 그룹 imho는 방법에 대한 세부 사항이며, 무엇과 기준이 where 절을 작성하는 기본이되어야합니다.
모델 = 앱에 필요한 최종 개체 또는 데이터 구조.
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는 깨끗하며 '기준 변환'코드를 재사용 할 수 있습니다.
나는 이것을하고 이것을 지원하고 이것을 취소했다.
주요 문제는 이것입니다. 어떻게하든 추가 된 추상화는 독립성을 얻지 못합니다. 정의에 따라 누출됩니다. 본질적으로 코드를 귀엽게 보이게하기 위해 전체 레이어를 발명하는 것입니다.하지만 유지 관리를 줄이거 나 가독성을 높이거나 어떤 유형의 모델 불가지론도 얻지는 못합니다.
재미있는 부분은 Olivier의 응답에 대한 자신의 질문에 답했다는 것입니다. "이것은 Linq에서 얻는 모든 이점없이 Linq의 기능을 본질적으로 복제하는 것입니다."
스스로에게 물어보십시오. 어떻게 그럴 수 없습니까?
유창한 인터페이스를 사용할 수 있습니다. 기본 아이디어는 클래스의 메서드가 어떤 작업을 수행 한 후 바로이 클래스의 현재 인스턴스를 반환한다는 것입니다. 이를 통해 메서드 호출을 연결할 수 있습니다.
적절한 클래스 계층을 만들면 액세스 가능한 메서드의 논리적 흐름을 만들 수 있습니다.
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;
}
}
You would call it like this
string query = new Query("myTable", "name", "SUM(amount) AS total")
.Where("name LIKE 'A%'")
.GroupBy("name")
.Having("COUNT(*) > 2")
.OrderBy("name")
.ToString();
You can only create a new instance of Query
. The other classes have a protected constructor. The point of the hierarchy is to "disable" methods. For instance, the GroupBy
method returns a GroupedQuery
which is the base class of Query
and does not have a Where
method (the where method is declared in Query
). Therefore it is not possible to call Where
after GroupBy
.
It is however not perfect. With this class hierarchy you can successively hide members, but not show new ones. Therefore Having
throws an exception when it is called before GroupBy
.
Note that it is possible to call Where
several times. This adds new conditions with an AND
to the existing conditions. This makes it easier to construct filters programmatically from single conditions. The same is possible with Having
.
The methods accepting field lists have a parameter params string[] fields
. It allows you to either pass single field names or a string array.
Fluent interfaces are very flexible and do not require you to create a lot of overloads of methods with different combinations of parameters. My example works with strings, however the approach can be extended to other types. You could also declare predefined methods for special cases or methods accepting custom types. You could also add methods like ExecuteReader
or ExceuteScalar<T>
. This would allow you to define queries like this
var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
.Where(new CurrentMonthCondition())
.Where(new DivisionCondition{ DivisionType = DivisionType.Production})
.OrderBy(new StandardMonthlyReportSorting())
.ExecuteReader();
Even SQL commands constructed this way can have command parameters and thus avoid SQL injection problems and at the same time allow commands to be cached by the database server. This is not a replacement for an O/R-mapper but can help in situations where you would create the commands using simple string concatenation otherwise.
참고URL : https://stackoverflow.com/questions/14420276/well-designed-query-commands-and-or-specifications
'code' 카테고리의 다른 글
하위 폴더를 가리 키도록 git 하위 모듈을 변경하는 방법은 무엇입니까? (0) | 2020.09.10 |
---|---|
memmove가 memcpy보다 빠른 이유는 무엇입니까? (0) | 2020.09.10 |
SQL Server 프로파일 러의 "감사 로그 아웃"이란 무엇입니까? (0) | 2020.09.10 |
Java에서 기본 문자 세트 / 인코딩을 찾는 방법은 무엇입니까? (0) | 2020.09.10 |
Java 로깅 프레임 워크의 마커는 무엇이며이를 사용하는 이유는 무엇입니까? (0) | 2020.09.10 |