How to add "one-to" part of "one-to-many" to fetch results - swift

I want to be able to add the player's data the "to-one" part of the many relationships. The fetch does some aggregation for me, but I would like to know what player it belongs to.
I have a CoreData model that looks like the following:
I have a fetch request that looks like the following:
func statsPerPlayer(player: Players, managedContext: NSManagedObjectContext) -> [String: Int] {
var resultsDic = [String: Int]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Shifts")
let predicate = NSPredicate(format: "playersRelationship = %#", player)
fetchRequest.predicate = predicate
let nsExpressionForKeyPath = NSExpression(forKeyPath: "timeOnIce")
let nsExpressionForFunctionMin = NSExpression(forFunction: "min:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionMin = NSExpressionDescription()
nsExpressionDescriptionMin.expression = nsExpressionForFunctionMin
nsExpressionDescriptionMin.name = "minShift"
nsExpressionDescriptionMin.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionMax = NSExpression(forFunction: "max:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionMax = NSExpressionDescription()
nsExpressionDescriptionMax.expression = nsExpressionForFunctionMax
nsExpressionDescriptionMax.name = "maxShift"
nsExpressionDescriptionMax.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionSum = NSExpression(forFunction: "sum:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionSum = NSExpressionDescription()
nsExpressionDescriptionSum.expression = nsExpressionForFunctionSum
nsExpressionDescriptionSum.name = "sumShift"
nsExpressionDescriptionSum.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionAvg = NSExpression(forFunction: "average:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionAvg = NSExpressionDescription()
nsExpressionDescriptionAvg.expression = nsExpressionForFunctionAvg
nsExpressionDescriptionAvg.name = "avgShift"
nsExpressionDescriptionAvg.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionCount = NSExpression(forFunction: "count:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionCount = NSExpressionDescription()
nsExpressionDescriptionCount.expression = nsExpressionForFunctionCount
nsExpressionDescriptionCount.name = "countShift"
nsExpressionDescriptionCount.expressionResultType = .integer16AttributeType
fetchRequest.propertiesToFetch = [nsExpressionDescriptionMin, nsExpressionDescriptionMax, nsExpressionDescriptionSum, nsExpressionDescriptionAvg, nsExpressionDescriptionCount]
fetchRequest.resultType = .dictionaryResultType
do {
let fetchArray = try managedContext.fetch(fetchRequest)
print(fetchArray)
resultsDic = fetchArray.first as! [String : Int]
} catch let error as NSError {
print("\(self) -> \(#function): Could not fetch. \(error), \(error.userInfo)")
}
return resultsDic
} //statsPerPlayer
The results look great and something like this:
[{
avgShift = 39;
countShift = 4;
maxShift = 89;
minShift = 6;
sumShift = 157;
}]
However, I would like to include the player that this data is for. How do I add the "to-one" part of the one-to-many relationship in the results?
Thanks!!

In your code above, just add the relevant relationship name to the properties to fetch:
fetchRequest.propertiesToFetch = ["playersRelationship", nsExpressionDescriptionMin, nsExpressionDescriptionMax, nsExpressionDescriptionSum, nsExpressionDescriptionAvg, nsExpressionDescriptionCount]
The dictionaries that are returned will then include the key "playersRelationship" with value set to the NSManagedObjectID of the corresponding Players object. You can then use the context's object(with:) method to access the Players object itself.
Update
So after some testing, it turns out:
a) CoreData gets confused regarding the count aggregate function if you include the relationship in the propertiesToFetch. That leads to the Invalid keypath (request for aggregate operation on a toOne-only keypath error.
b) CoreData gets confused for all the other aggregate functions if you include the relationship in the propertiesToFetch. (It calculates the aggregate across every object, not just those matching the predicate.)
The solution to both problems is to add the relationship as a GROUP BY property. CoreData then calculates the aggregates correctly and also correctly recognises count as a valid operation. So, add the following line:
fetchRequest.propertiesToGroupBy = ["playersRelationship"]

Related

sum only values with specified bool with core data

I have a tableview with values. The database is made with core data. You can set the values to true or false. I only want to sum the values with true. To sum the values i have this code.
func printData() {
//Shorthand Argument Names
//let request: NSFetchRequest<Gegenstand> = Gegenstand.fetchRequest()
//let records = try! context.fetch(request) as [NSManagedObject]
let sum = ViewController.liste.reduce(0) { $0 + ($1.value(forKey: "gewicht") as? Double ?? 0) }
print("Gesamtgewicht: \(sum) kg")
gewicht = sum
if gewicht > 3500 {
gewichtLabel.textColor = .red
gewichtLabel.text = "\(gewicht) kg"
}
}
I tried it with an if-function but i don't know to use it with core data.
Create a coreData fetchRequest with isValue=true,
Calculate the sum of return fetchRequest
func fetchAllWithTrue() -> [Gegenstand] {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: Gegenstand)
fetchRequest.predicate = NSPredicate(format: “isValue== YES")
do {
let fetchedObjects = try coreDataManager.context.fetch(fetchRequest) as? [Gegenstand]
return fetchedObjects
} catch {
print(error.localizedDescription)
return [Gegenstand]()
}
}
You can do all of it in Core Data if you want. Filter for true filtering with an NSPredicate, and have Core Data calculate the sum using NSExpression.
First set up the fetch request to get only entries where the property is true and make sure it returns dictionary-type results (I don't know what your boolean is called, so here I'm calling it flag. Put your property name there):
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Gegenstand")
fetchRequest.resultType = .dictionaryResultType
fetchRequest.predicate = NSPredicate(format: "flag = true")
Then set up an NSExpressionDescription that can get the sum of gewicht values:
let sumExpression = NSExpression(format: "sum:(gewicht)")
let sumExpressionDescription = NSExpressionDescription()
sumExpressionDescription.expression = sumExpression
sumExpressionDescription.resultType = .double
sumExpressionDescription.name = "gewichtSum"
What this does is create a new kind of property that Core Data understands where the value is the sum the values of gewicht and is named gewichtSum. Fetch requests know how to use that kind of property. You do it like this:
fetchRequest.propertiesToFetch = [ sumExpressionDescription ]
Since the fetch uses dictionaryResultType, running the fetch returns an array of dictionaries. It's an array because fetch always returns an array, but here there's only one entry. The dictionary in that array entry has a key called gewichtSum for a Double property that was calculated from the expression above. You get the value like this:
do {
let result = try context.fetch(fetchRequest)
if result.count > 0,
let sumInfo = result[0] as? [String:Double],
let gewichtSum: Double = sumInfo["gewichtSum"] {
print("Sum: \(gewichtSum)")
}
} catch {
...
}
The print statement above prints the sum of all gewicht values where flag is true. It only includes true values for flag because of the NSPredicate, and it contains the sum because of the expression description.

Swift - Core Data - using the result of fetch with GroupBy

I couldn't find answer to this question, I checked all questions here.
Summary:
I don't know how to use the result(that I successfully get back) of .fetch() method with GroupBy. The fetch method result must have .dictionaryResultType because I am using GroupBy, otherwise the GroupBy would not work. I can see that fetch returns [Any], so array of Any. However I try to cast/use/access this [Any] it fails.
My code:
//Create new entity
let timeLogEntity = NSEntityDescription.entity(forEntityName: "TimeLog", in: managedContext)!
//sum up the duration column
let keypathDuration = NSExpression(forKeyPath: "duration")
let expression = NSExpression(forFunction: "sum:", arguments: [keypathDuration])
let sumDesc = NSExpressionDescription()
sumDesc.expression = expression
sumDesc.name = "DurationSum"
sumDesc.expressionResultType = .integer64AttributeType
//Fetch request
var timeLogFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TimeLog")
//group by region
timeLogFetchRequest.returnsObjectsAsFaults = false
timeLogFetchRequest.propertiesToGroupBy = ["region"]
//sum of duration column
timeLogFetchRequest.propertiesToFetch = [sumDesc, "region"]
timeLogFetchRequest.resultType = .dictionaryResultType
//it works only like this with [Any[\]
var dailyFetchResult: [Any]?
do{
dailyFetchResult = try managedContext.fetch(timeLogFetchRequest)
}catch...
So the fetch works and returns values, I just don't know how to use them, or convert them to anything usable dictionary, tuple, String and Int...
The result looks like this: {DurationSum = 235; region = RegionName1}{DurationSum = 256; region = RegionName2} etc
Thanks
(dailyFetchResult[0] as! [String:Any]).keys , and (dailyFetchResult[0] as! [String:Any]).values did it. Thank you!

CoreData Fetch: Getting the objects with the maximum of one atttribute and having another attribute in common

I have a CoreData ManagedObject type Event with the properties name:Sting and date:Date.
I want to fetch all EventObject, with the name containing a filter. If more than one object with the same name matches the filter, only the object with the latest date should be returned.
Just to clarify what I want. In a table based approach I would query in SQL like:
SELECT name, max(date) FROM Event
WHERE name contains 'filter'
GROUP BY name
In CoreData I tried like this, but got:
Could not cast value of type 'NSKnownKeysDictionary1' to 'xx12.Event'.
let fetchRequest : NSFetchRequest<Event> = Event.fetchRequest( fetchRequest.predicate = NSPredicate (format: "name contains %#", filter)
fetchRequest.sortDescriptors = [NSSortDescriptor (key: "name", ascending: true)]
// We want Event not Dictionary back
// fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType
let keypathExpression = NSExpression(forKeyPath: "date")
let maxExpression = NSExpression(forFunction: "max:", arguments: [keypathExpression])
let key = "maxDate"
let expressionDescription = NSExpressionDescription()
expressionDescription.name = key
expressionDescription.expression = maxExpression
expressionDescription.expressionResultType = .dateAttributeType
fetchRequest.propertiesToFetch = [expressionDescription]
let resultEvents = try moc.fetch(fetchRequest)
Example:
if I have these objects
meeting1, 1.1.2020
meeting1, 1.4.2020
meeting2, 1.1.2020
meeting2, 1.4.2020
party, 2.3.2020
and the filter: meeting
I want to get the objects:
meeting1, 1.4.2020
meeting2, 1.4.2020
I would use a basic fetch request and then do the grouping part in swift code instead of working with NSExpression
let fetchRequest : NSFetchRequest<Event> = Event.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name contains %#", filter)
let result = try moc.fetch(fetchRequest)
return Dictionary(grouping: result, by: \.name).values
.compactMap { $0.max {$0.date < $1.date } }

CoreData Aggregate one to many relationships

I have a CoreData model with the following "architecture": I have a Session entity with system_version as an attribute. Then I have a Views (Session<-->>Views) entity with a delta_time an attribute. Finally I have a "UI_Elements" (Views <-->> UI_Elements) entity with a variable_value attribute. I would like to filter the content according to a specific variable_value value and then display the delta_time average grouped by system_version.
I have tried the following implementation:
var expressionDescriptions = [AnyObject]()
expressionDescriptions.append("views_ui.sessions.systemVersion" as AnyObject)
let expressionDescription = NSExpressionDescription()
expressionDescription.name = "Average Time"
expressionDescription.expression = NSExpression(format: "#avg.views_ui.delta_time_number")
expressionDescription.expressionResultType = .integer32AttributeType
expressionDescriptions.append(expressionDescription)
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "UI_Elements")
fetch.predicate = requete
fetch.propertiesToGroupBy = ["views_ui.sessions.systemVersion"]
fetch.resultType = .dictionaryResultType
fetch.sortDescriptors = [NSSortDescriptor(key: "views_ui.sessions.systemVersion", ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))]
fetch.propertiesToFetch = expressionDescriptions
var results:[[String:AnyObject]]?
Unfortunately I got the following error:
CoreData: error: SQLCore dispatchRequest: exception handling request:
, Invalid keypath (request
for aggregate operation on a toOne-only keypath):
views_ui.delta_time_number with userInfo of (null)
I finally managed to get the desired result by modifying the code as follows:
let keypathExp = NSExpression(forKeyPath: "views_ui.delta_time_number")
expressionDescription.expression = NSExpression(forFunction: "average:", arguments: [keypathExp])

CoreData sum in Swift

I have a CoreData entity Transaction with a decimal amount attribute. I want to get the sum of all amounts filtered by a predicate.
Altohugh there are solutions using loops I want to do it directly in CoreData because of performance (and understanding NSExpression).
The following code works as expected but, as I said, is not what I am looking for
// Easy way: Fetch all and use `value(forKeyPath:)`
let transactionFetch = NSFetchRequest<NSManagedObject>(entityName: CoreDateWrapper.Entity.Transaction)
var results: [NSManagedObject] = []
do {
results = try wrapper.context.fetch(transactionFetch)
} catch {
print("Error: \(error.localizedDescription)")
}
print((results as NSArray).value(forKeyPath: "#sum.\(CoreDateWrapper.Attribute.amount)") as! Decimal)
Now I want to use NSExpression, but the fetch result always is an empty array.
// Setup the expression and expression-description
let amountExpression = NSExpression(forKeyPath: CoreDateWrapper.Attribute.amount)
let sumExpression = NSExpression(forFunction: "sum:", arguments: [amountExpression])
let sumDescription = NSExpressionDescription()
sumDescription.expression = sumExpression
sumDescription.expressionResultType = .decimalAttributeType
sumDescription.name = "sum"
// Setup the fetchRequest to only get the sum.
// I expect a dictionary as a result instead of a `NSManagedObject`
let sumFetch = NSFetchRequest<NSDictionary>(entityName: CoreDateWrapper.Entity.Transaction)
sumFetch.propertiesToFetch = [sumDescription]
sumFetch.resultType = .dictionaryResultType
// Fetch the sum
var sumResult: [String: Decimal]? = nil
do {
let array = try wrapper.context.fetch(sumFetch)
if let res = array.first as? [String: Decimal] {
sumResult = res
} else {
print("Wrong type for result")
}
} catch {
print("Error fetching result: \(error.localizedDescription)")
}
// Output the sum
if let sum = sumResult?["sum"] {
print("Total: \(sum)")
}
This always prints Wrong type for result because array is empty.