Skip to content

CRUD

CRUD operations (Create, Read, Update, Delete) are the foundation of most web applications. This beginner guide shows you how to build a complete data management system using SØAD's ActiveJDBC models and Handlebars templates.


Complete CRUD Example - Contact Management

contact.py
from utils import render
from models import Contact

class Contact(object):
    def list(self, ctx):
        """READ - Display all contacts"""
        # Get all contacts ordered by name
        contacts = Contact.findAll().orderBy("name ASC")

        # Pass to template
        ctx.output["contacts"] = contacts

        ctx.go_to = render.as_view(ctx, "contact_list")

    def view(self, ctx):
        """READ - Display single contact details"""
        request = ctx.getRequest()
        contact_id = request.getParameter("id")

        if not contact_id:
            # Redirect to list if no ID provided
            ctx.go_to = "/t/example/contact/list"
            return

        # Find contact by ID
        contact = Contact.findById(contact_id)

        if not contact:
            # Handle contact not found
            ctx.output["error"] = "Contact not found"
            ctx.go_to = render.as_view(ctx, "contact_error")
            return

        ctx.output["contact"] = contact
        ctx.go_to = render.as_view(ctx, "contact_view")

    def create_form(self, ctx):
        """Show form to CREATE new contact"""
        ctx.go_to = render.as_view(ctx, "contact_create")

    def create(self, ctx):
        """CREATE - Save new contact"""
        request = ctx.getRequest()

        # Get form data
        name = request.getParameter("name")
        email = request.getParameter("email")
        phone = request.getParameter("phone")
        company = request.getParameter("company")
        notes = request.getParameter("notes")

        # Validate required fields
        errors = []
        if not name or len(name.strip()) < 2:
            errors.append("Name is required (minimum 2 characters)")

        if not email or "@" not in email:
            errors.append("Valid email is required")

        if errors:
            # Show form again with errors
            ctx.output["errors"] = errors
            ctx.output["form_data"] = {
                "name": name,
                "email": email,
                "phone": phone,
                "company": company,
                "notes": notes
            }
            ctx.go_to = render.as_view(ctx, "contact_create")
            return

        # Create new contact
        new_contact = Contact()
        new_contact.set("name", name.strip())
        new_contact.set("email", email.strip())
        new_contact.set("phone", phone.strip() if phone else "")
        new_contact.set("company", company.strip() if company else "")
        new_contact.set("notes", notes.strip() if notes else "")
        new_contact.save()

        # Redirect to contact list with success message
        ctx.go_to = "/t/example/contact/list?success=created"

    def edit_form(self, ctx):
        """Show form to UPDATE contact"""
        request = ctx.getRequest()
        contact_id = request.getParameter("id")

        if not contact_id:
            ctx.go_to = "/t/example/contact/list"
            return

        contact = Contact.findById(contact_id)
        if not contact:
            ctx.output["error"] = "Contact not found"
            ctx.go_to = render.as_view(ctx, "contact_error")
            return

        ctx.output["contact"] = contact
        ctx.go_to = render.as_view(ctx, "contact_edit")

    def update(self, ctx):
        """UPDATE - Save changes to contact"""
        request = ctx.getRequest()
        contact_id = request.getParameter("id")

        if not contact_id:
            ctx.go_to = "/t/example/contact/list"
            return

        # Find existing contact
        contact = Contact.findById(contact_id)
        if not contact:
            ctx.output["error"] = "Contact not found"
            ctx.go_to = render.as_view(ctx, "contact_error")
            return

        # Get form data
        name = request.getParameter("name")
        email = request.getParameter("email")
        phone = request.getParameter("phone")
        company = request.getParameter("company")
        notes = request.getParameter("notes")

        # Validate
        errors = []
        if not name or len(name.strip()) < 2:
            errors.append("Name is required (minimum 2 characters)")

        if not email or "@" not in email:
            errors.append("Valid email is required")

        if errors:
            # Show form again with errors
            ctx.output["errors"] = errors
            ctx.output["contact"] = contact
            # Update contact object with new form data for display
            contact.set("name", name)
            contact.set("email", email)
            contact.set("phone", phone)
            contact.set("company", company)
            contact.set("notes", notes)
            ctx.go_to = render.as_view(ctx, "contact_edit")
            return

        # Update contact
        contact.set("name", name.strip())
        contact.set("email", email.strip())
        contact.set("phone", phone.strip() if phone else "")
        contact.set("company", company.strip() if company else "")
        contact.set("notes", notes.strip() if notes else "")
        contact.save()

        # Redirect to contact view with success message
        ctx.go_to = "/t/example/contact/view?id=%s&success=updated" % contact_id

    def delete(self, ctx):
        """DELETE - Remove contact"""
        request = ctx.getRequest()
        contact_id = request.getParameter("id")

        if not contact_id:
            ctx.go_to = "/t/example/contact/list"
            return

        # Find and delete contact
        contact = Contact.findById(contact_id)
        if contact:
            contact.delete()

        # Redirect to list with success message
        ctx.go_to = "/t/example/contact/list?success=deleted"
contact_list.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contact List</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="d-flex justify-content-between align-items-center mb-4">
            <h1>My Contacts</h1>
            <a href="/t/example/contact/create_form" class="btn btn-success">
                <i class="bi bi-plus-circle"></i> Add New Contact
            </a>
        </div>

        <!-- Success Messages -->
        {{#if success}}
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                {{#ifequal success "created"}}
                    <strong>Success!</strong> Contact has been created.
                {{else if success "updated"}}
                    <strong>Success!</strong> Contact has been updated.
                {{else if success "deleted"}}
                    <strong>Success!</strong> Contact has been deleted.
                {{/ifequal}}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        {{/if}}

        {{#if contacts}}
            <div class="row">
                {{#each contacts}}
                <div class="col-md-6 col-lg-4 mb-4">
                    <div class="card h-100">
                        <div class="card-body">
                            <h5 class="card-title">{{name}}</h5>
                            <p class="card-text">
                                <strong>Email:</strong> {{email}}<br>
                                {{#if phone}}
                                    <strong>Phone:</strong> {{phone}}<br>
                                {{/if}}
                                {{#if company}}
                                    <strong>Company:</strong> {{company}}
                                {{/if}}
                            </p>
                        </div>
                        <div class="card-footer bg-transparent">
                            <div class="btn-group w-100" role="group">
                                <a href="/t/example/contact/view?id={{id}}" class="btn btn-outline-primary btn-sm">View</a>
                                <a href="/t/example/contact/edit_form?id={{id}}" class="btn btn-outline-warning btn-sm">Edit</a>
                                <a href="/t/example/contact/delete?id={{id}}" 
                                   class="btn btn-outline-danger btn-sm"
                                   onclick="return confirm('Are you sure you want to delete {{name}}?')">Delete</a>
                            </div>
                        </div>
                    </div>
                </div>
                {{/each}}
            </div>

            <div class="mt-4">
                <small class="text-muted">Total contacts: {{contacts.length}}</small>
            </div>
        {{else}}
            <div class="alert alert-info text-center">
                <h4>No contacts yet</h4>
                <p>Get started by adding your first contact!</p>
                <a href="/t/example/contact/create_form" class="btn btn-success">Add First Contact</a>
            </div>
        {{/if}}
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
</body>
</html>
contact_create.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add New Contact</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">
                        <h3>Add New Contact</h3>
                    </div>
                    <div class="card-body">
                        <!-- Display Errors -->
                        {{#if errors}}
                            <div class="alert alert-danger">
                                <h6>Please fix the following errors:</h6>
                                <ul class="mb-0">
                                    {{#each errors}}
                                        <li>{{this}}</li>
                                    {{/each}}
                                </ul>
                            </div>
                        {{/if}}

                        <form method="POST" action="/t/example/contact/create">
                            <div class="mb-3">
                                <label for="name" class="form-label">Name <span class="text-danger">*</span></label>
                                <input type="text" class="form-control" id="name" name="name" 
                                       value="{{form_data.name}}" required>
                            </div>

                            <div class="mb-3">
                                <label for="email" class="form-label">Email <span class="text-danger">*</span></label>
                                <input type="email" class="form-control" id="email" name="email" 
                                       value="{{form_data.email}}" required>
                            </div>

                            <div class="mb-3">
                                <label for="phone" class="form-label">Phone</label>
                                <input type="tel" class="form-control" id="phone" name="phone" 
                                       value="{{form_data.phone}}">
                            </div>

                            <div class="mb-3">
                                <label for="company" class="form-label">Company</label>
                                <input type="text" class="form-control" id="company" name="company" 
                                       value="{{form_data.company}}">
                            </div>

                            <div class="mb-3">
                                <label for="notes" class="form-label">Notes</label>
                                <textarea class="form-control" id="notes" name="notes" rows="3">{{form_data.notes}}</textarea>
                            </div>

                            <div class="d-flex justify-content-between">
                                <a href="/t/example/contact/list" class="btn btn-secondary">Cancel</a>
                                <button type="submit" class="btn btn-success">Create Contact</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>
contact_view.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{contact.name}} - Contact Details</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <!-- Success Message -->
                {{#if success}}
                    <div class="alert alert-success alert-dismissible fade show" role="alert">
                        <strong>Success!</strong> Contact has been updated.
                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                    </div>
                {{/if}}

                <div class="card">
                    <div class="card-header d-flex justify-content-between align-items-center">
                        <h3>{{contact.name}}</h3>
                        <div>
                            <a href="/t/example/contact/edit_form?id={{contact.id}}" class="btn btn-warning btn-sm">Edit</a>
                            <a href="/t/example/contact/delete?id={{contact.id}}" 
                               class="btn btn-danger btn-sm"
                               onclick="return confirm('Are you sure you want to delete {{contact.name}}?')">Delete</a>
                        </div>
                    </div>
                    <div class="card-body">
                        <div class="row">
                            <div class="col-md-6">
                                <h6>Contact Information</h6>
                                <table class="table table-sm">
                                    <tr>
                                        <td><strong>Name:</strong></td>
                                        <td>{{contact.name}}</td>
                                    </tr>
                                    <tr>
                                        <td><strong>Email:</strong></td>
                                        <td><a href="mailto:{{contact.email}}">{{contact.email}}</a></td>
                                    </tr>
                                    <tr>
                                        <td><strong>Phone:</strong></td>
                                        <td>
                                            {{#if contact.phone}}
                                                <a href="tel:{{contact.phone}}">{{contact.phone}}</a>
                                            {{else}}
                                                <span class="text-muted">Not provided</span>
                                            {{/if}}
                                        </td>
                                    </tr>
                                    <tr>
                                        <td><strong>Company:</strong></td>
                                        <td>
                                            {{#if contact.company}}
                                                {{contact.company}}
                                            {{else}}
                                                <span class="text-muted">Not provided</span>
                                            {{/if}}
                                        </td>
                                    </tr>
                                </table>
                            </div>
                            <div class="col-md-6">
                                <h6>Notes</h6>
                                {{#if contact.notes}}
                                    <p>{{contact.notes}}</p>
                                {{else}}
                                    <p class="text-muted">No notes added</p>
                                {{/if}}

                                <h6>Created</h6>
                                <small class="text-muted">{{contact.created_at}}</small>
                            </div>
                        </div>
                    </div>
                    <div class="card-footer">
                        <a href="/t/example/contact/list" class="btn btn-outline-secondary">Back to List</a>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
</body>
</html>
contact_edit.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Edit {{contact.name}}</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">
                        <h3>Edit Contact</h3>
                    </div>
                    <div class="card-body">
                        <!-- Display Errors -->
                        {{#if errors}}
                            <div class="alert alert-danger">
                                <h6>Please fix the following errors:</h6>
                                <ul class="mb-0">
                                    {{#each errors}}
                                        <li>{{this}}</li>
                                    {{/each}}
                                </ul>
                            </div>
                        {{/if}}

                        <form method="POST" action="/t/example/contact/update">
                            <input type="hidden" name="id" value="{{contact.id}}">

                            <div class="mb-3">
                                <label for="name" class="form-label">Name <span class="text-danger">*</span></label>
                                <input type="text" class="form-control" id="name" name="name" 
                                       value="{{contact.name}}" required>
                            </div>

                            <div class="mb-3">
                                <label for="email" class="form-label">Email <span class="text-danger">*</span></label>
                                <input type="email" class="form-control" id="email" name="email" 
                                       value="{{contact.email}}" required>
                            </div>

                            <div class="mb-3">
                                <label for="phone" class="form-label">Phone</label>
                                <input type="tel" class="form-control" id="phone" name="phone" 
                                       value="{{contact.phone}}">
                            </div>

                            <div class="mb-3">
                                <label for="company" class="form-label">Company</label>
                                <input type="text" class="form-control" id="company" name="company" 
                                       value="{{contact.company}}">
                            </div>

                            <div class="mb-3">
                                <label for="notes" class="form-label">Notes</label>
                                <textarea class="form-control" id="notes" name="notes" rows="3">{{contact.notes}}</textarea>
                            </div>

                            <div class="d-flex justify-content-between">
                                <a href="/t/example/contact/view?id={{contact.id}}" class="btn btn-secondary">Cancel</a>
                                <button type="submit" class="btn btn-warning">Update Contact</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>
CREATE TABLE contact (
    id int NOT NULL AUTO_INCREMENT,
    name varchar(100) NOT NULL,
    email varchar(150) NOT NULL,
    phone varchar(20),
    company varchar(100),
    notes text,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_name (name),
    KEY idx_email (email)
);

INSERT INTO contact (name, email, phone, company, notes) VALUES
('John Smith', '[email protected]', '+1-555-0101', 'Tech Solutions Inc', 'Met at conference 2024'),
('Sarah Johnson', '[email protected]', '+1-555-0102', 'Marketing Pro', 'Potential client for Q2 project'),
('Mike Davis', '[email protected]', '+1-555-0103', 'StartupXYZ', 'Interested in partnership'),
('Lisa Chen', '[email protected]', '+1-555-0104', 'Creative Agency', 'UI/UX design expert'),
('Tom Wilson', '[email protected]', NULL, NULL, 'Friend from college');

CRUD Operations Explained

  • CREATE: Contact() creates new record, save() stores it
  • READ: findAll() gets all records, findById() gets one record
  • UPDATE: set() updates fields, save() stores changes
  • DELETE: delete() removes record from database

Simple CRUD - Todo List

todo.py
from utils import render
from models import Todo

class Todo(object):
    def list(self, ctx):
        """Show all todos"""
        todos = Todo.findAll().orderBy("created_at DESC")
        ctx.output["todos"] = todos
        ctx.go_to = render.as_view(ctx, "todo_list")

    def add(self, ctx):
        """Add new todo"""
        request = ctx.getRequest()
        task = request.getParameter("task")

        if task and len(task.strip()) > 0:
            # Create new todo
            new_todo = Todo()
            new_todo.set("task", task.strip())
            new_todo.set("completed", False)
            new_todo.save()

        # Redirect back to list
        ctx.go_to = "/t/example/todo/list"

    def complete(self, ctx):
        """Mark todo as completed"""
        request = ctx.getRequest()
        todo_id = request.getParameter("id")

        if todo_id:
            todo = Todo.findById(todo_id)
            if todo:
                todo.set("completed", True)
                todo.save()

        ctx.go_to = "/t/example/todo/list"

    def uncomplete(self, ctx):
        """Mark todo as not completed"""
        request = ctx.getRequest()
        todo_id = request.getParameter("id")

        if todo_id:
            todo = Todo.findById(todo_id)
            if todo:
                todo.set("completed", False)
                todo.save()

        ctx.go_to = "/t/example/todo/list"

    def delete(self, ctx):
        """Delete todo"""
        request = ctx.getRequest()
        todo_id = request.getParameter("id")

        if todo_id:
            todo = Todo.findById(todo_id)
            if todo:
                todo.delete()

        ctx.go_to = "/t/example/todo/list"
todo_list.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Todo List</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .completed { 
            text-decoration: line-through; 
            opacity: 0.6; 
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <h1 class="text-center mb-4">My Todo List</h1>

                <!-- Add New Todo -->
                <div class="card mb-4">
                    <div class="card-body">
                        <form method="POST" action="/t/example/todo/add" class="d-flex">
                            <input type="text" class="form-control me-2" name="task" 
                                   placeholder="What needs to be done?" required>
                            <button type="submit" class="btn btn-primary">Add</button>
                        </form>
                    </div>
                </div>

                <!-- Todo List -->
                {{#if todos}}
                    <div class="list-group">
                        {{#each todos}}
                            <div class="list-group-item d-flex justify-content-between align-items-center {{#if completed}}completed{{/if}}">
                                <div class="flex-grow-1">
                                    {{#if completed}}
                                        <i class="bi bi-check-circle-fill text-success me-2"></i>
                                    {{else}}
                                        <i class="bi bi-circle me-2"></i>
                                    {{/if}}
                                    {{task}}
                                </div>
                                <div class="btn-group btn-group-sm">
                                    {{#if completed}}
                                        <a href="/t/example/todo/uncomplete?id={{id}}" 
                                           class="btn btn-outline-warning" title="Mark as incomplete">
                                            <i class="bi bi-arrow-counterclockwise"></i>
                                        </a>
                                    {{else}}
                                        <a href="/t/example/todo/complete?id={{id}}" 
                                           class="btn btn-outline-success" title="Mark as complete">
                                            <i class="bi bi-check"></i>
                                        </a>
                                    {{/if}}
                                    <a href="/t/example/todo/delete?id={{id}}" 
                                       class="btn btn-outline-danger" 
                                       title="Delete"
                                       onclick="return confirm('Delete this task?')">
                                        <i class="bi bi-trash"></i>
                                    </a>
                                </div>
                            </div>
                        {{/each}}
                    </div>

                    <div class="mt-3 text-center">
                        <small class="text-muted">
                            {{todos.length}} task(s) total
                        </small>
                    </div>
                {{else}}
                    <div class="alert alert-info text-center">
                        <h5>No tasks yet!</h5>
                        <p>Add your first task above to get started.</p>
                    </div>
                {{/if}}
            </div>
        </div>
    </div>

    <link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet">
</body>
</html>
CREATE TABLE todo (
    id int NOT NULL AUTO_INCREMENT,
    task varchar(255) NOT NULL,
    completed boolean DEFAULT false,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_completed (completed)
);

INSERT INTO todo (task, completed) VALUES
('Learn SØAD framework basics', false),
('Build my first CRUD application', false),
('Read the documentation', true),
('Practice with database operations', false),
('Create a real project', false);

Simple CRUD Features

  • Quick Create: Add new items with simple form
  • Status Toggle: Mark items as complete/incomplete
  • Instant Delete: Remove items with confirmation
  • Visual Feedback: Different styling for completed items

CRUD Essentials for Beginners

Create: Model() + set() + save()
Read: findAll() or findById()
Update: findById() + set() + save()
Delete: findById() + delete()

Key Tips: - Always validate user input before saving - Handle cases where records don't exist - Provide feedback to users after operations - Use redirects after POST operations to prevent duplicate submissions