Core Data : calling validation in awakeFromInsert problem ?

  • Hi list !

    I'm trying to implement validation on my managed objects and am
    facing a strange problem.

    I have a managed object with one property named "name". This property
    is a string. I want this property to be unique in the managed context
    of the object, so I've implemented "validateName..." method in my
    managed object subclass (see code below).

    Next, I want new objects inserted into the context to have a name set
    to a validated one (that means : unique). So I have implemented
    "awakeFromInsert..." method in my subclass that set the name property
    to a valid one.

    Here is the code for my custom class (implementation only, there is
    nothing of interest in the header) :

    //
    //  TestObject.m
    //  CoreData
    //
    //  Created by Eric on 05/09/05.
    //  Copyright 2005 __MyCompanyName__. All rights reserved.
    //

    #import "TestObject.h"

    @implementation TestObject

    - (NSString *)name
    {
        NSString * tmpValue;

        [self willAccessValueForKey: @"name"];
        tmpValue = [self primitiveValueForKey: @"name"];
        [self didAccessValueForKey: @"name"];

        return tmpValue;
    }

    - (void)setName:(NSString *)value
    {
        [self willChangeValueForKey: @"name"];
        [self setPrimitiveValue: value forKey: @"name"];
        [self didChangeValueForKey: @"name"];
    }

    - (void)awakeFromInsert
    {
        // Here I want the default name to be valid !
        // In case objects already exit with the name "TestObjectName",
        // append "(index++)" at the end of the name until this name
    doesn't already exists
        // in the managed object context

        NSString * validName = nil;
        NSString * defaultName = [NSString
    stringWithString:@"TestObjectName"];

        validName = defaultName;

        int index = 0;

        while ( ![self validateValue:&validName forKey:@"name" error:nil] )
        {
            index++;

            validName = [defaultName stringByAppendingFormat:@" (%d)",
    index];
        }

        [self setName:validName];
    }

    - (BOOL)validateName: (id *)valueRef error:(NSError **)outError
    {
        NSPredicate * predicate = [NSPredicate
    predicateWithFormat:@"name == %@", *valueRef];

        NSEntityDescription * entityDescription = [NSEntityDescription
    entityForName:@"TestObject" inManagedObjectContext:[self
    managedObjectContext]];
        NSFetchRequest * fetchRequest = [[[NSFetchRequest alloc] init]
    autorelease];

        [fetchRequest setEntity:entityDescription];
        [fetchRequest setPredicate:predicate];

        NSArray * results = [[self managedObjectContext]
    executeFetchRequest:fetchRequest error:nil];

        return ( [results count] < 1 ) || ( [results objectAtIndex:0] ==
    self );
    }

    @end

    Now, everything looks fine to me.

    I'm using a simple interface to test the insertion process : in fact
    it is the standard interface created automatically when alt-dragging
    the object from Xcode to IB - one table view, 3 buttons (Fetch, Add,
    Remove), a text field to enter name and a search field. Then, the
    problems begin : when I click on the add button, TWO objects are
    added to the table view !

    If I replace the line :

    while ( ![self validateValue:&validName forKey:@"name" error:nil] )

    by :

    while ( index < 5 )

    it works like a charm (every object is created with the same name
    "TestObjectName (5)", of course). So it seems that calling the
    validation method in the awakeFromInsert is causing a problem but I
    can't put my finger on it.

    Do someone know somethign about it ?
    And do you think I'm using the right process to validate my name
    property and mak sure my object are inserted with valid name ?

    Thanks,

    Eric.
  • On Sep 5, 2005, at 9:42 AM, Eric Morand wrote:
    > I have a managed object with one property named "name". This
    > property is a string. I want this property to be unique in the
    > managed context of the object, so I've implemented
    > "validateName..." method in my managed object subclass (see code
    > below).
    > Next, I want new objects inserted into the context to have a name
    > set to a validated one (that means : unique). So I have implemented
    > "awakeFromInsert..." method in my subclass that set the name
    > property to a valid one.
    > [...]
    > Here is the code for my custom class (implementation only, there is
    > nothing of interest in the header) :
    > ]...]
    > - (void)awakeFromInsert
    > {
    > [...]
    > NSString * validName = nil;
    > NSString * defaultName = [NSString
    > stringWithString:@"TestObjectName"];
    >
    > validName = defaultName;
    >
    > int index = 0;
    >
    > while ( ![self validateValue:&validName forKey:@"name"
    > error:nil] )
    > {
    > index++;
    >
    > validName = [defaultName stringByAppendingFormat:@" (%d)",
    > index];
    > }
    >
    > [self setName:validName];
    > }
    >
    > - (BOOL)validateName: (id *)valueRef error:(NSError **)outError
    > {
    > NSPredicate * predicate = [NSPredicate
    > predicateWithFormat:@"name == %@", *valueRef];
    >
    > NSEntityDescription * entityDescription = [NSEntityDescription
    > entityForName:@"TestObject" inManagedObjectContext:[self
    > managedObjectContext]];
    > NSFetchRequest * fetchRequest = [[[NSFetchRequest alloc] init]
    > autorelease];
    >
    > [fetchRequest setEntity:entityDescription];
    > [fetchRequest setPredicate:predicate];
    >
    > NSArray * results = [[self managedObjectContext]
    > executeFetchRequest:fetchRequest error:nil];
    >
    > return ( [results count] < 1 ) || ( [results objectAtIndex:0]
    > == self );
    > }
    >
    > @end
    >
    > I'm using a simple interface to test the insertion process : in
    > fact it is the standard interface created automatically when alt-
    > dragging the object from Xcode to IB - one table view, 3 buttons
    > (Fetch, Add, Remove), a text field to enter name and a search
    > field. Then, the problems begin : when I click on the add button,
    > TWO objects are added to the table view !
    >
    I'm not sure if this is a bug (I'll follow up) but it's because
    you're executing a fetch (whose results will include self) before the
    insertion has completed.  You can sidestep this by setting the name
    after awakeFromInsert:

    - (void)awakeFromInsert
    {
        [super awakeFromInsert];
        [self performSelector:@selector(setUniqueName) withObject:nil
    afterDelay:0];
    }

    There are several issues with the remainder of the awakeFromInsert
    code (now moved to the new method) and the validation method:

    - (void)setUniqueName
    {
        NSString * defaultName = @"TestObjectName"; // no need for
    stringWithString:
        NSString * validName = defaultName; // no need for two assignments
        int index = 0;
        while ( ![self validateValue:&validName forKey:@"name" error:nil] )
        {
            index++;
            validName = [defaultName stringByAppendingFormat:@" (%d)",
    index];
        }
        [self setName:validName];
    }

    - (BOOL)validateName: (id *)valueRef error:(NSError **)outError
    {
    /*
    Check (SELF != %@) in fetch; use 'LIKE' not '==' for string comparison
    */
        NSPredicate * predicate = [NSPredicate
    predicateWithFormat:@"(SELF != %@) AND (name LIKE %@)", self,
    *valueRef];
        NSFetchRequest * fetchRequest = [[NSFetchRequest alloc] init];
    /*
    Use self's entity -- no need to use NSEntityDescription method to
    retrieve (also avoids hard-wiring entity name)
    */
        [fetchRequest setEntity:[self entity]];
        [fetchRequest setPredicate:predicate];

    /*
    Should check for error
    */
        NSArray * results = [[self managedObjectContext]
    executeFetchRequest:fetchRequest error:nil];

        [fetchRequest release];

        return ( [results count] < 1 );
    }

    > And do you think I'm using the right process to validate my name
    > property and mak sure my object are inserted with valid name ?

    This approach will be very inefficient for anything but data sets
    where there is little likelihood of name collisions -- you have to do
    an additional fetch for every collision.  It's not clear what your
    goal is here.  If you're simply wanting to set temporary distinct
    names (like window titles) for small numbers of objects, this would
    probably be OK.  If these will be permanent values for an ever-
    increasing number of objects, then you'll need a different approach...

    mmalc
  • Thanks for such a great explanation ! I'll try it ass soon as I've
    typed this mail.

    > This approach will be very inefficient for anything but data sets
    > where there is little likelihood of name collisions -- you have to
    > do an additional fetch for every collision.  It's not clear what
    > your goal is here.  If you're simply wanting to set temporary
    > distinct names (like window titles) for small numbers of objects,
    > this would probably be OK.  If these will be permanent values for
    > an ever-increasing number of objects, then you'll need a different
    > approach...

    Well, that's simple : just like in iTunes, when you create two or
    three  playlist without renaming them, the playlist name is append
    with "(index++). I need the same kind of behavior : in my app, user
    can add managed objects via a "Add" button and I want the name of the
    added object to "increase" automatically. Does it seem a bad idea to
    you ?

    How am I supposed to do if I want to be sure that a property is
    unique for a given entity ?

    Eric.

    Le 5 sept. 05 à 21:07, mmalcolm crawford a écrit :

    >
    > On Sep 5, 2005, at 9:42 AM, Eric Morand wrote:
    >
    >> I have a managed object with one property named "name". This
    >> property is a string. I want this property to be unique in the
    >> managed context of the object, so I've implemented
    >> "validateName..." method in my managed object subclass (see code
    >> below).
    >> Next, I want new objects inserted into the context to have a name
    >> set to a validated one (that means : unique). So I have
    >> implemented "awakeFromInsert..." method in my subclass that set
    >> the name property to a valid one.
    >> [...]
    >> Here is the code for my custom class (implementation only, there
    >> is nothing of interest in the header) :
    >> ]...]
    >> - (void)awakeFromInsert
    >> {
    >> [...]
    >> NSString * validName = nil;
    >> NSString * defaultName = [NSString
    >> stringWithString:@"TestObjectName"];
    >>
    >> validName = defaultName;
    >>
    >> int index = 0;
    >>
    >> while ( ![self validateValue:&validName forKey:@"name"
    >> error:nil] )
    >> {
    >> index++;
    >>
    >> validName = [defaultName stringByAppendingFormat:@" (%d)",
    >> index];
    >> }
    >>
    >> [self setName:validName];
    >> }
    >>
    >> - (BOOL)validateName: (id *)valueRef error:(NSError **)outError
    >> {
    >> NSPredicate * predicate = [NSPredicate
    >> predicateWithFormat:@"name == %@", *valueRef];
    >>
    >> NSEntityDescription * entityDescription = [NSEntityDescription
    >> entityForName:@"TestObject" inManagedObjectContext:[self
    >> managedObjectContext]];
    >> NSFetchRequest * fetchRequest = [[[NSFetchRequest alloc] init]
    >> autorelease];
    >>
    >> [fetchRequest setEntity:entityDescription];
    >> [fetchRequest setPredicate:predicate];
    >>
    >> NSArray * results = [[self managedObjectContext]
    >> executeFetchRequest:fetchRequest error:nil];
    >>
    >> return ( [results count] < 1 ) || ( [results objectAtIndex:0]
    >> == self );
    >> }
    >>
    >> @end
    >>
    >> I'm using a simple interface to test the insertion process : in
    >> fact it is the standard interface created automatically when alt-
    >> dragging the object from Xcode to IB - one table view, 3 buttons
    >> (Fetch, Add, Remove), a text field to enter name and a search
    >> field. Then, the problems begin : when I click on the add button,
    >> TWO objects are added to the table view !
    >>
    >>
    > I'm not sure if this is a bug (I'll follow up) but it's because
    > you're executing a fetch (whose results will include self) before
    > the insertion has completed.  You can sidestep this by setting the
    > name after awakeFromInsert:
    >
    > - (void)awakeFromInsert
    > {
    > [super awakeFromInsert];
    > [self performSelector:@selector(setUniqueName) withObject:nil
    > afterDelay:0];
    > }
    >
    > There are several issues with the remainder of the awakeFromInsert
    > code (now moved to the new method) and the validation method:
    >
    >
    > - (void)setUniqueName
    > {
    > NSString * defaultName = @"TestObjectName"; // no need for
    > stringWithString:
    > NSString * validName = defaultName; // no need for two assignments
    > int index = 0;
    > while ( ![self validateValue:&validName forKey:@"name"
    > error:nil] )
    > {
    > index++;
    > validName = [defaultName stringByAppendingFormat:@" (%d)",
    > index];
    > }
    > [self setName:validName];
    > }
    >
    >
    > - (BOOL)validateName: (id *)valueRef error:(NSError **)outError
    > {
    > /*
    > Check (SELF != %@) in fetch; use 'LIKE' not '==' for string comparison
    > */
    > NSPredicate * predicate = [NSPredicate
    > predicateWithFormat:@"(SELF != %@) AND (name LIKE %@)", self,
    > *valueRef];
    > NSFetchRequest * fetchRequest = [[NSFetchRequest alloc] init];
    > /*
    > Use self's entity -- no need to use NSEntityDescription method to
    > retrieve (also avoids hard-wiring entity name)
    > */
    > [fetchRequest setEntity:[self entity]];
    > [fetchRequest setPredicate:predicate];
    >
    > /*
    > Should check for error
    > */
    > NSArray * results = [[self managedObjectContext]
    > executeFetchRequest:fetchRequest error:nil];
    >
    > [fetchRequest release];
    >
    > return ( [results count] < 1 );
    > }
    >
    >
    >
    >
    >> And do you think I'm using the right process to validate my name
    >> property and mak sure my object are inserted with valid name ?
    >>
    >
    > This approach will be very inefficient for anything but data sets
    > where there is little likelihood of name collisions -- you have to
    > do an additional fetch for every collision.  It's not clear what
    > your goal is here.  If you're simply wanting to set temporary
    > distinct names (like window titles) for small numbers of objects,
    > this would probably be OK.  If these will be permanent values for
    > an ever-increasing number of objects, then you'll need a different
    > approach...
    >
    > mmalc
    >
    > _______________________________________________
    > Do not post admin requests to the list. They will be ignored.
    > Cocoa-dev mailing list      (<Cocoa-dev...>)
    > Help/Unsubscribe/Update your Subscription:
    > http://lists.apple.com/mailman/options/cocoa-dev/<eric.morand...>
    >
    > This email sent to <eric.morand...>
    >
  • Le 5 sept. 05 à 21:07, mmalcolm crawford a écrit :

    > This approach will be very inefficient for anything but data sets
    > where there is little likelihood of name collisions -- you have to
    > do an additional fetch for every collision.  It's not clear what
    > your goal is here.  If you're simply wanting to set temporary
    > distinct names (like window titles) for small numbers of objects,
    > this would probably be OK.  If these will be permanent values for
    > an ever-increasing number of objects, then you'll need a different
    > approach...
    >
    > mmalc
    >

    What kind of approach you think ?
    I have the same goal i want make  unique a propertie but i don't find
    solution for this.
    I have think to perform this verification in the add methods of the
    coressponding controller but i'm not sure it a good solution. Do you
    think its good or not ? and what it is the best solution ?
  • On Sep 5, 2005, at 1:08 PM, Eric Morand wrote:

    > Well, that's simple : just like in iTunes, when you create two or
    > three  playlist without renaming them, the playlist name is append
    > with "(index++). I need the same kind of behavior : in my app, user
    > can add managed objects via a "Add" button and I want the name of
    > the added object to "increase" automatically. Does it seem a bad
    > idea to you ?
    >
    No, not at all, the idea is good, it's the implementation that might
    be sub-optimal.

    > How am I supposed to do if I want to be sure that a property is
    > unique for a given entity ?
    >
    The validation method is fine.

    The way you generate the new names might be improved -- executing an
    additional fetch for each collision is a (comparatively) huge
    overheard.  If you're creating small numbers (< 10) of unique names
    like this, then you can probably get away with this, otherwise the
    approach you take will depend on exactly the behaviour you want.  If
    all you want is for the name to be unique, then you might for example
    create a new entity that stores just a number, and that represents
    the current highest value.  You just have to retrieve that.  If the
    user is forced to give a different name, then you might do something
    similar just in memory without another entity.  Worst case scenario
    would be to use the same approach you have at the moment but fetch
    all the current instances in one go and iterate through them in
    memory...

    mmalc
previous month september 2005 next month
MTWTFSS
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
Go to today
MindNode
MindNode offered a free license !