筛选具有多个流派的列表

记得当每部影片只有一个流派时,我们可以很容易实现快速过滤:在 GenreId 属性加入了 [QuickFilter] 特性即可。

让我们试着在 MovieColumns.cs 做类似修改:

[ColumnsScript("MovieDB.Movie")]
[BasedOnRow(typeof(Entities.MovieRow))]
public class MovieColumns
{
    //...
    [Width(200), GenreListFormatter, QuickFilter]
    public List<Int32> GenreList { get; set; }
}

只要在 Genres 中输入流派就会得到该错误:

Invalid Column GenreList

ListHandler 尝试使用 GenreList 字段过滤,但是在数据库中并没有这样的字段,因此我们得到这个错误。

事实上,LinkingSetRelation 会拦截此过滤器并把它转换为 EXISTS 子查询,但是列行为(list behaviors)还没有实现这样的功能,也许会在以后版本……

因此,我们现在就用某种方式来处理它。

声明 MovieListRequest 类型

因为我们打算做一些不规范的事,例如通过关联表(linking set table)的值来过滤时,我们需要防止 ListHandler 在 GenreList 属性中过滤自身。

我们可以使用一个访问者模式处理请求 条件(Criteria) 对象(它类似于表达式目录树)和处理 GenreList 自身,但这有点复杂。所以现在我会使用一个简单的方式。

让我们看一个含标准的 ListRequest 对象的子类,我们将在这里添加流派过滤器参数。在 MovieRepository.cs 文件旁边添加 MovieListRequest.cs 文件:

namespace MovieTutorial.MovieDB
{
    using Serenity.Services;
    using System.Collections.Generic;

    public class MovieListRequest : ListRequest
    {
        public List<int> Genres { get; set; }
    }
}

在列表请求对象中,我们添加一个 Genres 属性,它将保存我们想在影片中过滤的 流派(Genres) 选项。

为新请求类型修改 Repository/Endpoint

为使我们的列表处理程序(list handler)和服务使用新的列表请求类型,需要在几个地方做修改。

先从 MovieRepository.cs 开始:

public class MovieRepository
{
    //...
    public ListResponse<MyRow> List(IDbConnection connection, MovieListRequest request)
    {
        return new MyListHandler().Process(connection, request);
    }

    //...
    private class MyListHandler : ListRequestHandler<MyRow, MovieListRequest> { }
}

若要使用新类型,而不是 ListRequest,在 List 方法中,我们把 ListRequest 改为 MovieListRequest,并在 MyListHandler 中添加一个泛型参数。

在另一文件 MovieEndpoint.cs 中也做一些小修改,该类实际上是 web 服务:

public class MovieController : ServiceEndpoint
{
    //...
    public ListResponse<MyRow> List(IDbConnection connection, MovieListRequest request)
    {
        return new MyRepository().List(connection, request);
    }
}

现在是时候生成和转换模板,以使我们的 MovieListRequest 对象及相关服务方法能在客户端生效。

将快速过滤器移到流派参数

我们仍然有同样的错误,因为快速过滤器并不知道我们刚添加到列表的请求类型,还一直使用着 Criteria 参数。

需要拦截快速过滤项并将流派列表移到 MovieListRequest流派(Genres) 属性。

编辑 MovieGrid.ts

export class MovieGrid extends Serenity.EntityGrid<MovieRow, any> {

    //...
    protected getQuickFilters() {
        let items = super.getQuickFilters();

        var genreListFilter = Q.first(items, x =>
            x.field == MovieRow.Fields.GenreList);

        genreListFilter.handler = h => {
            var request = (h.request as MovieListRequest);
            var values = (h.widget as Serenity.LookupEditor).values;
            request.Genres = values.map(x => parseInt(x, 10));
            h.handled = true;
        };

        return items;
    }
}

getQuickFilters 是一个获取此网格列表的快速过滤器对象列表的方法。

默认情况下,网格列表枚举 MovieColumns.cs 中所有含 [QuickFilter] 特性的属性,并为其创建合适的快速过滤器对象。

我们从基类获取 QuickFilter 对象列表开始。

let items = super.getQuickFilters();

然后找到 GenreList 属性的快速过滤器对象:

var genreListFilter = Q.first(items, x =>
    x.field == MovieRow.Fields.GenreList);

实际上现在只有一个快速过滤器。

下一步是设置 handler 方法。 在提交到列表服务之前,快速过滤器对象读取编辑器值并将其应用到请求的 Criteria (如果有多个) 或 EqualityFilter (如果单个值)参数。

genreListFilter.handler = h => {

然后我们获得当前 ListRequest 引用:

var request = (h.request as MovieListRequest);

并读取检索编辑器(LookupEditor)中的当前值:

var values = (h.widget as Serenity.LookupEditor).values;

把该值设置到 request.Genres 属性:

request.Genres = values.map(x => parseInt(x, 10));

这是一个字符串列表的值,我们需要将它们转换为整数。

最后一步是设置 handled 为 true,要禁用快速过虑器对象的默认行为,因此它将不会使用自己设置的 CriteriaEqualityFilter

h.handled = true;

现在,我们将不再有 无效的列名 GenreList 的错误,但 Genres 过滤器还没有应用到服务器端。

在仓储(Repository)中处理流派的过滤

MovieRepository.cs 文件对 MyListHandler 做如下修改:

private class MyListHandler : ListRequestHandler<MyRow, MovieListRequest>
{
    protected override void ApplyFilters(SqlQuery query)
    {
        base.ApplyFilters(query);

        if (!Request.Genres.IsEmptyOrNull())
        {
            var mg = Entities.MovieGenresRow.Fields.As("mg");

            query.Where(Criteria.Exists(
                query.SubQuery()
                    .From(mg)
                    .Select("1")
                    .Where(
                        mg.MovieId == fld.MovieId &&
                        mg.GenreId.In(Request.Genres))
                    .ToString()));
        }
    }
}

ApplyFilters 是一个应用过滤器指定的 CriteriaEqualityFilter 请求参数表的方法。这里是应用自定义过滤器的好地方。

如果需要做任何过滤,我们首先要检查 Request.Genres 是否是 null 或空列表。

接下来,我们获得一个别名为 mg 的字段 MovieGenresRow 引用。

var mg = Entities.MovieGenresRow.Fields.As("mg");

这里需要说明一下,我们还没有覆盖 Serenity 实体系统。

让我们从还没有别名的 MovieGenresRow.Fields 开始:

var x = MovieGenresRow.Fields;
new SqlQuery()
  .From(x)
  .Select(x.MovieId)
  .Select(x.GenreId);

如果我们写类似上述的查询,它输出的 SQL 会是这样的:

SELECT t0.MovieId, t0.GenreId FROM MovieGenres t0

除非特别指出,Serenity 总是分配 t0 到行的主表。即使我们命名 MovieGenresRow.Fields 为变量 x,它的别名仍将是 t0

因为在编译时,x 不会存在并且 Serenity 已没有办法知道其变量的名称。Serenity 实体系统没有使用像 LINQ to SQL 或 Entity Framework 那样的表达式树。它使用非常简单的字符串/查询生成器。

所以,如果想要使用 x 作为别名,我们必须明确地声明:

var x = MovieGenresRow.Fields.As("x");
new SqlQuery()
  .From(x)
  .Select(x.MovieId)
  .Select(x.GenreId);

结果为:

SELECT x.MovieId, x.GenreId FROM MovieGenres x

MovieRow 实体的 MyListHandler 中,t0 已经被 MovieRow 字段使用。因此,为防止 MovieGenresRow 字段(名为 fld)的冲突,我需要把 MovieGenresRow 别名指定为 mg

var mg = Entities.MovieGenresRow.Fields.As("mg");

我想实现的是这样的一个查询(就像我们会使用纯SQL):

SELECT t0.MovieId, t0.Title, ... FROM Movies t0
WHERE EXISTS (
   SELECT 1 
   FROM MovieGenres mg 
   WHERE 
     mg.MovieId = t0.MovieId AND
     mg.GenreId IN (1, 3, 5, 7)
)

因此,我向 query 对象的 Where 方法添加 WHERE 过滤器,使用 EXISTS 条件:

query.Where(Criteria.Exists(

然后开始写子查询:

query.SubQuery()
    .From(mg)
    .Select("1")

为子查询添加 where 声明:

.Where(
    mg.MovieId == fld.MovieId &&
    mg.GenreId.In(Request.Genres))

其实这里 fld 包含 MovieRow 字段的别名 t0。

由于 Criteria.Exists 方法需要一个字符串,所以我需要在末尾使用 .ToString() 方法把子查询转换为字符串。

是的,我注意到应该添加一个接受子查询的重载……

.ToString()));

开始使用时,这种写法看上去可能有点陌生,但花点时间你就会明白,Serenity 查询系统与 SQL 有 99% 的相似。但它不能是具体的 SQL,因为我们需要在不同的语言(C#)工作。

现在,我们的 GenreList 属性过滤器就可以很好地工作了……

results matching ""

    No results matching ""