Returning deeper relations in Parse Cloud Code whe

2019-08-09 22:38发布

问题:

I have received some excellent information on Parse promises so far and am ready to soak up the next bit of the puzzle to me... relations of relations. The following code is in most part in answering Issues with Parse Cloud Code Promises but I have in the comments of the sections piece of code my next question. How would I go about further querying of relations within the top level relations? would be great to see how you would extend you code to do that @roamer-1888

Latest:

//Return a single project
Parse.Cloud.define('getProjectByUrl', function(request, response) {
    var Projects = Parse.Object.extend("projects"); // with credit to @kRiZ
    var query = new Parse.Query(Projects);
    query.equalTo('projectUrl', request.params.projectUrl);
    query.include("projectStatus"); // *MJ Added the include to add the project Status pointer
    query.find().then(function(projectsResult) {
        var project = projectsResult[0];
        //From here, `project` is hierarchically monkey-patched with the results of further queries.

        //Make tags query, the referees query and the sections query in parallel.
        var tagsPromise = project.relation('tags').query().find();
        var refereesPromise = project.relation('referees').query().find();
        var sectionsPromise = project.relation('sections').query().include("sectionType").find(); //*MJ Added the include of the sectionType pointer

        // Aggregate the three promises with Parse.Promise.when(), and handle the responses.
        return Parse.Promise.when(tagsPromise, refereesPromise, sectionsPromise).then(function(tags, referees, sections) {
            project.set('tags', tags);
            project.set('referees', referees);
            project.set('sections', sections);

            //Go deeper into `sections`
            var sectionsRelationsPromises = sections.map(function(section) {
                // *MJ Remove sectionTypesPromise as it's a pointer
                var filesPromise = section.relation('files').query().include("fileType").find(); // *MJ Added the include to grab the fileType pointer
                return Parse.Promise.when(filesPromise).then(function(files) {
                    //*MJ Removed the promise for section Types
                    section.set('files', files);
                    // *MJ Removed the Deeper Logic
                });
            });
            return Parse.Promise.when(sectionsRelationsPromises);
        }).then(function() {
            return project;
        });
    }).then(function(project) {
        // At this point, all the data is gathered in the form of Parse objects in project,
        // which now needs to be mapped into a js plain object.
        var projectData = projectObj(project);
        projectData.tags = project.get('tags').map(tagObj); //Process the tags response
        projectData.referees = project.get('referees').map(refereeObj); // *MJ removed one e from refereeObj //Process the referees response
        projectData.sections = project.get('sections').map(sectionObj); //Process the sections response
        // *MJ Removed the adding of the files and looking over the sections.

        //Yay! (hopfully)
        response.success(projectData);
    }).fail(function(error) {
        response.error('Error: ' + error);
    });

    // ***********************************
    // ***** start: mapper functions *****
    // ***********************************
    function projectObj(project) {
        return { // *MJ Removed the stray equals sign here
            'id': project.id,
            'title': project.get('title'),
            'previewImage': project.get('previewImage'),
            'longDescription': project.get('longDescription'),
            'shortDescription': project.get('shortDescription'),
            'visibleToPublic': project.get('visibleToPublic'),
            'dateStart': project.get('dateStart'),
            'dateEnd': project.get('dateEnd'),
            'updatedAt': project.get('updatedAt'),
            "projectStatus": project.get("projectStatus").get("status") //*MJ Added the get of the Project Status status.
        }
    }
    function tagObj(tag) {
        return {
            'tag': tag.get('tag')
        };
    }
    function refereeObj(referee) {
        return {
            'name': referee.get('name'),
            'role': referee.get('role'),
            'emailAddress': referee.get('emailAddress'),
            'phoneNumber': referee.get('phoneNumber'),
            'linkedInUrl': referee.get('linkedInUrl')
        };
    }
    function sectionObj(section) {
        return {
            'type': section.get('sectionType').get("type"), // *MJ Added the pointer for SectionType > type
            'order': section.get('order'),
            'content': section.get('content'),
            'files': section.get('files').map(fileObj)
        };
    }
    function fileObj(file) {
        return {
            'name': file.get('name'), // *MJ chnaged the name to be more appropriate
            'url': file.get('url'), // *MJ Added the missing comma
            'type': file.get('fileType').get("type") //*MJ Added the pointer for fileType > type and removed semi colon
        };
    }
    // *********************************
    // ***** fin: mapper functions *****
    // *********************************
});

Old:

//Return a single project
Parse.Cloud.define('getProject', function(request, response) {
    var Projects = Parse.Object.extend("projects"); // with credit to @kRiZ
    var query = new Parse.Query(Projects);
    query.equalTo('projectUrl', request.params.projectUrl);
    query.find().then(function(projectsResult) {
        var project = projectsResult[0];
        var projectData = {
            'id': project.id,
            'title': project.get('title'),
            'previewImage': project.get('previewImage'),
            'longDescription': project.get('longDescription'),
            'shortDescription': project.get('shortDescription'),
            'visibleToPublic': project.get('visibleToPublic'),
            'dateStart': project.get('dateStart'),
            'dateEnd': project.get('dateEnd'),
            'updatedAt': project.get('updatedAt')
        };

        //Now make the tags query and the referees query in parallel.
        var tagsPromise = project.relation('tags').query().find();
        var refereesPromise = project.relation('referees').query().find();
        var sectionsPromise = project.relation('sections').query().find();
        var sectionTypesPromise = project.relation('sections').query().find().relation('type').query().find();
        var filesPromise = project.relation('sections').query().find().relation('files').query().find();
        var fileTypesPromise = project.relation('sections').query().find().relation('files').query().find().relation('type').query().find();


        // Aggregate the two promises with Parse.Promise.when(), and handle the responses.
        return Parse.Promise.when(tagsPromise, refereesPromise, sectionsPromise, sectionTypesPromise, filesPromise, fileTypesPromise).then(function(tags, referees, sections, sectionTypes, files, fileTypes) {
            //Process the tags response
            projectData.tags = tags.map(function(tag) {
                return {
                    'tag': tag.get('tag')
                };
            });
            //Process the referees response
            projectData.referees = referees.map(function(refereee) {
                return {
                    'name': refereee.get('name'),
                    'role': refereee.get('role'),
                    'emailAddress': refereee.get('emailAddress'),
                    'phoneNumber': refereee.get('phoneNumber'),
                    'linkedInUrl': refereee.get('linkedInUrl')
                };
            });

            //Process the sections response
            projectData.sections = sections.map(function(section) {
                return {
                    'order': section.get('order'),
                    'content': section.get('content')
                };
            });

            // Problem: Sections have a relations column (files)
            // which I would like to return as a child of sections.
            // The files class then has a pointer column (type)
            // to another class which contains the a list of
            // file types (i.e. Image, Document, etc...)
            // The sections structure of projectDate should
            // look like:
            //
            // "sections": [{
            //   "type": "Section Type"
            //   "order": "1",
            //   "content": "This is the Section content",
            //   "files": [{
            //     "filename": "Image 1",
            //     "url": "image-1.png"
            //     "type": "Image"
            //   },
            //   {
            //     "filename": "Image 2",
            //     "url": "image-2.png",
            //     "type": "Image"
            //   },
            //   {
            //     ...
            //   }]
            //  },
            //  {
            //    ...
            //  }
            // }]

            //Process the section type response. This is reliant on sections being retrieved.
            projectData.sections.type = sections.map(function(sectionTypes) {
                return {
                    'type': sectionTypes.get('type')
                };
            });

            //Process the section files response. This is reliant on sections being retrieved.
            projectData.sections.files = sections.map(function(files) {
                return {
                    'filename': files.get('filename'),
                    'url': files.get('url')
                };
            });

            //Process the section files types response. This is reliant on files being retrieved.
            projectData.sections.files.type = sections.map(function(fileTypes) {
                return {
                    'type': fileTypes.get('type')
                };
            });
            // Currently not so Yay!

            response.success(projectData);
        });
    }).fail(function(error) {
        response.error('Error: ' + error);
    });
});

回答1:

Going deeper than one level is slightly more complicated. Each level needs a pattern much like the top level pattern.

Unfortunately, these patterns don't naturally lend themselves to being "flat". The need to constantly refer back to earlier results makes nesting attractive, though probably not absolutely necessary; I'm sure that other approaches exist.

Here's an attempt at the code. With no means of testing it, there may well be mistakes.

//Return a single project
Parse.Cloud.define('getProject', function(request, response) {
    var Projects = Parse.Object.extend("projects"); // with credit to @kRiZ
    var query = new Parse.Query(Projects);
    query.equalTo('projectUrl', request.params.projectUrl);
    query.find().then(function(projectsResult) {
        var project = projectsResult[0];
        //From here, `project` is hierarchically monkey-patched with the results of further queries.

        //Make tags query, the referees query and the sections query in parallel.
        var tagsPromise = project.relation('tags').query().find();
        var refereesPromise = project.relation('referees').query().find();
        var sectionsPromise = project.relation('sections').query().find();

        // Aggregate the three promises with Parse.Promise.when(), and handle the responses.
        return Parse.Promise.when(tagsPromise, refereesPromise, sectionsPromise).then(function(tags, referees, sections) {
            project.set('tags', tags);
            project.set('referees', referees);
            project.set('sections', sections);

            //Go deeper into `sections`
            var sectionsRelationsPromises = sections.map(function(section) {
                var sectionTypesPromise = section.relation('type').query().find();
                var filesPromise = section.relation('files').query().find();
                return Parse.Promise.when(sectionTypesPromise, filesPromise).then(function(sectionTypes, files) {
                    section.set('type', sectionTypes[0]);
                    section.set('files', files);
                    //And deeper still into each of the section's files to find their types
                    var filesRelationsPromises = files.map(function(file) {
                        return file.relation('type').query().find().then(function(fileTypes) {
                            file.set('type', fileTypes[0]);
                        });
                    });
                    return Parse.Promise.when(filesRelationsPromises);
                });
            });
            return Parse.Promise.when(sectionsRelationsPromises);
        }).then(function() {
            return project;
        });
    }).then(function(project) {
        // At this point, all the data is gathered in the form of Parse objects in project,
        // which now needs to be mapped into a js plain object.
        var projectData = projectObj(project);
        projectData.tags = project.get('tags').map(tagObj); //Process the tags response
        projectData.referees = project.get('referees').map(refereeeObj); //Process the referees response
        projectData.sections = project.get('sections', sections).map(sectionObj); //Process the sections response
        projectData.sections.each(function(section) {
            section.files = section.get('files').map(fileObj);
        });
        //Yay! (hopfully)
        response.success(projectData);
    }).fail(function(error) {
        response.error('Error: ' + error);
    });

    // ***********************************
    // ***** start: mapper functions *****
    // ***********************************
    function projectObj(project) {
        return = {
            'id': project.id,
            'title': project.get('title'),
            'previewImage': project.get('previewImage'),
            'longDescription': project.get('longDescription'),
            'shortDescription': project.get('shortDescription'),
            'visibleToPublic': project.get('visibleToPublic'),
            'dateStart': project.get('dateStart'),
            'dateEnd': project.get('dateEnd'),
            'updatedAt': project.get('updatedAt')
        }
    }
    function tagObj(tag) {
        return {
            'tag': tag.get('tag')
        };
    }
    function refereeObj(referee) {
        return {
            'name': referee.get('name'),
            'role': referee.get('role'),
            'emailAddress': referee.get('emailAddress'),
            'phoneNumber': referee.get('phoneNumber'),
            'linkedInUrl': referee.get('linkedInUrl')
        };
    }
    function sectionObj(section) {
        return {
            'type': section.get('type'),
            'order': section.get('order'),
            'content': section.get('content'),
            'files': section.get('files').map(fileObj)
        };
    }
    function fileObj(file) {
        return {
            'filename': file.get('filename'),
            'url': file.get('url')
            'type': file.get('type');
        };
    }
    // *********************************
    // ***** fin: mapper functions *****
    // *********************************
});

I've made the code as simple as possible to follow by :

  • limiting the first .then's callback to gathering data asynchronously.
  • orchestrating the mapping of the gathered data in a subsequent .then's callback.
  • pulling out the individual mapper functions into a separate section at the end.

Trust me, that is more readable than doing everything in a single block. I tried that, and it was nightmarish.

Happy debugging.



回答2:

To treat the question generally: say you have an class called "ClassA" and it has a relation called "classBs" relating it to many instances of "ClassB". And say, also, that "ClassB" instances have a relation called "classCs" relating them to instances of "ClassC".

ClassA --relates to many --< ClassB --relates to many --< ClassC
           ("classBs")                 ("classCs") 

Now, given a single instance of "ClassA", how would one go about fetching all of the "ClassC" instances related to it via "ClassB"?

The answer is straight-forward, remembering a few things: (1) spell out the data model very clearly as done here, (2) compose more complex logic out of smaller, simpler promise-returning primitives (3) use Parse.Promise.or to execute the union of multiple queries.

var _ = require("underscore");

// return a promise for many instances of ClassC related to the given aObject
function getCsFromA(aObject) {
    var bRelation = aObject.get("classBs");
    return bRelation.query().find().then(function(bObjects) {
        // gather all of the bObject's relation's queries
        var  queries = _.map(bObjects, function(bObject) {
            return aObject.get("classCs").query();
        });
        // get the union of many queries
        return Parse.Query.or(queries);
    });
}

A couple practical points: queries are by default limited to 100, and you can change the limit up to 1000. At maximum, a thousand relations of a thousand relations is a million results. This will certainly violate some resource constraints at parse.com.