Thank you for reporting this bug. We will make it our priority to review this report.
  1. #1
    Ext GWT Premium Member
    Join Date
    Oct 2009
    Posts
    10
    Vote Rating
    2
    Tom Haller is on a distinguished road

      0  

    Exclamation 4.1-B2: HasOne constructor does not work

    4.1-B2: HasOne constructor does not work


    The HasOne constructor incorrectly uses the class name of the contained model to generate getter, setter and other members. This causes two problems:
    1. If the contained model name is namespaced, the generated member names have periods in them and are invalid.
    2. The same model cannot be contained more than once; this makes doing something reasonable like having a model include two address instances (work and home) impossible.
    Here is a complete example. The first attempt to retrieve one of the child model instances will fail, as the getters are not created correctly:

    Code:
    // Contained model.
    Ext.define('MyApp.Address', {
        extend: 'Ext.data.Model',
        fields: [ 'street', 'city' ]
    });
     
    // Containing model.
    Ext.define('MyApp.Person', {
        extend: 'Ext.data.Model',
        fields: [ 'id', 'name' ],
        associations: [
            { type: 'hasOne', model: 'MyApp.Address', name: 'work' },
            { type: 'hasOne', model: 'MyApp.Address', name: 'home' }
        ],
        proxy: {
            type: 'memory',
            data: {
                success: true,
                user: {
                   id: 1,
                   name: 'Mickey Mouse',
                   work: {
                       street: 'Main Street',
                       city: 'Anytown'
                   },
                   home: {
                       street: 'Easy Street',
                       city: 'Nowheresville'
                   }
                }
            },
            reader: {
                type: 'json',
                root: 'user',
                successProperty: 'success'
            }
        }
    });
     
    Ext.application({
        launch: function () {
            MyApp.Person.load(1, {
                success: function (person) {
                    console.log('person:');
                    console.log('  id   = ' + person.get('id'));
                    console.log('  name = ' + person.get('name'));
     
                    var work = person.getWork();
     
                    console.log('  work:');
                    console.log('    city   = ' + work.get('city'));
                    console.log('    street = ' + work.get('street'));
     
                    var home = person.getHome();
     
                    console.log('  home:');
                    console.log('    city   = ' + home.get('city'));
                    console.log('    street = ' + home.get('street'));
                }
            });
        }
    });
    
    Replacing the constructor of HasOne with the following produces the correct behavior:

    Code:
    constructor: function(config) {
        this.callParent(arguments);
     
        var me             = this,
            ownerProto     = me.ownerModel.prototype,
            associatedName = me.associatedName;
     
        // If name not supplied, strip periods from possibly namespaced model name and use that.
        // NOTE: this does not account for same model being used for more than one "has one".
        Ext.applyIf(me, {
            name : associatedName.replace(/\./g, "")
        });
     
        var name = me.name;
     
        // Generate members from "name", NOT "model" (associatedName).
        Ext.applyIf(me, {
            foreignKey     : name.toLowerCase() + "_id",
            instanceName   : name + 'HasOneInstance',
            associationKey : name.toLowerCase()
        });
     
        // e.g. if name is "myChild", generate getMyChild() and setMyChild().
        getterName = me.getterName || 'get' + name.substr(0, 1).toUpperCase() + name.substr(1);
        setterName = me.setterName || 'set' + name.substr(0, 1).toUpperCase() + name.substr(1);
     
        ownerProto[getterName] = me.createGetter();
        ownerProto[setterName] = me.createSetter();
    }
    

    I originally opened a ticket with support for this, but was directed to post the issue here in the forum instead. The ticket is here:
    http://support.extjs.com/index.php#ticket-7171
    Last edited by Tom Haller; 10 Feb 2012 at 7:15 PM. Reason: code samples are mangled

  2. #2
    Sencha Premium Member
    Join Date
    Dec 2009
    Location
    Rhode Island
    Posts
    223
    Vote Rating
    22
    dmulcahey will become famous soon enough dmulcahey will become famous soon enough

      0  

    Default


    I just started messing with this myself. One thing I did notice is that it uses getterName and setterName configs instead of name... Did you try those? I have the hasOne association working but I have not tried to use the same model more than once yet. I'll give it a shot and post my result.

  3. #3
    Ext GWT Premium Member
    Join Date
    Oct 2009
    Posts
    10
    Vote Rating
    2
    Tom Haller is on a distinguished road

      0  

    Default


    Thank you for that suggestion; I should have tried that before!

    Specifying getterName and setterName explicitly in the config does solve the problem of naming those members, but unfortunately there is also another member, instanceName, which is generated and used internally as the name of the contained model instance. It is still based on the contained model's class name regardless of how you specified getterName/setterName; so both problems I mentioned still happen.

  4. #4
    Sencha Premium Member
    Join Date
    Dec 2009
    Location
    Rhode Island
    Posts
    223
    Vote Rating
    22
    dmulcahey will become famous soon enough dmulcahey will become famous soon enough

      0  

    Default


    Ok, just had a chance to try adding 2 hasOne associations of the same type to the same model and because of the way the internal instance is stored they do not work as expected. Both getters return the same instance of the model so I agree with Tom on point 2.

    I'm not so sure that the property names are invalid though (Point 1). it seems that they can be accessed via ['namespace.model.modelNameHasOneInstance'] on the model which is similar to how the filters are passed around for the grids if i am remembering correctly. It's a bit confusing to look at at first but i wouldn't say it is invalid.

  5. #5
    Ext GWT Premium Member
    Join Date
    Oct 2009
    Posts
    10
    Vote Rating
    2
    Tom Haller is on a distinguished road

      0  

    Default


    I see what you're saying about using the bracket notation instead of dot notation to get at the default getters/setters, and I can override those names in the config, so you're right, that in itself is not a show-stopper.

    However using a namespaced class name internally for the instance name still prevents HasOne from creating the child instance if the retrieved data already includes the children. So when you execute the getter you get this error, because it failed to create the instance, and it thinks it needs another server call to retrieve the child:

    Uncaught Ext.Error: You are using a ServerProxy but have not supplied it with a url.

    I tried using alternateClassName in the define of the child so I could use a model name without periods, but HasOne can't find the model class and silently fails to create the association.

    Last edited by Tom Haller; 11 Feb 2012 at 6:37 AM. Reason: clarify

  6. #6
    Sencha Premium Member
    Join Date
    Dec 2009
    Location
    Rhode Island
    Posts
    223
    Vote Rating
    22
    dmulcahey will become famous soon enough dmulcahey will become famous soon enough

      0  

    Default


    Is this in memory data you are working with? if so put a memory proxy on the model pointed to by the hasOne association. I have hasOne associations working.... The error you are seeing there is because the default proxy type for models is Ajax. I'm assuming your model that is referenced by the hasOne association has no proxy on it so the default of Ajax is kicking in and attempting to send a request to a URL that was never initialized. Give me a few and i'll see if i can strip a small example out of my application for you.

  7. #7
    Ext GWT Premium Member
    Join Date
    Oct 2009
    Posts
    10
    Vote Rating
    2
    Tom Haller is on a distinguished road

      0  

    Default


    Yes, for my test I'm using a memory proxy (see example I posted in the beginning of the thread). I'm using a rest proxy for the parent model in my actual app, my server returns a complete object graph, and I have hasMany relationships defined and can access the children using generated methods on the parent without having to define proxies on the children. I also tested hasMany with a memory proxy for the parent and no proxy for the children and it worked.
    Last edited by Tom Haller; 11 Feb 2012 at 6:47 AM. Reason: add

  8. #8
    Sencha Premium Member
    Join Date
    Dec 2009
    Location
    Rhode Island
    Posts
    223
    Vote Rating
    22
    dmulcahey will become famous soon enough dmulcahey will become famous soon enough

      0  

    Default


    Ok, Shame on me for not looking at your example a bit more....

    I have modified your example to show you that they do work..... but that one of your previous points was 100% correct! Here is the code:

    Code:
    <!DOCTYPE html>
    <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Data test</title>
        <!--  link rel="stylesheet" type="text/css" href="ext407/resources/css/ext-all.css"/-->
        <link rel="stylesheet" href="ext41b2/resources/css/ext-all.css"/>
        <script src="ext41b2/ext-all-dev.js"></script>
        <script src="index.js"></script>
      </head>
      <body>
      </body>
    </html>
    
    // Contained model.
    Ext.define('MyApp.Address', {
        extend: 'Ext.data.Model',
        fields: [ 'street', 'city' ],
        proxy: {
            type: 'memory',
            reader: {
                type: 'json'
            }
        }
    });
     
    // Containing model.
    Ext.define('MyApp.Person', {
        extend: 'Ext.data.Model',
        fields: [ 'id', 'name' ],
        associations: [
            { 
                type: 'hasOne', 
                model: 'MyApp.Address', 
                getterName: 'getWorkAddress',
                setterName: 'setWorkAddress',
                associationKey: 'work' 
            },
            { 
                type: 'hasOne', 
                model: 'MyApp.Address', 
                getterName: 'getHomeAddress',
                setterName: 'setHomeAddress',
                associationKey: 'home'  
            }
        ],
        proxy: {
            type: 'memory',
            data: {
                success: true,
                user: {
                   id: 1,
                   name: 'Mickey Mouse',
                   work: {
                       street: 'Main Street',
                       city: 'Anytown'
                   },
                   home: {
                       street: 'Easy Street',
                       city: 'Nowheresville'
                   }
                }
            },
            reader: {
                type: 'json',
                root: 'user',
                successProperty: 'success'
            }
        }
    });
     
    Ext.application({
        launch: function () {
            MyApp.Person.load(1, {
                success: function (person) {
                    console.log('person:');
                    console.log('  id   = ' + person.get('id'));
                    console.log('  name = ' + person.get('name'));
     
                    var work = person.getWorkAddress();
     
                    console.log('  work: ' + work.id);
                    console.log('    city   = ' + work.get('city'));
                    console.log('    street = ' + work.get('street'));
     
                    var home = person.getHomeAddress();
     
                    console.log('  home: ' + home.id);
                    console.log('    city   = ' + home.get('city'));
                    console.log('    street = ' + home.get('street'));
                }
            });
        }
    });

    here is the firebug console output:

    Code:
    person:
      id   = 1
      name = Mickey Mouse
      work: MyApp.Address-ext-record-1
        city   = Nowheresville
        street = Easy Street
      home: MyApp.Address-ext-record-1
        city   = Nowheresville
        street = Easy Street
    Note: the id returned by work and home is the same! this is a problem... ( MyApp.Address-ext-record-1 ) and i think it is because of the way the reference is added to the contained model. One way to fix this on your end would be to make the address a has many association and add a type property to the address model.

    Now the semantics of hasOne vs hasMany are a bit different. hasMany associations place a store on the model to hold the associated model instances where as the hasOne associations are just placing model instances on the model instance. This could be the reason they work slightly different. Also, I'm not quite sure the hasOne association is finished.... I don't think it existed until one of the recent betas

  9. #9
    Ext GWT Premium Member
    Join Date
    Oct 2009
    Posts
    10
    Vote Rating
    2
    Tom Haller is on a distinguished road

      0  

    Default


    Thanks, Dave! This makes HasOne usable where a model is only contained once; that is actually my use case, but I just happened to notice this wouldn't work where the same model is contained twice, so I wanted any forthcoming fix to deal with that as well. Indeed you can simulate it with a store and type column, but that moves part of the implementation from the model into the data.

    I know HasOne and HasMany has to be implemented differently, but from the outside I see it just as a difference in cardinality, so I would've expected them work pretty much the same - in the one case you get a model instance returned, in the other you get a store.

    When I was researching this I found that HasOne was added by popular demand as an afterthought. I don't think it was completely thought through or tested thoroughly, and certainly the documentation does not cover it adequately.

    I would still prefer the behavior resulting from my constructor rewrite, which makes HasOne work like HasMany (i.e. provide a "name" and the constructor uses that), and it works without having to add a memory proxy to the child model.

    Many thanks for your insightful responses.

  10. #10
    Sencha Premium Member
    Join Date
    Dec 2009
    Location
    Rhode Island
    Posts
    223
    Vote Rating
    22
    dmulcahey will become famous soon enough dmulcahey will become famous soon enough

      0  

    Default


    np and I agree the implementation is kind of odd.... It doesn't work as expected in many places and i do not think it is finished yet. I stumbled over it for a bit myself until i went through the code. I am going to post a few things that i think are issues once i finish going through it a bit more.