I have a system that parses a logfile which contains changesets of mysql tables, think of something like a binlog. There can be updates and inserts, deletes we ignore for now. The function of my module gets an input like this:
type Changeset struct {
Table string // which table was affected
Type string // INSERT or UPDATE
OldData map[string]string // these 2 fields contain all columns of a table row
NewData map[string]string
}
OldData
is empty when it's an INSERT
changeset, when it's an UPDATE
changeset, OldData
and NewData
are filled (the data before and after the update).
Now I don't want to work with untyped data like this in my module, as I need to model some domain and it would be nicer to have some type safety. However, I need to still retain the knowledge if a change was an insert or an update for that domain logic (like, if it's an update, I will validate that some fields didn't change, as an example).
Assume I have two tables (let's say they only have one field named Id, but in reality they have more and different ones). So I modeled these objects like so:
type Foo struct { // foo table
Id string
// ... imagine more fields here ...
}
type Bar struct { // bar table
Id string
// ... imagine more fields here ...
}
Now I can map the map[string][string]
from Changeset.OldData
and Changeset.NewData
, but then I don't know anymore if the change was an insert or an update. I was thinking a bit back and forth, but the best I came up with was:
type FooInsert struct {
New Foo
}
type FooUpdate struct {
New Foo
Old Foo
}
type BarInsert struct {
New Bar
}
type BarUpdate struct {
New Bar
Old Bar
}
And the mapping code looks like this:
func doMap(c Changeset) interface{} {
if c.Table == "foo" {
switch c.Type {
case "UPDATE":
return FooUpdate{Old: Foo{Id: c.OldData["id"]}, New: Foo{Id: c.NewData["id"]}}
case "INSERT":
return FooInsert{New: Foo{Id: c.NewData["id"]}}
}
}
if c.Table == "bar" {
switch c.Type {
// ... almost same as above, but return BarUpdate/BarInsert ...
}
}
return nil
}
The upside is, it enables me to write do do a typeswitch on the result of this mapping function like so:
insertChangeset := Changeset{
Table: "foo",
Type: "INSERT",
NewData: map[string]string{"id": "1"},
}
o := doMap(insertChangeset)
switch o.(type) {
case BarUpdate:
println("Got an update of table bar")
case FooUpdate:
println("Got an update of table foo")
case BarInsert:
println("Got an insert to table bar")
case FooInsert:
println("Got an insert to table foo")
}
The typeswitch is what I would need to have in the end (different types per change changeset type and per entity.) But:
- the mapping code as seen in
doMap
is very ugly and repetitive. - for every new entity
X
I introduce, I need to create two more typesXInsert
andXUpdate
.
Is there any way around this mess? In other programming languages I might have thought of something like:
type Update<T> {
T Old
T New
}
type Insert<T> {
T New
}
But not sure how to model this in Go. I created also a playground sample that shows the whole code in one program: https://play.golang.org/p/ZMnB5K7RaI