如何在Go中使这个对象映射更干燥和可重用?

时间:2016-02-07 22:47:21

标签: go

我在Go中创建了一个非关系的对象映射,它非常简单。

我有几个看起来像这样的结构:

type Message struct {
    Id       int64
    Message  string
    ReplyTo  sql.NullInt64 `db:"reply_to"`
    FromId   int64         `db:"from_id"`
    ToId     int64         `db:"to_id"`
    IsActive bool          `db:"is_active"`
    SentTime int64         `db:"sent_time"`
    IsViewed bool          `db:"is_viewed"`

    Method   string `db:"-"`
    AppendTo int64  `db:"-"`
}

要创建新消息,我只需运行此功能:

func New() *Message {
    return &Message{
        IsActive: true,
        SentTime: time.Now().Unix(),
        Method:   "new",
    }
}

然后我有一个这个结构的message_crud.go文件,如下所示:

要按唯一列查找消息(例如按id),我运行此函数:

func ByUnique(column string, value interface{}) (*Message, error) {

    query := fmt.Sprintf(`
        SELECT *
        FROM message
        WHERE %s = ?
        LIMIT 1;
    `, column)

    message := &Message{}
    err := sql.DB.QueryRowx(query, value).StructScan(message)
    if err != nil {
        return nil, err
    }
    return message, nil
}

要保存消息(在数据库中插入或更新),我运行此方法:

func (this *Message) save() error {
    s := ""
    if this.Id == 0 {
        s = "INSERT INTO message SET %s;"
    } else {
        s = "UPDATE message SET %s WHERE id=:id;"
    }
    query := fmt.Sprintf(s, sql.PlaceholderPairs(this))

    nstmt, err := sql.DB.PrepareNamed(query)
    if err != nil {
        return err
    }

    res, err := nstmt.Exec(*this)
    if err != nil {
        return err
    }

    if this.Id == 0 {
        lastId, err := res.LastInsertId()
        if err != nil {
            return err
        }
        this.Id = lastId
    }

    return nil
}

sql.PlaceholderPairs()函数如下所示:

func PlaceholderPairs(i interface{}) string {

    s := ""
    val := reflect.ValueOf(i).Elem()
    count := val.NumField()

    for i := 0; i < count; i++ {
        typeField := val.Type().Field(i)
        tag := typeField.Tag

        fname := strings.ToLower(typeField.Name)

        if fname == "id" {
            continue
        }

        if t := tag.Get("db"); t == "-" {
            continue
        } else if t != "" {
            s += t + "=:" + t
        } else {
            s += fname + "=:" + fname
        }
        s += ", "
    }
    s = s[:len(s)-2]
    return s
}

但每次我创建一个新结构时,例如一个User结构,我必须复制粘贴&#34; crud部分&#34;在上面创建一个user_crud.go文件并替换单词&#34; Message&#34;使用&#34;用户&#34;和单词&#34; message&#34;与&#34;用户&#34;。我重复了很多代码而且它不是很干。有什么我可以做的,不重复这些代码我会重用的东西?我总是有一个save()方法,并且总是有一个ByUnique()函数,我可以返回一个结构并按唯一列搜索。

在PHP中,这很简单,因为PHP不是静态类型的。

这可以在Go吗?

3 个答案:

答案 0 :(得分:0)

您可能想要使用ORM。 它们消除了许多你描述的样板代码。

请参阅this question了解&#34;什么是ORM?&#34;

以下是go https://github.com/avelino/awesome-go#orm

的ORM列表

我自己从未使用过,所以我无法推荐。主要原因是ORM需要开发人员的大量控制,并引入了不可忽视的性能开销。你需要亲自看看它们是否适合你的用例和/或你是否对#34;魔法&#34;那些图书馆正在进行。

答案 1 :(得分:0)

我不建议这样做,我个人更愿意明确扫描结构并创建查询。

但如果你真的想坚持反思你可以做到:

[indexPath section]

为了您的保存:

func ByUnique(obj interface{}, column string, value interface{}) error {
    // ...
    return sql.DB.QueryRowx(query, value).StructScan(obj)
}

// Call with
message := &Message{}
ByUnique(message, ...)

我使用的方法会向您推荐:

type Identifiable interface {
    Id() int64
}

// Implement Identifiable for message, etc.

func Save(obj Identifiable) error {
    // ...
}

// Call with
Save(message)

这样做的好处是可以使用类型系统并只映射应该实际映射的内容。

答案 2 :(得分:0)

您的ByUnique几乎已经是通用的了。只需拉出不同的部分(桌子和目的地):

func ByUnique(table string, column string, value interface{}, dest interface{}) error {
    query := fmt.Sprintf(`
            SELECT *
            FROM %s
            WHERE %s = ?
            LIMIT 1;
        `, table, column)

    return sql.DB.QueryRowx(query, value).StructScan(dest)
}

func ByUniqueMessage(column string, value interface{}) (*Message, error) {
    message := &Message{}
    if err := ByUnique("message", column, value, &message); err != nil {
        return nil, err
    }
    return message, error
}

您的save非常相似。您只需要按照以下方式进行通用保存功能:

func Save(table string, identifier int64, source interface{}) { ... }

然后在(*Message)save内,您只需调用常规Save()函数。看起来非常简单。

附注:不要使用this作为方法内对象的名称。有关详细信息,请参阅@OneOfOne的链接。并且不要对DRY着迷。它本身不是一个目标。 Go专注于简单,清晰和可靠的代码。不要创建复杂和脆弱的东西,以避免键入简单的错误处理行。这并不意味着您不应该提取重复的代码。它只是意味着在Go中通常更好地重复简单的代码而不是创建复杂的代码来避免它。

编辑:如果你想使用接口实现Save,那没问题。只需创建一个Identifier界面。

type Ider interface {
    Id() int64
    SetId(newId int64)
}

func (msg *Message) Id() int64 {
    return msg.Id
}

func (msg *Message) SetId(newId int64) {
    msg.Id = newId
}

func Save(table string, source Ider) error {
    s := ""
    if source.Id() == 0 {
        s = fmt.Sprintf("INSERT INTO %s SET %%s;", table)
    } else {
        s = fmt.Sprintf("UPDATE %s SET %%s WHERE id=:id;", table)
    }
    query := fmt.Sprintf(s, sql.PlaceholderPairs(source))

    nstmt, err := sql.DB.PrepareNamed(query)
    if err != nil {
        return err
    }

    res, err := nstmt.Exec(source)
    if err != nil {
        return err
    }

    if source.Id() == 0 {
        lastId, err := res.LastInsertId()
        if err != nil {
            return err
        }
        source.SetId(lastId)
    }

    return nil
}

func (msg *Message) save() error {
    return Save("message", msg)
}

可能会爆炸的一件事就是对Exec的召唤。我不知道你正在使用什么包,如果你传递一个接口而不是实际的结构,Exec可能无法正常工作,但它可能会起作用。也就是说,我可能只是传递标识符而不是增加这个开销。