Topic 10: More Modular Applications: DAOs and Controllers

IMPORTANT: if you are a student from last year looking for the file I/O work, this is currently available here.


Today we will look at:

First - Additional points regarding mapping

These points might be useful for the assignment, but we have not had time to introduce them yet, so we will do it this week.

How do I add a form to a popup?

This may help you. bindPopup() can take a DOM node as well as plain text or HTML. So you can create a DOM structure containing your form and pass it in as an argument to bindPopup(), e.g.:

const domDiv = document.createElement('div');
// ... other code to create a form ...
marker.bindPopup(domDiv); // add the DOM-created div to a marker

} Then, at the point you want to show it, change the display property from JavaScript to block, e.g.:
document.getElementById('myDiv').style.display = 'block';
Then, when you want to hide it again, change display back to none:
document.getElementById('myDiv').style.display = 'none';


The main material

Now onto the main topic for this week. Today we will look at using a more modular, easier-to-maintain structure for our Node/Express applications by making use of Data Access Objects (DAOs) and controllers.

Note that this is a more advanced topic, aimed at those of you who are coping reasonably well with the module. You should complete previous exercises before attempting this week's - particularly login systems from last week.

Data Access Objects

Example of a DAO for the students table


class StudentDao {
    // db is our sqlite database connection
    // table is the table storing the students
    constructor(db, table) {
        this.db = db;
        this.table = table;
    }


    // find a student with a given ID

    findStudentById(id) {
        
        const stmt = this.db.prepare(`SELECT * FROM ${this.table} WHERE id=?`);
        const rows = stmt.all(id);
        if (rows.length == 0) {
            // return null if no results
            return null;
        } else {
            // only one student will be found but "results" will still be an array with one member. 
            // To simplify code which makes use of the DAO, extract the one and only row from the array 
            // and return that.
            return rows[0];
        }
    }

    // find all students on a given course
    findStudentsByCourse(course) {
        const stmt = this.db.prepare(`SELECT * FROM ${this.table} WHERE course=?`);
        const rows = stmt.all(course);
        return rows; // will be an empty array if there are no results
    }

    // add a new student 
    addStudent(name, course) {
        const stmt = this.db.prepare(`INSERT INTO ${this.table} (name,course) VALUES (?,?)`);
        const info = stmt.run(name, course);
        return info.lastInsertRowid;
    }

    // update a student - takes student ID, name, and course as parameters and 
    // updates the name and the course of the record with that ID
    // For you to complete!

    updateStudent(id, name, course) {
    }


    // delete a student - takes a student ID as a parameter
    // and deletes the record with that ID.
    // For you to complete!

    deleteStudent(id) {
    }
}

export default StudentDao; // so that other code can use it

DAO - Explanation

Controllers

We can further enhance the modularity of our code through the use of controllers. Controllers are part of the model/view/controller (MVC) architecture, in which we divide the code into three components:


MVC architecture
The typical role of a controller would be to communicate with the model (the DAO) to get database results, and then format those results to be presented to the client. Or, alternatively, the controller could gather information from the client and then forward it on to the model (DAO) to insert it into the database.

Hopefully, from this you may be able to figure out that the router takes the role of the controller. To improve the modularity of your code, however, you can create separate controller objects to represent the controller, and add methods to these objects to carry out the functionality of your router. Then, in your router, you create a controller object and call these methods from each route in the router.

A good way to organise your project is to have sub-folders within the main folder of your project for routes, controllers and DAOs. These folders could simply be named:

So, if we imagine a student router, within the routes folder:

import express from 'express';
const studentRouter = express.Router();

// Initialise database, not shown

// assume the StudentController is inside a 'student.mjs' file within the 
// 'controllers' folder of the project, as described above
import StudentController from '../controllers/student.mjs';


// Create the controller object, and pass in the database connection as an argument
const sController = new StudentController(db);

// handle get requests to route /id/:id using the controller's findStudentById() method
studentRouter.get('/id/:id', sController.findStudentById.bind(sController));

// handle get requests to route /course/:course using the controller's findStudentByCourse() method
studentRouter.get('/course/:course', sController.findStudentByCourse.bind(sController));

// handle post requests to /create using the controller's addStudent() method
studentRouter.post('/create', sController.addStudent.bind(sController));

export default studentRouter; // so that main application can use it
Note how the router creates a StudentController object, passing the database connection to it (which will be needed by the DAO) and note also how we handle each route with methods of the controller object. Remember we looked at "bind()" in week 4. Here we are using it slightly differently. bind() is necessary here to preserve the context of the this object in callbacks. By default, the context of this (the current object) is lost in a callback function, and to preserve the context of this, you have to bind the callback to the object you wish to use as this (which will be the controller here). You can read about the use of bind() to preserve the context of this in callbacks in more detail here.

So, let's look at the Controller object, which would be in the controllers folder:

// assume the DAO is in a 'student.mjs' file within the 
//'dao' folder of the project, as described above
import StudentDao from '../dao/student.mjs';

class StudentController {
    constructor(db) {
        // Create a DAO for communicating with the database
        this.dao = new StudentDao(db, "students");
    }

    // findStudentById()
    // calls the DAO's findStudentById() method, passing the ID parameter to
    // it, and formats the JSON returned.
    findStudentById(req, res) {
        try {
            const student = this.dao.findStudentById(req.params.id);
            // Remember from the DAO that the method returns null if there are no results
            if(student == null) {
                res.status(404).json({error: "No student with that ID"});
            } else {
                res.json(student);    
            }
        } catch(e) {
            res.status(500).json({error: e});
        }
    }

    // findStudentByCourse()
    // calls the DAO's findStudentByCourse() method, passing the course parameter to
    // it, and formats the JSON returned.
    findStudentByCourse(req, res) {
        try {
            const students = this.dao.findStudentByCourse(req.params.course);
            res.json(students);    
        } catch(e) {
            res.status(500).json({error: e});
        }
    }

    // addStudent()
    // calls the DAO's addStudent() method, passing in the 'name' and 'course'
    // POST data to it, and formats the allocated ID as JSON to send back to the client.
    addStudent(req, res) {
        try {
            const studentId = this.dao.addStudent(req.body.name, req.body.course);
            res.json({id: studentId});
        } catch(e) {
            res.status(500).json({error: e});
        }
    }
}

export default StudentController; // so that the router can use it
Note how the methods of the controller object can act as route handlers, as they have the req and res parameters. Note also how they call the methods of the DAO and then format the data returned from the DAO as JSON, ready to be used by the client. Hopefully, by examining the code, you can see how the controller is acting as a "middle-person" between the model (the DAO) and the view (the client).

Exercise

Rewrite your Express server to use appropriate DAOs and controllers as well as routes: