DGraph Advanced Data Modeling: Part 2 - Cascade Delete

Jonathan Gamble - Nov 28 '21 - - Dev Community

I have searched through other Graph Databases, and I can't find anything similar to SQL's Cascade Delete (nor Nested Updates). Even with Cypher, it seems the recommended solution is to just run a query to delete the connected nodes. Of course this can be done, it is just not forcing the database to always be up-to-date with a constraint. DGraph does have certain constraints built in (look at reverse edges in DQL).

I kind of disagree with my proposed solution. The constraint should be put directly into DQL, and not on the GraphQL side. I believe this would be faster, safer, and ensure data integrity. Manish, if you ever read this, I believe these kind of things should be done on the database end. We should learn from SQL, and keep things consistent. That being said, pre-hooks would also make for a cleaner solution similar to this if my GraphQL solution never makes it to production.

That being said, let's continue with the current solution. These are first world problems to have when you come from a noSQL database... hehe.

Solution

Like the previous post, you can solve this problem too with a custom mutation. However, we don't want to render our regular add / delete useless.

Create your Lambda Webhook and put your functions in it. See the previous post for this.

async function postHook({ event, dql }) {

    // update timestamps
    await updateTimestamps({ event, dql });

    // cascade delete
    await cascadeDelete({ event, dql, nodes: ['private'] });

    // deep update
    await deepUpdate({ event, dql, nodes: ['private'] });
}

(self as any).addGraphQLResolvers({
    "Mutation.toggleVote": toggleVote
});

(self as any).addWebHookResolvers({
    "User.add": updateTimestamps,
    "User.update": updateTimestamps,
    "User.delete": updateTimestamps
});

(self as any).addWebHookResolvers({
    "Post.add": postHook,
    "Post.update": postHook,
    "Post.delete": postHook
});
Enter fullscreen mode Exit fullscreen mode

Add the fields to the nodes array that you wish to update. I did not write code for nested-nested deletes (2 levels or more deep), but you can see how this would work. If I have a need for it one day, I may update the code on this post.

await cascadeDelete({ event, dql, nodes: ['stats', 'levels'] });
Enter fullscreen mode Exit fullscreen mode

I also thought about just updating one level, and having that level update its nested level delete. You could technically do this using graphql instead of dql. The problem is that graphql does not support vars or nested filters (needed in this case for a delete query). That would mean you would need two queries to get the ids, then delete them. If you have thousands of nested items, it is way quicker to stick with DQL. If not, you could just trigger the query, which triggers another query, which triggers another delete using graphql (dql does not trigger anything). Just FWI for those advanced users.

async function cascadeDelete({ event, dql, nodes }) {

    const op = event.operation;
    const uid = event[event.operation].rootUIDs[0];
    const invType = (event.__typename as string).toLowerCase();
    const type: string = event.__typename;

    const titleCase = (t: string) =>
        t.charAt(0).toUpperCase()
        + t.substring(1).toLowerCase();

    let args: any;

    if (op === 'delete') {

        // get inverse relationships, delete them
        args = `upsert { query { `;
        for (let i = 0; i < nodes.length; ++i) {
            const child = titleCase(nodes[i]);
            args += `t${i} as var(func: type(${child}))
            @filter(uid_in(${child}.${invType}, ${uid})) `;
        }
        args += `} mutation { delete { `;
        for (let i = 0; i < nodes.length; ++i) {
            args += `uid(t${i}) * * . `;
        }
        args += `} } }`;

    } else if (op === 'add') {

        // creates inverse relationships
        args = `upsert { query { q(func: uid(${uid})) { `;
        for (let i = 0; i < nodes.length; ++i) {
            args += `t${i} as 
            ${type}.${(nodes[i] as string).toLowerCase()} `;
        }
        args += `} } mutation { set { `;
        for (let i = 0; i < nodes.length; ++i) {
            args += `uid(t${i}) 
            <${titleCase(nodes[i])}.${invType}> <${uid}> . `
        }
        args += `} } }`;
    }
    const r = await dql.mutate(args);
    console.log(r);
}
Enter fullscreen mode Exit fullscreen mode

And just like the case in the previous post, DO NOT use @hasInverse on the fields you want to do Cascade Delete.

type Post @lambdaOnMutate(add: true, update: true, delete: true)
@auth (
... your rules here...
) {
  id: ID!
  name: String! @search(by:[fulltext,term,hash])
  timestamp: Timestamp
  link: Link!
  nested: [Nested]
}

type Nested {
  id: ID!
  text: String!
  post: Post!
}
Enter fullscreen mode Exit fullscreen mode

You must also keep the naming the same. The nested field and inverse field must be exact lowercase opposites of each other. (Nested.post <=> Post.nested)

I have tested this with single nested nodes and array nested nodes. It will delete them all, as required.

That's it!

Next up... deep updates (case1 - array type, case2 - remove-set type)

Let me know if you have problems or any suggestions to the code!

J

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .