iPhone OS: Fetching a random entity instance using NSPredicate Nsfetchrequest and core data
Asked Answered
G

6

12

Working on an app where I have a large collections of managed objects against which I want to fetch a few random instances.

My question is, is there any way I can use NSPredicate and NSFetchRequest to return several objects at random.

I saw that you could actually add a NSFetchRequest into the entity using the data modeler, any way to do the random fetch using this?

Also what would be the best method for determining the "count" of a table so I can set the bounds of the random number generator.

let me know if you need more details.

Thanks!

Nick

Glad answered 13/5, 2010 at 21:42 Comment(2)
Extra info: the things I'm trying to grab are objects with two properties, an nsstring any where from 1-50 chars long, and a pseudo primary key int I thought might help with the random selection bit. I can restructure the model though if necessary still prototyping this sucker.Glad
This comment should be added to your question.Ulceration
H
5

This may not be exactly how you implement this, but hopefully it will get you started.

Somewhere in your header or at the top of your implementation file:

#import <stdlib.h>
#import <time.h>

Elsewhere in your implementation:

//
// get count of entities
//
NSFetchRequest *myRequest = [[NSFetchRequest alloc] init];
[myRequest setEntity: [NSEntityDescription entityForName:myEntityName inManagedObjectContext:myManagedObjectContext]];
NSError *error = nil;
NSUInteger myEntityCount = [myManagedObjectContext countForFetchRequest:myRequest error:&error];    
[myRequest release];

//
// add another fetch request that fetches all entities for myEntityName -- you fill in the details
// if you don't trigger faults or access properties this should not be too expensive
//
NSArray *myEntities = [...];

//
// sample with replacement, i.e. you may get duplicates
//
srandom(time(NULL)); // seed random number generator, so that you get a reasonably different series of random integers on each execution
NSUInteger numberOfRandomSamples = ...;
NSMutableSet *sampledEntities = [NSMutableSet setWithCapacity:numberOfRandomSamples];
for (NSInteger sampleIndex = 0; sampleIndex < numberOfRandomSamples; sampleIndex++) {
    int randomEntityIndex = random() % myEntityCount; // generates random integer between 0 and myEntityCount-1
    [sampledEntities addObject:[myEntities objectAtIndex:randomEntityIndex]];
}

// do stuff with sampledEntities set

If you need to sample without replacement, to eliminate duplicates, you might create an NSSet of randomEntityIndex NSNumber objects, instead of just sampling random ints.

In this case, sample from an ordered NSSet, remove NSNumber objects as you pull them out of the bag, and decrement myEntityCount for the purposes of picking a random NSNumber object from the set.

Hedjaz answered 13/5, 2010 at 22:17 Comment(5)
Hey Alex thanks so much, very cool! This looks like it should work perfectly. My only concern is that the myEntities array will contain anywhere from 3k-5k instances, even with that many pieces of data are you thinking it will not get too crappily slow? I was ideally hoping to perform the random selection without grabbing every entity instance, am i barking up the wrong tree here? Thanks again Alex!Glad
Extra info: the things I'm trying to grab are objects with two properties, an nsstring any where from 1-50 chars long, and a pseudo primary key int I thought might help with the random selection bit. I can restructure the model though if necessary still prototyping this sucker.Glad
You could definitely use the pseudo-primary-key attribute instead of grabbing all records.Hedjaz
Cool, I'll report back what I end up doing, I'm reading this core data book and trying not to gauge my eyes out. cool.Glad
The objects returned by Core Data will be empty except for their -objectID property and therefore the response will be very quick. Only the objects that you are accessing attributes on will be fully realized and pulled into memory.Ulceration
T
21

Instead use the fetchLimit in conjunction with the fetchOffset that way you can efficiently fetch only a single entity into memory:

NSFetchRequest *myRequest = [[NSFetchRequest alloc] init];
[myRequest setEntity: [NSEntityDescription entityForName:myEntityName inManagedObjectContext:myManagedObjectContext]];
NSError *error = nil;
NSUInteger myEntityCount = [myManagedObjectContext countForFetchRequest:myRequest error:&error];    

NSUInteger offset = myEntityCount - (arc4random() % myEntityCount);
[myRequest setFetchOffset:offset];
[myRequest setFetchLimit:1];

NSArray* objects = [myManagedObjectContext executeFetchRequest:myRequest error:&error];    
id randomObject = [objects objectAtIndex:0];
Thorium answered 6/3, 2011 at 4:9 Comment(3)
I tried this method of fetching a random object because I thought it would have much less overhead than continually re-fetching my entire list of over 5k entries to pull out one random object. However, what I found was that no matter what the offset was I would always receive back one of the same ten or so managed objects. This method of generating random objects was by no means 'random' and thus, for my usage, unacceptable. The overhead from following the accepted answer was minimal, at least much less than I had thought. Due to this, I would recommend the accepted answer over this one.Kobi
I am also having trouble with this... I needed to get a random item from my core data instances and created a method that did almost the same than Corey Floyd says, but it seems there is something that CoreData doesn't like and it doesn't return the expected result...Excisable
+1 for this answer, but it needs a small fix, when calculated offset gets equal to myEntityCount, the fetch will return zero records, at least in my case, so i subtracted 1 from calculated offset. Then you need another small check -> if myEntityCount is zero you can't actually get any results whatsoever, then you need to exit the method before the offset is calculated.Gamy
H
5

This may not be exactly how you implement this, but hopefully it will get you started.

Somewhere in your header or at the top of your implementation file:

#import <stdlib.h>
#import <time.h>

Elsewhere in your implementation:

//
// get count of entities
//
NSFetchRequest *myRequest = [[NSFetchRequest alloc] init];
[myRequest setEntity: [NSEntityDescription entityForName:myEntityName inManagedObjectContext:myManagedObjectContext]];
NSError *error = nil;
NSUInteger myEntityCount = [myManagedObjectContext countForFetchRequest:myRequest error:&error];    
[myRequest release];

//
// add another fetch request that fetches all entities for myEntityName -- you fill in the details
// if you don't trigger faults or access properties this should not be too expensive
//
NSArray *myEntities = [...];

//
// sample with replacement, i.e. you may get duplicates
//
srandom(time(NULL)); // seed random number generator, so that you get a reasonably different series of random integers on each execution
NSUInteger numberOfRandomSamples = ...;
NSMutableSet *sampledEntities = [NSMutableSet setWithCapacity:numberOfRandomSamples];
for (NSInteger sampleIndex = 0; sampleIndex < numberOfRandomSamples; sampleIndex++) {
    int randomEntityIndex = random() % myEntityCount; // generates random integer between 0 and myEntityCount-1
    [sampledEntities addObject:[myEntities objectAtIndex:randomEntityIndex]];
}

// do stuff with sampledEntities set

If you need to sample without replacement, to eliminate duplicates, you might create an NSSet of randomEntityIndex NSNumber objects, instead of just sampling random ints.

In this case, sample from an ordered NSSet, remove NSNumber objects as you pull them out of the bag, and decrement myEntityCount for the purposes of picking a random NSNumber object from the set.

Hedjaz answered 13/5, 2010 at 22:17 Comment(5)
Hey Alex thanks so much, very cool! This looks like it should work perfectly. My only concern is that the myEntities array will contain anywhere from 3k-5k instances, even with that many pieces of data are you thinking it will not get too crappily slow? I was ideally hoping to perform the random selection without grabbing every entity instance, am i barking up the wrong tree here? Thanks again Alex!Glad
Extra info: the things I'm trying to grab are objects with two properties, an nsstring any where from 1-50 chars long, and a pseudo primary key int I thought might help with the random selection bit. I can restructure the model though if necessary still prototyping this sucker.Glad
You could definitely use the pseudo-primary-key attribute instead of grabbing all records.Hedjaz
Cool, I'll report back what I end up doing, I'm reading this core data book and trying not to gauge my eyes out. cool.Glad
The objects returned by Core Data will be empty except for their -objectID property and therefore the response will be very quick. Only the objects that you are accessing attributes on will be fully realized and pulled into memory.Ulceration
G
2

I've searched around a lot for this, essentially Coredata won't give you any random rows, and it is not meant to. You have to create your own.

This is what I came up with up with, assuming we are using an NSPredicate and theres no primary Unique key, this is the best possible answer i think with the least overhead.

  1. Set NSFetchRequest type to NSManagedObjectID. Turn everything off, to minimize overhead.
  2. Execute fetch request with your desired predicate, Do Not use any FetchLimit.
  3. From the received NSManagedObjectID array. get your random number of objects. this is a good solution: Get n random objects (for example 4) from nsarray

  4. Now you've got random NSManagedObjectIDs of your desired count, (which are more or less random)

  5. Loop through the random objectID array and Use NSManagedObjectContext objectWithID: to get the objects.
Gaslit answered 23/2, 2015 at 22:1 Comment(0)
O
1

This is what I did in Swift based on Corey Floyd's answer, given a Verb entity. A few manual tests give me satisfactory random results.

let request = Verb.fetchRequest()

do {
    let count = try self.context.count(for: request)
            
    let randomOffset = Int.random(in: 0..<count)
    request.fetchOffset = randomOffset
    request.fetchLimit = 1
            
    let verbs = try self.context.fetch(request)
}
catch let error {
    print(error.localizedDescription)
}
Oneupmanship answered 19/12, 2022 at 22:50 Comment(0)
K
0

If you are fetching all the objects anyway, there is no need for the first request to get the object count. You can just use something like:

myEntityCount = [myEntities count]
Kremlin answered 14/12, 2010 at 0:18 Comment(0)
U
0

Solution suggested by Core will not work if you need to randomize the fetch within a subset table rows restricted by a predicate (e.g. "where something < something").

The best solution I have so far (where I do not need to fetching all or large number of rows) is using randomly selecting based on a primary key (off course the requires a primary key in the table, preferably w/o any missing values).

Unisexual answered 29/4, 2013 at 20:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.