zoukankan      html  css  js  c++  java
  • RestfulApi 学习笔记——分页和排序(五)

    前言

    分页和排序时一些非常常规的操作,同样也有一些我们注意的点。

    正文

    分页

    先来谈及分页。

    看下前端传递的参数。

    public class EmployeeDtoParameters
    {
    	private const int MaxPageSize = 20;
    	public string Gender { get; set; }
    	public string Q { get; set; }
    	public int PageNumber { get; set; } = 1;
    	private int _pageSize = 5;
    
    	public int PageSize
    	{
    		get => _pageSize;
    		set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
    	}
    }
    

    第一个注意的地方就是避免一些攻击,比如说人家设置_pageSize 非常大的话,那么很有可能会让你的机器宕机,同样你也来不及反应抵挡数据爬取。

    所以针对这个你可以设置一下最大数量。

    然后呢,这上面有个不完善的地方在于由很多类可能都会使用到这个MaxPageSize 和_pageSize 这些,这时候我们最好去提取出来作为一个基础类,然后继承,这样统一了风格。

    接着我们可以这样写分页。

    public async Task<IEnumerable<Company>> GetCompaniesAsync(CompanyDtoParameters parameters)
    {
    	if (parameters == null)
    	{
    		throw new ArgumentNullException(nameof(parameters));
    	}
       
    	var queryExpression = _context.Companies as IQueryable<Company>;
    
    	if (!string.IsNullOrWhiteSpace(parameters.CompanyName))
    	{
    		parameters.CompanyName = parameters.CompanyName.Trim();
    		queryExpression = queryExpression.Where(x => x.Name == parameters.CompanyName);
    	}
    
    	if (!string.IsNullOrWhiteSpace(parameters.SearchTerm))
    	{
    		parameters.SearchTerm = parameters.SearchTerm.Trim();
    		queryExpression = queryExpression.Where(x => x.Name.Contains(parameters.SearchTerm) ||
    													 x.Introduction.Contains(parameters.SearchTerm));
    	}
    
    	return await queryExpression.Skip(parameters.PageNumber-1).Take(parameters.PageSize);
    }
    

    利用skip和take,但是这样写不好。

    为什么这么说呢? 是这样子的,我们返回分页数据的时候,一般来说,要带上总页数或者说记录数。

    为了方便起见呢,后台同样要返回是否有前一页和后一页,还有前一页的地址,和后一页的地址,这样前端写起来方便。

    同样的不要谈什么让前端去计算,因为这会让前端苦不堪言,而现在的前端并不是就是干ui切图的,人家还有很多思考性的问题。

    在这样还有一个问题,就是以前我们返回的时候一般json数据是这样子的。

    {
       currentPage:1,
       totalPages:20,
       prePageLink:"",
       nextPageLink:"",
       items:[{数据},{数据}],
    }
    

    这样写是不符合restful 风格的,比如说api/companies,人家需要的是companies的资源,而不是什么currentPage和totalPages 等等信息。

    这些不属于它要请求的资源,那么这些资源应该放到另外一个地方去。很多情况下放到header中,业界一般放在X-Pagination中,那么看下如何实现吧。

    首先建立一个pageList 来作为非请求资源的存储,比如说currentPage、totalPages等

    public class PagedList<T>: List<T>
    {
    	public int CurrentPage { get; private set; }
    	public int TotalPages { get; private set; }
    	public int PageSize { get; private set; }
    	public int TotalCount { get; private set; }
    
    	public bool HasPrevious => CurrentPage > 1;
    	public bool HasNext => CurrentPage < TotalPages;
    
    	public PagedList(List<T> items, int count, int pageNumber, int pageSize)
    	{
    		TotalCount = count;
    		PageSize = pageSize;
    		CurrentPage = pageNumber;
    		TotalPages = (int) Math.Ceiling(count / (double) pageSize);
    		AddRange(items);
    	}
    
    	public static async Task<PagedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
    	{
    		var count = await source.CountAsync();
    		var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
    		return new PagedList<T>(items, count, pageNumber, pageSize);
    	}
    }
    

    那么我们的查询这样写:

    public async Task<IEnumerable<Company>> GetCompaniesAsync(CompanyDtoParameters parameters)
    {
    	if (parameters == null)
    	{
    		throw new ArgumentNullException(nameof(parameters));
    	}
       
    	var queryExpression = _context.Companies as IQueryable<Company>;
    
    	if (!string.IsNullOrWhiteSpace(parameters.CompanyName))
    	{
    		parameters.CompanyName = parameters.CompanyName.Trim();
    		queryExpression = queryExpression.Where(x => x.Name == parameters.CompanyName);
    	}
    
    	if (!string.IsNullOrWhiteSpace(parameters.SearchTerm))
    	{
    		parameters.SearchTerm = parameters.SearchTerm.Trim();
    		queryExpression = queryExpression.Where(x => x.Name.Contains(parameters.SearchTerm) ||
    													 x.Introduction.Contains(parameters.SearchTerm));
    	}
    
    	return await PagedList<Company>.CreateAsync(queryExpression, parameters.PageNumber, parameters.PageSize);
    }
    

    这样我们返回的就是一个PagedList,现在我们的资源还是和一个附加参数在一起,比如说页数,那么这时候就看我们的action如何写了。

    [HttpGet(Name = nameof(GetCompanies))]
    [HttpHead]
    public async Task<IActionResult> GetCompanies([FromQuery] CompanyDtoParameters parameters)
    {
    
    	var companies = await _companyRepository.GetCompaniesAsync(parameters);
    
    	var paginationMetadata = new
    	{
    		totalCount = companies.TotalCount,
    		pageSize = companies.PageSize,
    		currentPage = companies.CurrentPage,
    		totalPages = companies.TotalPages,
    		previousLink = companies.HasPrevious ? CreateCompaniesResourceUri(parameters, ResourceUriType.PreviousPage) : null,
    		nextLink = companies.HasNext ? CreateCompaniesResourceUri(parameters, ResourceUriType.NextPage) : null
    	};
    
    	Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(paginationMetadata,
    		new JsonSerializerOptions
    		{
    			Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    		}));
    	var companyDtos = _mapper.Map<CompanyDto>(companies);
    	return Ok(companyDtos);
    }
    

    action 的方式倒是好写,就是把一些参数json序列化,放在header 中的x-pagination 中。

    这里有一个生成link的,贴一下方法。

    private string CreateLinkForCompany(CompanyDtoParameters parameters, ResourceUriType resourceUri)
    {
    	switch (resourceUri)
    	{
    		case ResourceUriType.PreviousPage:
    			return Url.Link(nameof(GetCompanies), new
    			{
    				pageNumber=parameters.PageNumber-1,
    				pageSize=parameters.PageSize,
    				companyName=parameters.CompanyName,
    				searchTerm=parameters.SearchTerm
    			});
    		case ResourceUriType.NextPage:
    			return Url.Link(nameof(GetCompanies), new
    			{
    				pageNumber = parameters.PageNumber + 1,
    				pageSize = parameters.PageSize,
    				companyName = parameters.CompanyName,
    				searchTerm = parameters.SearchTerm
    			});
    		default:
    			return Url.Link(nameof(GetCompanies), new
    			{
    				pageNumber = parameters.PageNumber - 1,
    				pageSize = parameters.PageSize,
    				companyName = parameters.CompanyName,
    				searchTerm = parameters.SearchTerm
    			});
    	}
    }
    

    这样就是一个简单的一个分页过程,同样的我们看到其中有很多的问题。

    比如说paginationMetadata 应该是一个固定的类,这样统一风格。

    同样的,CreateLinkForCompany也存在很多的问题,我们在创建link的时候呢,我们写入了很多的参数,比如说:

    companyName = parameters.CompanyName,
    searchTerm = parameters.SearchTerm
    

    如果查询的参数变化快的话,这将是一个问题,而且每次改动都需要改动这些,这也是一个问题,暂时就不改动,后面有一个小节来解决这些问题。

    排序

    排序问题也是我们常见的一个业务,这个业务应该属于过于常见吧。按照restful 风格的排序呢,一般是这样子的。/api/companies?orderby=name desc这样子的。

    这里排序字段name和排序规则desc写在了一起,为什么这么做呢?原因就是以后可能多个扩展,如果扩展一个,那么可能就是多两个字段,这样不好。

    还有一个重大问题,我们一般在uri中写的排序字段name,不是真正的数据库字段,数据库字段是companyName,这样子的,那么我们就要有一个映射关系,ok,那么这个如何处理呢?

    我们想到的就是这种:

    if(orderby.startWidth('name'))
    {
        dbset.company.orderby(u=>u.companyName)
    }
    

    但是呢,这样有一个问题,如果排序字段的选择可以很多(比如说name或者其他一些都可以作为排序的)这样会形成一个大的逻辑段,那么应该怎么办呢?

    这时候应该形成一个映射关系,使用PropertyMappingService可以做到。

    这里用emplyee的查询举例,在这个例子中不需要知道具体的emplyee是什么,只需看到如何映射即可。

    public class PropertyMappingService : IPropertyMappingService
    {
    	private readonly Dictionary<string, PropertyMappingValue> _companyPropertyMapping =
    		new Dictionary<string, PropertyMappingValue>(StringComparer.OrdinalIgnoreCase)
    		{
    			{"Id", new PropertyMappingValue(new List<string>{"Id"}) },
    			{"CompanyName", new PropertyMappingValue(new List<string>{"Name"}) },
    			{"Country", new PropertyMappingValue(new List<string>{"Country"}) },
    			{"Industry", new PropertyMappingValue(new List<string>{ "Industry"})},
    			{"Product", new PropertyMappingValue(new List<string>{"Product"})},
    			{"Introduction", new PropertyMappingValue(new List<string>{"Introduction"})}
    		};
    
    	private readonly Dictionary<string, PropertyMappingValue> _employeePropertyMapping =
    		new Dictionary<string, PropertyMappingValue>(StringComparer.OrdinalIgnoreCase)
    		{
    			{"Id", new PropertyMappingValue(new List<string>{"Id"}) },
    			{"CompanyId", new PropertyMappingValue(new List<string>{"CompanyId"}) },
    			{"EmployeeNo", new PropertyMappingValue(new List<string>{"EmployeeNo"}) },
    			{"Name", new PropertyMappingValue(new List<string>{"FirstName", "LastName"})},
    			{"GenderDisplay", new PropertyMappingValue(new List<string>{"Gender"})},
    			{"Age", new PropertyMappingValue(new List<string>{"DateOfBirth"}, true)}
    		};
    
    	private readonly IList<IPropertyMapping> _propertyMappings = new List<IPropertyMapping>();
    
    	public PropertyMappingService()
    	{
    		_propertyMappings.Add(new PropertyMapping<EmployeeDto, Employee>(_employeePropertyMapping));
    		_propertyMappings.Add(new PropertyMapping<CompanyDto, Company>(_companyPropertyMapping));
    	}
    
    	public Dictionary<string, PropertyMappingValue> GetPropertyMapping<TSource, TDestination>()
    	{
    		var matchingMapping = _propertyMappings.OfType<PropertyMapping<TSource, TDestination>>();
    
    		var propertyMappings = matchingMapping.ToList();
    		if (propertyMappings.Count == 1)
    		{
    			return propertyMappings.First().MappingDictionary;
    		}
    
    		throw new Exception($"无法找到唯一的映射关系:{typeof(TSource)}, {typeof(TDestination)}");
    	}
    
    	public bool ValidMappingExistsFor<TSource, TDestination>(string fields)
    	{
    		var propertyMapping = GetPropertyMapping<TSource, TDestination>();
    
    		if (string.IsNullOrWhiteSpace(fields))
    		{
    			return true;
    		}
    
    		var fieldAfterSplit = fields.Split(",");
    		foreach (var field in fieldAfterSplit)
    		{
    			var trimmedField = field.Trim();
    			var indexOfFirstSpace = trimmedField.IndexOf(" ", StringComparison.Ordinal);
    			var propertyName = indexOfFirstSpace == -1 ? trimmedField 
    				: trimmedField.Remove(indexOfFirstSpace);
    
    			if (!propertyMapping.ContainsKey(propertyName))
    			{
    				return false;
    			}
    		}
    
    		return true;
    	}
    }
    

    这样就可以形成一个映射关系。比如说Name 表示两个字段firstname 还有lastname这样,这个其实自己也可以实现。

    好的,来解释一下代码吧。

    首先实例化了一个数组:private readonly IList _propertyMappings = new List();

    private readonly Dictionary<string, PropertyMappingValue> _employeePropertyMapping =
    	new Dictionary<string, PropertyMappingValue>(StringComparer.OrdinalIgnoreCase)
    	{
    		{"Id", new PropertyMappingValue(new List<string>{"Id"}) },
    		{"CompanyId", new PropertyMappingValue(new List<string>{"CompanyId"}) },
    		{"EmployeeNo", new PropertyMappingValue(new List<string>{"EmployeeNo"}) },
    		{"Name", new PropertyMappingValue(new List<string>{"FirstName", "LastName"})},
    		{"GenderDisplay", new PropertyMappingValue(new List<string>{"Gender"})},
    		{"Age", new PropertyMappingValue(new List<string>{"DateOfBirth"}, true)}
    	};
    

    上面就是我们写的映射字典。

    来看下PropertyMappingValue 这个是啥,这个其实就是自己定义的一个东西:

    public class PropertyMappingValue
    {
    	public IEnumerable<string> DestinationProperties { get; set; }
    
    	public bool Revert { get; set; }
    
    	public PropertyMappingValue(IEnumerable<string> destinationProperties, bool revert = false)
    	{
    		DestinationProperties = destinationProperties 
    								?? throw new ArgumentNullException(nameof(destinationProperties));
    		Revert = revert;
    	}
    }
    

    接下来就是加进去:

    public PropertyMappingService()
    {
    	_propertyMappings.Add(new PropertyMapping<EmployeeDto, Employee>(_employeePropertyMapping));
    	_propertyMappings.Add(new PropertyMapping<CompanyDto, Company>(_companyPropertyMapping));
    }
    

    然后把这几个添加到里面。

    public Dictionary<string, PropertyMappingValue> GetPropertyMapping<TSource, TDestination>()
    {
    	var matchingMapping = _propertyMappings.OfType<PropertyMapping<TSource, TDestination>>();
    
    	if (matchingMapping.Count() == 1)
    	{
    		return matchingMapping.First().MappingDictionary;
    	}
    
    	throw new Exception($"无法找到唯一的映射关系:{typeof(TSource)}, {typeof(TDestination)}");
    }
    

    上面这个可以说是关键了,_propertyMappings通过OfType,找到PropertyMapping<TSource, TDestination>类型集合。

    如果是PropertyMapping<EmployeeDto, Employee>也就是new PropertyMapping<EmployeeDto, Employee>的一个集合。

    然后判断是否添加进去了,如果形成一个1对1的关系,那么就返回我们添加进去的,也就是new PropertyMapping<EmployeeDto, Employee>(_employeePropertyMapping)

    看下PropertyMapping 是啥?

    public class PropertyMapping<TSource, TDestination>: IPropertyMapping
    {
    	public Dictionary<string,PropertyMappingValue> MappingDictionary { get; private set; }
    
    	public PropertyMapping(Dictionary<string, PropertyMappingValue> mappingDictionary)
    	{
    		MappingDictionary = mappingDictionary 
    							 ?? throw new ArgumentNullException(nameof(mappingDictionary));
    	}
    }
    

    其实就是做一层封装而已,为的就是OfType。在这里非常关键的就是ofType,https://www.cnblogs.com/macT/p/12069362.html 可以看下这个,挺好理解的。

    那么我们拿到一个映射关系后如何处理呢?来看查询方法。

    public async Task<IEnumerable<Employee>> GetEmployeesAsync(Guid companyId,
    	EmployeeDtoParameters parameters)
    {
    	if (companyId == Guid.Empty)
    	{
    		throw new ArgumentNullException(nameof(companyId));
    	}
    
    	var items = _context.Employees.Where(x => x.CompanyId == companyId);
    
    	if (!string.IsNullOrWhiteSpace(parameters.Gender))
    	{
    		parameters.Gender = parameters.Gender.Trim();
    		var gender = Enum.Parse<Gender>(parameters.Gender);
    
    		items = items.Where(x => x.Gender == gender);
    	}
    
    	if (!string.IsNullOrWhiteSpace(parameters.Q))
    	{
    		parameters.Q = parameters.Q.Trim();
    
    		items = items.Where(x => x.EmployeeNo.Contains(parameters.Q)
    								 || x.FirstName.Contains(parameters.Q)
    								 || x.LastName.Contains(parameters.Q));
    	}
    
    	var mappingDictionary = _propertyMappingService.GetPropertyMapping<EmployeeDto, Employee>();
    
    	items = items.ApplySort(parameters.OrderBy, mappingDictionary);
    
    	return await items.ToListAsync();
    }
    

    我们拿到一个映射关系后就可以进行排序了,看下ApplySort。

    public static IQueryable<T> ApplySort<T>(
    	this IQueryable<T> source,
    	string orderBy,
    	Dictionary<string, PropertyMappingValue> mappingDictionary)
    {
    	if (source == null)
    	{
    		throw new ArgumentNullException(nameof(source));
    	}
    
    	if (mappingDictionary == null)
    	{
    		throw new ArgumentNullException(nameof(mappingDictionary));
    	}
    
    	if (string.IsNullOrWhiteSpace(orderBy))
    	{
    		return source;
    	}
    
    	var orderByAfterSplit = orderBy.Split(",");
    
    	foreach (var orderByClause in orderByAfterSplit.Reverse())
    	{
    		var trimmedOrderByClause = orderByClause.Trim();
    
    		var orderDescending = trimmedOrderByClause.EndsWith(" desc");
    
    		var indexOfFirstSpace = trimmedOrderByClause.IndexOf(" ", StringComparison.Ordinal);
    
    		var propertyName = indexOfFirstSpace == -1
    			? trimmedOrderByClause
    			: trimmedOrderByClause.Remove(indexOfFirstSpace);
    
    		if (!mappingDictionary.ContainsKey(propertyName))
    		{
    			throw new ArgumentNullException($"没有找到Key为{propertyName}的映射");
    		}
    
    		var propertyMappingValue = mappingDictionary[propertyName];
    		if (propertyMappingValue == null)
    		{
    			throw new ArgumentNullException(nameof(propertyMappingValue));
    		}
    
    		foreach (var destinationProperty in propertyMappingValue.DestinationProperties.Reverse())
    		{
    			if (propertyMappingValue.Revert)
    			{
    				orderDescending = !orderDescending;
    			}
    
    			source = source.OrderBy(destinationProperty +
    									(orderDescending ? " descending" : " ascending"));
    		}
    	}
    
    	return source;
    }
    

    这个还是比较好理解的,思路就是用,切割orderby,然后判断是否包含desc,然后赋值给orderDescending 表示是否降序。

    同样如果包含 desc,去除掉相应的desc,保留前面的字段。然后就去找我们的映射字典,接下来就是通过orderby的拼接来完成。

    在这里我们发现这个orderby传入的是一个字符串,而我们ef又没有,是的,这是通过扩展程序来的,用库。using System.Linq.Dynamic.Core;,安装即可。

    是的至此就基本结束了,对了我们依然需要依赖注入,因为可能以后我们有更优雅的方式,注入方式如下:

    services.AddTransient<IPropertyMappingService, PropertyMappingService>();
    

    排序的思想很简单,值得学习的是一个映射方式,为什么添加一个包装类。

    下一节其他请求方式

  • 相关阅读:
    Oracle分页SQL
    CentOS7下安装Anaconda3
    Alibaba分层领域模型规约
    java的continue标签
    SQLserver 及 redis 无法连接问题
    HTTP状态码
    java命令功能
    sql 查询结果自增序号
    Viewpage实现左右无限滑动
    Android OOM 问题的总结
  • 原文地址:https://www.cnblogs.com/aoximin/p/14017990.html
Copyright © 2011-2022 走看看