john chesley

fresh stuff

destinations

favorites

reflection and vb

reflection is an rather interesting programming concept. i was thinking about it on my way home from work, because i've been exploring the .Net framework's Reflection library lately to make visual basic do very useful things for me. it's kindof like my program has become semi-self-aware, in the sense that it looks at itself and takes the parts of itself that it needs, and uses them in various ways.

some background

django and various other web frameworks have a pretty neat feature where you can define a class that inherits from a parent "data model" class. within your class, you define the fields you want in your database, not as normal ints or strings or what-have-you, but as subclasses of a class provided by the framework, which represents the datatype you want (int, varchar, etc). doing this adds an abstracting layer that lets the framework take care of all your database work behind the scenes. this abstraction makes it easy not only to automate the database heavy lifting, but also makes it easier to use the same code with different database servers.

as far as i know, nobody has gone to the trouble to create a nice web framework like that for visual basic (django and appengine use python), and having seen and used this feature in a number of places, i missed it when i went to use VB.

so..i wrote my own, very simple "framework" to do most of the database work i needed done. i had already done it the hard way, in fact, and knew all along that i was doing it wrong, but i had a vague idea that i would be rewriting this code to make it more general, and luckily had made it easy on myself.

getting there...

i started with a parent class named DbField, from which various other datatypes inherit, defining datatypes that i would want to store in a database:

Public MustInherit Class DbField
  ...
End Class

Public Class CharField
    Inherits DbField
  ...
End Class

Public Class IntField
    Inherits DbField
  ...
End Class

...

now in the classes i use to represent my database data, instead of defining a string, i define a CharField, give it a name and a maximum length, and that CharField object will know how to make itself ready to be used in a SQL statement. it also knows the name of the column it should be inserted into when the time comes. other datatypes know how to do whatever they need to do to get safely into the database too.

Public Class SomeItem
    Inherits DbModel

    Protected _name As New CharField("name", 50)
    ...
End Class

so now i have a variable whose value should be stored in a column named "name", with a varchar(50) type.

notice that in the fake-code above, SomeItem inherits from the DbModel class. the DbModel class is where the real magic starts to happen. since SomeItem inherits from DbModel, subroutines and methods defined within DbModel can be called on SomeItem...for example a save() method. save(), if defined in the DbModel class, will see all the variables and members defined within the SomeItem class if it gets called on a SomeItem object, thus it can look for members that inherit from DbField, and find all the data that should be stored in the database.

System.Reflection

System.Reflection is an interesting place to explore. normally, accessing members of a sub-class, from a method defined on the parent class, would be impossible, because how does the parent class find out about the sub-class' private members? it normally can't, but using System.Reflection, it can see anything it wants.

Me.GetType

in order to point the reflection library in the right direction, or in any direction at all, you need a Type object. to inspect a class' own type, you'd get that type using Me.GetType. when called from within a method defined on a parent class, and called from an inherited class, Me.GetType returns the inherited type. this is key, because it lets the parent class inspect the details of its own sub-classes. using this fact and the system.reflection library, we can find all the DbField variables of any class that inherits DbModel, and use those to construct SQL queries or what-have-you:

Public Class DbModel
    ...
    Protected _sql_columns As ArrayList
    ...
    Protected Sub getFieldInstances()
        Dim f As Reflection.FieldInfo
        Dim t As Type = Me.GetType()
        Dim t2 As Type = GetType(DbField)

        Dim allfields() As Object = t.GetFields(Reflection.BindingFlags.NonPublic Or _
                                        Reflection.BindingFlags.Instance)
        _sql_columns = New ArrayList()

        For Each f In allfields
            If f.FieldType.IsSubclassOf(t2) Then
                _sql_columns.Add(f.GetValue(Me))
            End If
        Next
    End 
    ...
End Class

getFieldInstances looks at the type of the object at hand, examines all the NonPublic member variables, and checks to see if they are a subclass of DbField. if they are, it pulls the value of that field out of the current object, and stores it in an array.

so, now we have a list of all the field names and types, and even the values that need to go into those respective columns. we can define as many subclasses of DbModel as we want, each one with DbField variables of various types and names, and we can always get the correct list of fields for each one. this could be used then to generate SQL to create the actual table if we needed to.

MetaData

there's one more question left to answer in order to auto-generate insert and update statements: what table do we insert into or update? we could just use the name of the class, or some such thing. that's what the other frameworks do. i already had my tables defined though, and the names weren't necessarily the same as the class name (maybe they should have been, but they aren't). so, i defined one more class: MetaData, which holds information like the primary key field, and the name of the table.

Partial Public Class MetaData
    Protected _tablename As String
    Protected _id_col As String
    Protected _parent_id_col As String
    Protected _sort_field As String
    Protected _sort_dir As String
    Protected _type As Type

    ... 
End Class

there are some ReadOnly properties set up for reading the values of these, so they're basically constants once defined. within each DbModel class, i create a subclass of the MetaData class, which defines them in the constructor:

Public Class AnotherObject
    Inherits DbModel

    Private Class Meta
        Inherits MetaData
        Public Sub New()
            Me._tablename = "somestuff"
            Me._id_col = "stuffid"
            Me._parent_id_col = "parentstuffid"
            Me._sort_field = "date"
            Me._type = GetType(AnotherObject)
        End Sub
    End Class

    Public Sub New()
        MyBase.new(New Meta)
        ...
    End Sub
    ...
End Class

to make it accessible world-wide in the DbModel class, and yet personalized to each DbModel sub-class, an instance of the private class gets passed up to the parent class's constructor. now we can easily figure out what table each item needs to be saved to, and writing a subroutine that does the actual saving is almost too easy.

all we have to do now is construct an appropriate insert or update statement using the names and values we have, and run it:

Public Class DbModel
     ...
    Public ReadOnly Property fields() As ArrayList
        Get
            If _sql_columns Is Nothing Then
                getFieldInstances()
            End If
            Return _sql_columns
        End Get
    End Property

    Private Sub getNamesValues(ByRef names, ByRef values)
        names = ""
        values = ""

        For Each f As DbField In fields
            names += f.name + ","
            values += f.getSqlValue + ","
        Next

        names = Regex.Replace(names, ",$", "")
        values = Regex.Replace(values, ",$", "")
    End Sub

    Public Sub save()
        Dim sqlconstring As String = Util.db.getSQLServerConnectionString()
        Dim names, values, sql As String

         If Me.pk Then
             sql = "UPDATE {0} SET "
             For Each f As DbField In fields
                 If f.update Then
                     sql += f.name & " = " & f.getSqlValue & ","
                End If
            Next
            sql = Regex.Replace(sql, ",$", "")
            sql += " WHERE {1} = {2}"

            sql = String.Format(sql, _meta.tablename, _
                                _meta.id_col, pk)
        Else
            getNamesValues(names, values)
            sql = "INSERT INTO {0} ({1}) VALUES ({2})"
            sql = String.Format(sql, _meta.tablename, names, values)
        End If

        Me.pk = Util.db.executeNonQuery(sql, sqlconstring, _meta.id_col, _meta.tablename)

    End Sub
     ...
 End Class

beautiful.