2

I am trying to figure out how to build a datastore abstraction in Go. I think I understand the basics of interfaces. However, the problem I have is all of the examples online only show you the most simple case, nothing beyond it.

What I am trying to do is figure out how and where to put the SQL code. I have tried to write the simplest bit of code that can illustrate what I am trying to do (yes, there is no error code, yes, the path structure is not idiomatic). I have a database with two tables. One to store Circles and one to store Squares. I have objects in Go to make these. My directory structure is:

project/main.go
project/test.db
project/shapes/shape.go
project/shapes/circle/circle.go
project/shapes/square/square.go
project/datastore/datastore.go
project/datastore/sqlite3/sqlite3.go

The only way I can think of how to make this work is to put the SQL INSERT and SELECT code inside of the actual shape files (circle.go and square.go). But that feels really wrong. It seems like it should be part of the datastore/sqlite3 package some how. I just do not see how to make that work. Here is what I have so far:

main.go

package main

import (
    "fmt"
    "project/datastore/sqlite3"
    "project/shapes/circle"
    "project/shapes/square"
)

func main() {
    c := circle.New(4)
    area := c.Area()
    fmt.Println("Area: ", area)

    s := square.New(12)
    area2 := s.Area()
    fmt.Println("Area: ", area2)

    db := sqlite3.New("test.db")
    db.Put(c)
    db.Close()
}

shapes/shape.go

package shapes

type Shaper interface {
    Area() float64
}

shapes/circle/circle.go

package circle

import (
    "project/shapes"
)

type CircleType struct {
    Radius float64
}

func New(radius float64) shapes.Shaper {
    var c CircleType
    c.Radius = radius
    return &c
}

func (c *CircleType) Area() float64 {
    area := 3.1415 * c.Radius * c.Radius
    return area
}

shapes/square/square.go

package square

import (
    "project/shapes"
)

type SquareType struct {
    Side float64
}

func New(side float64) shapes.Shaper {
    var s SquareType
    s.Side = side
    return &s
}

func (s *SquareType) Area() float64 {
    area := s.Side * s.Side
    return area
}

datastore/datastore.go

package datastore

import (
    "project/shapes"
)

type ShapeStorer interface {
    Put(shape shapes.Shaper)
    Close()
}

datastore/sqlite3/sqlite3.go

package sqlite3

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "project/datastore"
    "project/shapes"
)

type Sqlite3DatastoreType struct {
    DB *sql.DB
}

func New(filename string) datastore.ShapeStorer {
    var ds Sqlite3DatastoreType

    db, sqlerr := sql.Open("sqlite3", filename)
    if sqlerr != nil {
        log.Fatalln("Unable to open file due to error: ", sqlerr)
    }
    ds.DB = db

    return &ds
}

func (ds *Sqlite3DatastoreType) Close() {
    err := ds.DB.Close()
    if err != nil {
        log.Fatalln(err)
    }
}

func (ds *Sqlite3DatastoreType) Put(shape shapes.Shaper) {
    log.Println("Help")
    // here you could either do a switch statement on the object type
    // or you could do something like shape.Write(), if Write() was defined
    // on the interface of shape/shapes.go Shaper interface and then
    // implemented in the Square and Circle objects. 
}

Since the database tables will be different for the Circle and Square objects, some how I need to have a method for each one. I can get this to work if I add a method to the circle package and square package to do the insert. But like I said above, that feels like I am doing it wrong.

Thanks much in advance.

ain
  • 22,394
  • 3
  • 54
  • 74
jordan2175
  • 878
  • 2
  • 10
  • 20
  • I think, there is no *right/wrong* approach. It's a matter of style. You can choose [`ActiveRecord` pattern](https://en.wikipedia.org/wiki/Active_record_pattern), or [`Data Mapper` pattern](https://en.wikipedia.org/wiki/Data_mapper_pattern). In the former, *CRUD* operation is defined as part of the *object*, while in the later, your *object* only holds the data and doesn't know anything about persistence layer (database). – putu Oct 24 '17 at 03:31
  • If you do the switch statement on the object types in datastore/sqlite3/sqlite3.go then all of your SQL statements can stay in the sqlite package. If you do it the other way, as I have illustrated in the comment, then you are putting database code in the objects. Then what happens when yo need to support 10 different datastores. It just seems wrong. It also seems like I am missing some neat trick to make this work better. – jordan2175 Oct 24 '17 at 04:25
  • It seems wrong because you *limit* the solution using *type switching*. You can use existing ORM package (e.g. [gorm](https://github.com/jinzhu/gorm)), or you can develop your own. In the later case, I suggest you to define a *convention/rule* for the table naming in relation with *struct* name. For example, you can define table name for `CircleType` will be `CircleType` or `circle_type`, etc... Then, use tags to define mapping between table's column name and *struct*'s field name. Based on these, you can construct SQL for simple CRUD inside *datastore* using reflection. – putu Oct 24 '17 at 05:00

1 Answers1

1

Type switch is the right thing to do. Your logic code, (the one you wrote because no body else did it before) should know nothing about the storage.

Lets say you will want to add a counter for requests and you decide to count requests on Redis. What then? add the counter collection name to the Shape too?

You then should create a new ShapeStorer as a decorator and in the put method call the Redis ShapeStorer and sqlite ShapeStorer.

For a no-sql databases you sometime don't care about the schema at all you just serialize the object and save it.

ZAky
  • 1,209
  • 8
  • 22