2016-02-07 1 views
0

Я создал отображение объектов в 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 
} 

Но каждый я создаю новую структуру, например структуру пользователя, которую я должен скопировать, вставить «crud section» выше и создать файл user_crud.go и заменить слова «Сообщение» на «Пользователь», а слова «сообщение» - пользователь». Я повторяю много кода, и это не очень сухо. Я могу что-то сделать, чтобы не повторять этот код для вещей, которые я бы использовал повторно? У меня всегда есть метод save() и всегда есть функция ByUnique(), где я могу вернуть структуру и поиск по уникальному столбцу.

В PHP это было легко, потому что PHP не статически типизирован.

Это можно сделать в Go?

+1

'Не используйте общие имена, такие как« я »,« это »или« я », идентификаторы, типичные для объектно-ориентированных языков, которые уделяют больше внимания методам, а не функциям.' - https: // github. com/golang/go/wiki/CodeReviewComments # имя-получателя – OneOfOne

ответ

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. И не одержимы сухим. Это не самоцель. Go фокусируется на том, что код является простым, понятным и надежным. Не создавайте что-то сложное и хрупкое, чтобы избежать ввода простой строки обработки ошибок. Это не означает, что вы не должны извлекать дублированный код. Это просто означает, что в Go обычно лучше повторять простой код немного, а не создавать сложный код, чтобы избежать его.


EDIT: Если вы хотите реализовать 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 не будет работать правильно, если вы передадите ему интерфейс, а не фактическую структуру, но, вероятно, это сработает. Тем не менее, я бы, скорее всего, просто передал идентификатор, а не добавлял эти накладные расходы.

+0

Я не совсем понял последнюю функцию сохранения. Как вы проходите в аргументе идентификатора функции? невозможно извлечь идентификатор из источника? Как source.id. Или синтаксис будет отличаться, так как это интерфейс? – Alex

+0

@ Убедитесь, что вы можете легко создать протокол идентификатора. Я изначально написал это так, но добавил еще один слой, и я не знал, были ли у всех ваших предметов поле Id. В любом случае все будет хорошо. –

+0

Я все еще не понимаю, как использовать его внутри функции Save(). Извините, но мне требуется дополнительное объяснение, чтобы вы могли полностью понять, как использовать идентификатор внутри функции Save и как использовать протокол Identifier.Спасибо за помощь, я очень ценю помощь! – Alex

0

Возможно, вы захотите использовать ОРМ. Они устраняют много кода шаблона, который вы описываете.

См. this question для "Что такое ОРМ?"

Вот список ORMs для пути: https://github.com/avelino/awesome-go#orm

Я никогда не использовал один сам, так что я не могу рекомендовать какой-либо. Основная причина заключается в том, что ORM берет на себя большой контроль со стороны разработчика и вводит незначительные издержки производительности. Вы должны сами убедиться, что они подходят вашему прецеденту и/или если вам нравится «волшебство», которое происходит в этих библиотеках.

0

Я не рекомендую это делать, я лично предпочел бы быть явным о сканировании в структуры и создании запросов.

Но если вы действительно хотите, чтобы придерживаться отражения вы могли бы сделать:

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) 

Подход, я использую и рекомендую вам:

type Redirect struct { 
    ID  string 
    URL  string 
    CreatedAt time.Time 
} 

func FindByID(db *sql.DB, id string) (*Redirect, error) { 
    var redirect Redirect 

    err := db.QueryRow(
     `SELECT "id", "url", "created_at" FROM "redirect" WHERE "id" = $1`, id). 
     Scan(&redirect.ID, &redirect.URL, &redirect.CreatedAt) 

    switch { 
    case err == sql.ErrNoRows: 
     return nil, nil 
    case err != nil: 
     return nil, err 
    } 

    return &redirect, nil 
} 

func Save(db *sql.DB, redirect *Redirect) error { 
    redirect.CreatedAt = time.Now() 

    _, err := db.Exec(
     `INSERT INTO "redirect" ("id", "url", "created_at") VALUES ($1, $2, $3)`, 
     redirect.ID, redirect.URL, redirect.CreatedAt) 

    return err 
} 

Это имеет преимущество использования системы типов и отображения только тех вещей, которые она должна фактически отображать.

+0

Я не совсем понимаю интерфейс идентификации. Как бы вы использовали его на практике с помощью функции Save()? Спасибо, кстати! – Alex

+0

Кажется, что Роб ответил на этот вопрос уже – Danilo

Смежные вопросы