Friday, May 20, 2016

Working with Swift subclasses

I decided to build the web service for my iOS and Android app using Perfect which is a Swift server implementation. I am using MySQL to host a database and wanted to a nice way to get the database tables written out as JSON. I had some problems but here is what I came up with:

My first attempt was to create a base class that I could subclass for each table type and do the read/write database operations and the JSON operations in the base class. But I ran into a lot of problems getting the base class to access the overridden properties and methods of the subclasses. Then I discovered this. Fist I create a simple protocol that all the record classes will use:

protocol DatabaseRecord : RequestHandler {
    var sqlTableName: String {get}
    var loaded: Bool {get set}
    func loadRow(row: [String])
    func getDictionary() -> [String : String]
    func load(forName:String)
    func load(forID:Int)
    func getAll() -> [AnyObject]
    init()
}

Then I create an extension to the protocol so all of this will get added to all of the classes that implement the DatabaseRecord protocol.

extension DatabaseRecord {

The methods here to load from the database don't seem like a big deal, but this just didn't work as a super class because despite all my google searching and efforts I couldn't get the base class to call the subclass methods or get the sqlTableName from the sub classes. But as an extension this jus works without any strange code.

    func loadFromDatabase(query:String) {
        var mysql = MySQLConnection.mysql;
        MySQLConnection.connectToMySQL()
        defer {
            mysql.close()
        }
        if mysql.query(query) {
            if let results = mysql.storeResults() {
                if results.numRows() >= 1 {
                    if let row = results.next() {
                        loadRow(row)
                        return
                    }
                }
            }
        }
    }
    func load(forName:String) {
        self.loadFromDatabase("SELECT * FROM \(sqlTableName) WHERE name = \"\(forName)\"")
    }
    func load(forID:Int) {
        self.loadFromDatabase("SELECT * FROM \(sqlTableName) WHERE id = \"\(forID)\"")
    }

This next method was where I had the most problems. I tried using Self, dynamicType, and everything else I could think of to get the sqlTableName from the subclasses and got it going but the creating objects of my subclasses and adding them to an array that I would return seemed impossible. Now as an extension the sqlTableName just works, and I can create a new object of the correct class using self.dynamicType.init() and add it to the array. 

    func getAll() -> [AnyObject] {
        var records = [AnyObject]()
        var mysql = MySQLConnection.mysql;
        
        MySQLConnection.connectToMySQL()
        defer {
            mysql.close()
        }
        if mysql.query("SELECT * FROM \(sqlTableName)") {
            if let results = mysql.storeResults() {
                results.forEachRow { row in
                    let obj = self.dynamicType.init()
                    obj.loadRow(row)
                    if let o = obj as? AnyObject {
                        records.append(o)
                  }
                }
            }
        }
        return records
    }

PerfectLib has a JSON encoder but when I tried to use it, it would crash. It was late and the JSON that I need to generate was simple so I just quickly wrote this to create the JSON for my table:

    func encodeAsJSON(dicts:Array<[String:String]>) -> String {
        var str: String = "["
        for dict in dicts {
            var first = true
            for (key, value) in dict {
                if !first {
                    str.appendContentsOf(",")
                } else {
                    first = false
                }
                str.appendContentsOf("{\"\(key)\":\"\(value)\"}")
            }
        }
        str.appendContentsOf("]")
        return str
    }

Now I can add a request handler to the extension and now all my database record classes have a easy way to take a http get request and turn it into JSON array.

    func handleRequest(request: WebRequest, response: WebResponse) {
        let records = getAll()
        var dicts = Array<[String:String]>()
    
        for rec in records {
            if let r = rec as? DatabaseRecord {
                let dict = r.getDictionary()
                dicts.append(dict)
            }
        }
        response.appendBodyString(encodeAsJSON(dicts))
        response.requestCompletedCallback()
    }
}

So then I have a simple Swift class representing a SQL database record. For example:

/*
auto_make SQL table
 +-------------+------------------+------+-----+---------+----------------+
 | Field       | Type             | Null | Key | Default | Extra          |
 +-------------+------------------+------+-----+---------+----------------+
 | id          | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
 | name        | varchar(255)     | YES  |     | NULL    |                |
 +-------------+------------------+------+-----+---------+----------------+
 */
class AutoMakeRecord: DatabaseRecord {
    var id: Int?
    var name: String = ""
    var loaded: Bool
    var sqlTableName: String {
        return "auto_make"
    }
    
    func loadRow(row: [String])
    {
        id = Int(row[0]) ?? -1
        name = row[1]
        loaded = true
    }
  
    func getDictionary() -> [String : String] {
        var dict = [String: String]()
        dict["id"] = String(id!)
        dict["name"] = name
        return dict
    }
    
    required init() {
        loaded = false
    }
}

Now a public method that registers the routes.  

public func PerfectServerModuleInit() {
    Routing.Handler.registerGlobally() 
   
    // Create Routes
    Routing.Routes["/auto_make"] = { _ in return AutoMakeRecord() }
}

And it is easy to create additional classes for database tables and add them to the routes.  Of course I will want to create http handlers that will allow to more than just dump out tables to JSON, but this now gives me the base I need to create the rest of my web service.

1 comment:


My Bicycle Store