核心数据等同于SQL DATEPART

时间:2015-12-23 15:38:09

标签: ios swift core-data

鉴于已发布的图书表格date_publishedNSAttributeType.DateAttributeType,我想知道有多少本书按年发布:

Year | Books
-----+------
2013 | 76
2014 | 172
2015 | 155

在普通的旧SQL中,这很简单(虽然它因RDBMS略有不同):

SELECT DATEPART(yyyy, date_published) AS "Year", COUNT(*) AS "Books"
FROM books
GROUP BY DATEPART(yyyy, date_published)

我一般都是Swift和iOS的新手,但我所看到的一切都建议预先计算年份并存储,或者加载所有数据并自己计算。这些方法都不适合我,因为这一年实际上是一个会计年度(在存储后可能会有所不同)并且数据量可能很大。

大多数方法都围绕向我的NSManagedObject添加自定义属性。这似乎对我来说太晚了,因为在这个阶段对象不会被加载到内存中。围绕NSFetchedResultsControllersectionNameKeyPath进行了讨论,但这再次感觉在获取过程中为时已晚。我发现NSExpression令人费解,所以我可能错过了一些东西,但似乎我不能在这里调用自定义的Swift函数。真的,在一天结束的时候,我期望找到像DATEPART,DATEADD,DATEDIFF这样的东西的内置函数,我希望有人能指出我正确的方向。

作为一个更具体的例子,考虑4月6日至4月5日的英国纳税年度。为计算纳税年度,我将减去3个月和6天(4月5日午夜)。因此,对于2012年3月1日发表的一本书,我将进行减法,这将给我2011年11月24日,包括2月29天的闰年。由此我简单地提取了2011年的年份部分。因此2012年3月1日的英国纳税年度是2011年。我可以预先计算2011年并将其存储在新专栏中。但是,如果我从英国搬到澳大利亚,财政年度会变为7月到6月。更有可能的是,我的公司会计期间与财政年度不同(很可能在英国)。然后该公司被一个使用日历年的美国集团接管,每个人都很高兴,除了我认为2012年3月是2011年的小应用程序。

这里有一些样板来开始... 没有尝试按年分组

// The raw date for grouping by - no attempt to extract year
let date = NSExpressionDescription()
date.expression = NSExpression(format: "date_published")
date.name = "date"
date.expressionResultType = .DateAttributeType

// The number of books
let books = NSExpressionDescription()
books.expression = NSExpression(format: "count:(publication_title)")
books.name = "books"
books.expressionResultType = .Integer32AttributeType

// Put a fetch together
let fetch = NSFetchRequest(entityName:"Book")
fetch.resultType = .DictionaryResultType
fetch.propertiesToFetch = [date, books]
fetch.propertiesToGroupBy = [date]

// Execute now
var error: NSError?
if let results = context.executeFetchRequest(fetch,
    error: &error) as Array<NSDictionary>? {

        for row in results {
            let date = row.valueForKey("date") as? NSDate
            let books = row.valueForKey("books") as? Int
            NSLog("%@ %d", date!, books!)
        }

} else {

    NSLog("Fail!")

}

感谢您的任何指示!

1 个答案:

答案 0 :(得分:4)

正如您所发现的,这触及了Core Data API中的弱点。通常可以解释一个人不应该在SQL方面考虑核心数据,因为它使用了不同的方法。日期是令人讨厌的地方,因为Core Data隐藏了一些SQLite功能。 (他们至少部分地这样做是因为Core Data不是SQLite包装器,并且可以与其他非SQL存储系统一起使用)。

核心问题是Core Data的“Date”类型对应于NSDate,而NSDate依次只是一个浮点数,表示自参考日期以来的秒数。它不包括年,月或日。这些值甚至没有固定,因为NSDate表示的时刻可能意味着加利福尼亚州与日本的日期不同,例如。遗憾的是,这些类型名称中的“日期”一词具有误导性。

这就是为什么人们通常建议对使用核心数据的应用程序使用额外字段或至少不同的数据类型,这些应用程序需要考虑某个时区的实际日期,而不考虑区域的精确时刻。没有一种好方法可以构建一个在“日期”字段上运行的核心数据查询,该字段可以满足您的需要。处理这个问题归结为存储您实际需要的数据而不是仅仅接近您需要的数据 - 除了调用此类型“Date”会混淆选择。您不需要此处的核心数据“日期”类型。

因此,让我们考虑一种方法来获得所需的结果,同时让SQLite尽可能多地完成工作。假设您使用integerDate字段替换日期字段,该字段使用格式yyyyMMDD将日期表示为整数(核心数据“整数64”)。今天将存储为20151223.从理论上讲,这可以通过一些NSExpression魔法一步完成,但Core Data不允许您按表达式进行分组,这样就可以了。

第1步:获取所有不同的年份值

NSExpression *yearExpression = [NSExpression expressionWithFormat:@"divide:by:(%K,10000)", @"integerDate"];
NSExpressionDescription *yearExpDescription = [[NSExpressionDescription alloc] init];
yearExpDescription.name = @"year";
yearExpDescription.expression = yearExpression;
yearExpDescription.expressionResultType = NSInteger64AttributeType;

NSFetchRequest *distinctYearsRequest = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
distinctYearsRequest.resultType = NSDictionaryResultType;
distinctYearsRequest.returnsDistinctResults = YES;
distinctYearsRequest.propertiesToFetch = @[ yearExpDescription ];

NSError *fetchError = nil;
NSArray *distinctYearsResult = [self.managedObjectContext executeFetchRequest:distinctYearsRequest error:&fetchError];
if (distinctYearsResult != nil) {
    NSLog(@"Results by year: %@", distinctYearsResult);
}
NSArray *distinctYears = [distinctYearsResult valueForKey:@"year"];

在上文中,yearExpression通过简单划分获得integerDate的年份部分。完成上述操作后,distinctYears将包含integerDate所代表的所有年份。

步骤2:循环使用年限,计算每个:

NSMutableDictionary *countByYear = [NSMutableDictionary dictionary];

for (NSNumber *year in distinctYears) {
    NSFetchRequest *countForYearFetch = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
    countForYearFetch.resultType = NSDictionaryResultType;
    countForYearFetch.propertiesToFetch = @[ yearExpDescription ];

    NSExpression *targetYearExpression = [NSExpression expressionForConstantValue:year];
    NSPredicate *yearPredicate = [NSComparisonPredicate predicateWithLeftExpression:yearExpression rightExpression:targetYearExpression modifier:NSDirectPredicateModifier type:NSEqualToPredicateOperatorType options:0];
    countForYearFetch.predicate = yearPredicate;

    NSError *fetchError = nil;
    NSUInteger countForYear = [self.managedObjectContext countForFetchRequest:countForYearFetch error:&fetchError];
    countByYear[year] = @(countForYear);
}

NSLog(@"Results by year: %@", countByYear);

这会对每年进行单独的提取,但仅通过获取结果计数而不是实际数据来保持较低的内存开销。完成后,countByYear根据integerDate字段按年份显示条目数。

说完所有这些后,请记住,您可以选择直接使用SQLite而不是使用Core Data。 PLDatabase将为您提供一个Objective-C样式的包装器,同时仍然允许对SQLite可以执行的所有操作进行原始SQL查询。