How to Code Solidity Smart Contract CRUD Functions: The Right Way

Written by daltonic | Published 2022/05/17
Tech Story Tags: blockchain | tatum_io | blockchain-writing-contest | solidity | crud | software-development | blockchain-development | blockchain-top-story

TLDRSmart contract development and the Web3 economy are still in their infancy, and there is a high demand for blockchain developers globally. With a 98 percent increase, this was the fastest growing industry in history, outpacing the entire industry by four times. The coolest part is that most blockchain jobs are remote-based, so you don’t need to worry about locations. Master CRUD skills are needed to effectively build a content-driven system, and being that the blockchain serves as a database to immutably store records.via the TL;DR App

Introduction

Smart contract development and the Web3 economy are still in their infancy, and there is a high demand for blockchain developers globally.

According to a recent LinkedIn report, job postings containing terms like Bitcoin, blockchain, and other digital asset-related roles increased 395 percent in 2021 in the United States compared to the previous year. With a 98 percent increase, this was the fastest growing industry in history, outpacing the entire industry by four times.

Blockchain development has a greater chance of landing you that six-figure dream job of yours. The coolest part is that most blockchain jobs are remote-based, so you don’t need to worry about locations.

But how do you jump in and capitalize on this space? It's by learning how to build things with technology.

If you are looking for a personal tutor to speed you up with web3 development, kindly book a session with me.

With that being said, let’s jump into this tutorial…

Why Should You Master CRUD Functions in Solidity?

Mastering how to create, read, update, and delete records from the blockchain is a skill you must understand. Yes, you can delete stuff from the blockchain (to an extent) using the technique you are about to learn.

CRUD skills are needed to effectively build a content-driven system, and being that the blockchain serves as a database to immutably store records, wisdom demands that you understand how to work with the technology.

Again, if you’re going to work on a blockchain project for either a company or a client, there may still be a need to remove some unnecessary records which is why you should ACE your Solidity CRUD skills now.

What if you’re working on a web3 blog project, and you know there needs to be a delete button for very bad comments? You will need to delete functionality in that case.

You see that a good knowledge of CRUD will become inevitable as you dive deep into blockchain development.

By the way, deleting a record from a blockchain network is kind of different from the way you delete it from a regular database. The record will still exist on the network but will be removed from your web3 app.

The full example below will show you step by step how to accomplish this.

CRUD Functions Example

This is a smart contract that I created and tested; to see how it works, you can open it in a remix editor and run it yourself.

But first, finish the tutorial; there is important information below that explains each function specifically.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract Blog {
    address public owner;
    uint256 public activePostCounter = 0;
    uint256 public inactivePostCounter = 0;
    uint256 private postCounter = 0;

    mapping(uint256 => address) public delPostOf;
    mapping(uint256 => address) public authorOf;
    mapping(address => uint256) public postsOf;

    enum Deactivated { NO, YES }

    struct PostStruct {
        uint256 postId;
        string title;
        string description;
        address author;
        Deactivated deleted;
        uint256 created;
        uint256 updated;
    }

    PostStruct[] activePosts;
    PostStruct[] inactivePosts;

    event Action (
        uint256 postId,
        string actionType,
        Deactivated deleted,
        address indexed executor,
        uint256 created
    );

    modifier ownerOnly(){
        require(msg.sender == owner, "Owner reserved only");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function createPost(
        string memory title,
        string memory description
    ) external returns (bool) {
        require(bytes(title).length > 0, "Title cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");

        postCounter++;
        authorOf[postCounter] = msg.sender;
        postsOf[msg.sender]++;
        activePostCounter++;

        activePosts.push(
            PostStruct(
                postCounter,
                title,
                description,
                msg.sender,
                Deactivated.NO,
                block.timestamp,
                block.timestamp
            )
        );

        emit Action (
            postCounter,
            "POST CREATED",
            Deactivated.NO,
            msg.sender,
            block.timestamp
        );

        return true;
    }

    function updatePost(
        uint256 postId,
        string memory title,
        string memory description
    ) external returns (bool) {
        require(authorOf[postId] == msg.sender, "Unauthorized entity");
        require(bytes(title).length > 0, "Title cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");

        for(uint i = 0; i < activePosts.length; i++) {
            if(activePosts[i].postId == postId) {
                activePosts[i].title = title;
                activePosts[i].description = description;
                activePosts[i].updated = block.timestamp;
            }
        }

        emit Action (
            postId,
            "POST UPDATED",
            Deactivated.NO,
            msg.sender,
            block.timestamp
        );

        return true;
    }

    function showPost(
        uint256 postId
    ) external view returns (PostStruct memory) {
        PostStruct memory post;
        for(uint i = 0; i < activePosts.length; i++) {
            if(activePosts[i].postId == postId) {
                post = activePosts[i];
            }
        }
        return post;
    }

    function getPosts() external view returns (PostStruct[] memory) {
        return activePosts;
    }

    function getDeletedPost() ownerOnly external view returns (PostStruct[] memory) {
        return inactivePosts;
    }

    function deletePost(uint256 postId) external returns (bool) {
        require(authorOf[postId] == msg.sender, "Unauthorized entity");

        for(uint i = 0; i < activePosts.length; i++) {
            if(activePosts[i].postId == postId) {
                activePosts[i].deleted = Deactivated.YES;
                activePosts[i].updated = block.timestamp;
                inactivePosts.push(activePosts[i]);
                delPostOf[postId] = authorOf[postId];
                delete activePosts[i];
                delete authorOf[postId];
            }
        }

        postsOf[msg.sender]--;
        inactivePostCounter++;
        activePostCounter--;

        emit Action (
            postId,
            "POST DELETED",
            Deactivated.YES,
            msg.sender,
            block.timestamp
        );

        return true;
    }
    
    function restorDeletedPost(
        uint256 postId, 
        address author
    ) ownerOnly external returns (bool) {
        require(delPostOf[postId] == author, "Unmatched Author");

        for(uint i = 0; i < inactivePosts.length; i++) {
            if(inactivePosts[i].postId == postId) {
                inactivePosts[i].deleted = Deactivated.NO;
                inactivePosts[i].updated = block.timestamp;

                activePosts.push(inactivePosts[i]);
                delete inactivePosts[i];
                authorOf[postId] = delPostOf[postId];
                delete delPostOf[postId];
            }
        }

        postsOf[author]++;
        inactivePostCounter--;
        activePostCounter++;

        emit Action (
            postId,
            "POST RESTORED",
            Deactivated.NO,
            msg.sender,
            block.timestamp
        );

        return true;
    }
}

Alright, let's dissect this smart contract step by step; if you're a beginner, don't worry; I wrote this smart contract example to demonstrate simplicity.

Step 1: The smart contract structure

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract Blog {
  // code goes here...
}

This is a typical smart contract structure; consider it a class in object-oriented programming because it is one in reality.

The SPDX is used to specify the smart contract's license type. Pragma refers to the version of the solidity compiler that will be used for the smart contract. Finally, the smart contract's name is Blog.

It is critical to note that the file name should always correspond to the class name; this will ensure consistency and prevent unnecessary bugs in your code.

Step 2: Defining Contract Variables

address public owner;
uint256 public activePostCounter = 0;
uint256 public inactivePostCounter = 0;
uint256 private postCounter = 0;

Here we have two types of variables: address and uint256. An address represents a user's account, whereas uint256 represents an unsigned integer ranging from 0 to 2256–1.

The owner will hold the deployer’s account and activePostCounter holds a number of available posts. inactivePostCounter keeps track of the deleted post, just in case, it needs to be recovered. postCounter holds the total number of posts in the smart contract, but it is private.

Step 3: Defining the Mappings

mapping(uint256 => address) public delPostOf;
mapping(uint256 => address) public authorOf;
mapping(address => uint256) public postsOf;

authorOf() is a mapped variable that accepts a postId and returns the address of the post author. Whereas postsOf() keeping track of an author’s posts, and delPostOf() serves as a recycling bin for each author.

Step 4: Defining an Enumerable

enum Deactivated { NO, YES }

This is used to indicate whether or not a post has been deleted. Enum converts an alphabetical value to unsigned integers beginning with zero. For example, the values NO and YES would become 0 and 1, respectively.

Step 5: Defining the Struct

struct PostStruct {
    uint256 postId;
    string title;
    string description;
    address author;
    Deactivated deleted; // We are using the enum here
    uint256 created;
    uint256 updated;
}

PostStruct[] activePosts;
PostStruct[] inactivePosts;

Structs are used to define complex data structures, which are made up of two or more data types.

We specified in the above PostStruct that a post should have an Id, title, description, author, a deleted key, time of creation, and time of update.

Next, we made two arrays: one for active posts and one for inactive posts or deleted posts. This method prevents data loss, but you can also use a single array and only toggle the deleted key on and off.

However, this raises another issue: you'll have to read them all in your frontend app before showing the active posts. I'd like to spare you that trip.

Step 6: Defining an Event

event Action (
    uint256 postId,
    string actionType,
    Deactivated deleted,
    address indexed executor,
    uint256 created
);

We defined a dynamic event to emit some vital information on created, updated, deleted and restored functions respectively.

Step 7: Defining modifier

modifier ownerOnly(){
    require(msg.sender == owner, "Owner reserved only");
    _;
}

A modifier is a function that changes the behavior of another function at compile time. We can say, it is a prerequisite for executing a function.

In the above modifier, we specified that the caller of the function to which this modifier will be attached must be the owner or deployer of the smart contract.

Step 8: Defining the Constructor

constructor() {
    owner = msg.sender;
}

Based on your OOP knowledge, a constructor is always the first function to run when a class is instantiated. OOP construction is mutual to smart contracts.

The first function to run when this smart contract is deployed is the constructor, and we specified that it should make the owner the one who deployed the smart contract.

Step 9: The createPost() function

function createPost(
    string memory title,
    string memory description
) external returns (bool) {
    // Checks for empty string
    require(bytes(title).length > 0, "Title cannot be empty");
    require(bytes(description).length > 0, "Description cannot be empty");

    // Performs computations
    postCounter++;
    authorOf[postCounter] = msg.sender;
    postsOf[msg.sender]++;
    activePostCounter++;

    // Adds post to array
    activePosts.push(
        PostStruct(
            postCounter,
            title,
            description,
            msg.sender,
            Deactivated.NO,
            block.timestamp,
            block.timestamp
        )
    );
  
    // Emits a created event
    emit Action (
        postCounter,
        "POST CREATED",
        Deactivated.NO,
        msg.sender,
        block.timestamp
    );
    
    // Returns success
    return true;
}

This function accomplishes the post-creation in five steps.

Firstly, it checks that you are sending real information to the smart contract and not just an empty string.

Secondly, it performs some computations like incrementing post counts for the blog and for the author, including making the author the post owner.

Third, it adds the post into the activePosts using the PostStruct defined earlier.

Fourthly, it emits a “POST CREATED” event which is logged off on the EVM, this is essential for enriching the information captured in a transaction.

Lastly, we returned a true boolean key indicating that the entire function ran successfully.

Step 10: The updatePost() function.

function updatePost(
    uint256 postId,
    string memory title,
    string memory description
) external returns (bool) {
    // Checks for empty string
    require(authorOf[postId] == msg.sender, "Unauthorized entity");
    require(bytes(title).length > 0, "Title cannot be empty");
    require(bytes(description).length > 0, "Description cannot be empty");

    // Changes post record
    for(uint i = 0; i < activePosts.length; i++) {
        if(activePosts[i].postId == postId) {
            activePosts[i].title = title;
            activePosts[i].description = description;
            activePosts[i].updated = block.timestamp;
        }
    }
    
    // Emits a updated event
    emit Action (
        postId,
        "POST UPDATED",
        Deactivated.NO,
        msg.sender,
        block.timestamp
    );
  
    // Returns success
    return true;
}

This function operates like a createPost() function. The difference is that when checking for an empty string, the updatePost() function ensures that the person updating the function is the post owner (author).

Next, it targets the particular post by the Id and updates the record.

Step 11: The showPost() function

function showPost(
    uint256 postId
) external view returns (PostStruct memory) {
    PostStruct memory post;
    for(uint i = 0; i < activePosts.length; i++) {
        if(activePosts[i].postId == postId) {
            post = activePosts[i];
        }
    }
    return post;
}

This simply returns to our post by recursively searching for a match in the activePost array.

Step 12: The getPosts() function.

function getPosts() external view returns (PostStruct[] memory) {
    return activePosts;
}

Returns all available active blog posts.

Step 13: The getDeletedPost() function

function getDeletedPost() 
  ownerOnly external view returns (PostStruct[] memory) {
    return inactivePosts;
}

This simply returns all deleted posts and can only be accessed by the owner of the blog.

Step 14: The deletePost() function

function deletePost(uint256 postId) external returns (bool) {
    // check if post belong to user
    require(authorOf[postId] == msg.sender, "Unauthorized entity");
    
    // find and delete post
    for(uint i = 0; i < activePosts.length; i++) {
        if(activePosts[i].postId == postId) {
            activePosts[i].deleted = Deactivated.YES;
            activePosts[i].updated = block.timestamp;
            inactivePosts.push(activePosts[i]);
            delPostOf[postId] = authorOf[postId];
            delete activePosts[i];
            delete authorOf[postId];
        }
    }

    // Recallibrate counters
    postsOf[msg.sender]--;
    inactivePostCounter++;
    activePostCounter--;

    // Emits event
    emit Action (
        postId,
        "POST DELETED",
        Deactivated.YES,
        msg.sender,
        block.timestamp
    );
  
    // Returns success
    return true;
}

The above function was achieved in four steps.

First, we validated that the owner of the post is the one deleting, then we searched for the post, deleted it, and emitted a “POST DELETED” event which also returns a boolean value of true.

Please take note that delete behavior is not done like in a normal database, rather we specified two arrays; activePosts and inactivePosts. Whenever we deleted a post, it will be moved to the inactivePosts array, and only the blog owner can restore it at the request of the post author.

Step 15: The Post Restore Function

function restorDeletedPost(
    uint256 postId, 
    address author
) ownerOnly external returns (bool) {
    // checks if post exists in users recycle bin
    require(delPostOf[postId] == author, "Unmatched Author");

    // Finds and restore the post to the author
    for(uint i = 0; i < inactivePosts.length; i++) {
        if(inactivePosts[i].postId == postId) {
            inactivePosts[i].deleted = Deactivated.NO;
            inactivePosts[i].updated = block.timestamp;
            activePosts.push(inactivePosts[i]);
            delete inactivePosts[i];
            authorOf[postId] = delPostOf[postId];
            delete delPostOf[postId];
        }
    }

    // Recallibrates counter to reflect restored post
    postsOf[author]++;
    inactivePostCounter--;
    activePostCounter++;

    // Emits a restoration event
    emit Action (
        postId,
        "POST RESTORED",
        Deactivated.NO,
        msg.sender,
        block.timestamp
    );

    // resturns success
    return true;
}

Whenever a post is restored, it is moved from the inactivePosts array to the activePosts array.

It is worth mentioning that you can design your delete pattern a little bit differently than mine. You can decide to leverage the enum Deactivated structure we created above and only delete it by toggling the deleted state to YES or NO.

Men and brethren, that is how you crush the solidity CRUD functions…

Conclusion

It’s been a blast journeying with you in this tutorial, I hope you got some valuable insights on how to write solidity smart contract CRUD functions.

I want to inform you of my private tutoring classes for those who want to jump into the web3 space. If you’re interested, please book me up on my website.

That’s all for this tutorial, please like and share…

Excited to see you in the next tutorial, till then, enjoy!

About the Author

Gospel Darlington kick-started his journey as a software engineer in 2016. Over the years, he has grown full-blown skills in JavaScript stacks such as React, ReactNative, NextJs, and now blockchain.

He is currently freelancing, building apps for clients, and writing technical tutorials teaching others how to do what he does.

Gospel Darlington is open and available to hear from you. You can reach him on LinkedIn, Facebook, Github, or on his website.


Written by daltonic | Youtuber | Blockchain Developer | Writer | Instructor
Published by HackerNoon on 2022/05/17