There are several structures (Users, Projects, Messages, etc) with a different set of fields. We make a request to the base, we get the rows , then for rows.Next() { rows.Scan(&поле1, &поле2) } this is understandable. But I would like to avoid repeating the code for each structure and make a function that any Rows converts to the desired structure. How best to implement it?

  • Use ORM (for example, GORM), or something more lightweight Dat , sqlx - Pifagorych

1 answer 1

We need a method that will take its own rows or row . I warn you at once that with the approach that I describe here it will be very difficult to make changes. So go:

 // Если моделей много, то удобней вынести их в отдельный пакет // Package models contains all sql-models package models import ( // в моём примере используется PosqtgreSQL и эта библиотека "github.com/lib/pq" "database/sql" "fmt" "time" ) // Scanner - интерфейс необходим для того // чтобы можно было испоьзовать одинаково // *sql.Rows и *sql.Row type Scanner interface { Scan(...interface{}) error } // Собственно тривиальная модель type User struct { ID int64 FirstName string LastName string Email string CreatedAt time.Time UpdatedAt time.Time DeletedAt time.Time } // Эта функция будет использоваться для извлечения // данных из rows. func (u *User) Scan(row Scanner) error { // строка может быть NULL-ом var firstName sql.NullString var lastName sql.NullString // при этом Email в базе данных отмечен // как NOT NULL и переживать о том, что // он будет NULL-ом нет смысла // Время может быть NULL-ом. В этом случае // я использую фишку библиотеки github.com/lib/pq var createdAt pq.NullTime var updatedAt pq.NullTime var deletedAt pq.NullTime // Собственно сканирование err := row.Scan( &u.ID, &firstName, &lastName, &u.Email, &createdAt, &updatedAt, &deletedAt, ) // ошибка сканнирования if err != nil { return err } // Проверка на валидность тех полей что // могут быть NULL-ом. В моём случае // я использую свеже-созданную структуру // и поэтому не сбрасываю значение, если оно // NULL. if firstName.Valid { u.FirstName = firstName.String } if lastName.Valid { u.LastName = lastName.String } if createdAt.Valid { u.CreatedAt = createdAt.Time } if updatedAt.Valid { u.UpdatedAt = updatedAt.Time } if deletedAt.Valid { u.DeletedAt = deletedAt.Time } return nil } 

How to use:

 for rows.Next() { usr := new(User) if err := usr.Scan(rows); err != nil { rows.Close() return err } // // работа с заполненным пользователем // } 

It is also often necessary to use complex queries like INNER JOIN and so on. It is convenient to do a couple more functions - extract all the fields of the structure as []interface{} and assign them back. If the types NullString , NullTime and the like are not used, then you can do with only one function. However, in SQL, you will have to insert NOT NULL everywhere, which is not always possible / productive.

For example

 // Извлечение полей. При этом для полей ID // и Email я использую просто указатель // для остальных - временные переменные. func (u *User) Fields() []interface{} { var firstName sql.NullString var lastName sql.NullString var createdAt pq.NullTime var updatedAt pq.NullTime var deletedAt pq.NullTime // отмеченные крестиком поля // требуют проверку на валидность // остальные будут записаны в структуру // и так (указатель же) return []interface{}{ &u.ID, // 0 &firstName, // 1 x &lastName, // 2 x &u.Email, // 3 &createdAt, // 4 x &updatedAt, // 5 x &deletedAt, // 6 x } } // Присвоение полей обратно. Тут главное не // напутать. Собственно в этом и весь основной // гемморрой - для каждого сложного запроса // знать в каком порядке следуют данные. func (u *User) Apply(fields ...interface{}) { // поля 0 и 3 уже на месте firstName := fields[1].(*sql.NullString) lastName := fields[2].(*sql.NullString) createdAt := fields[4].(*pq.NullTime) updatedAt := fields[5].(*pq.NullTime) deletedAt := fields[6].(*pq.NullTime) if firstName.Valid { u.FirstName = firstName.String } if lastName.Valid { u.LastName = lastName.String } if createdAt.Valid { u.CreatedAt = createdAt.Time } if updatedAt.Valid { u.UpdatedAt = updatedAt.Time } if deletedAt.Valid { u.DeletedAt = deletedAt.Time } } 

How to use. So, suppose that in addition to User , we also have an App and we make an INNER JOIN , so that the answer is first User and then App :

 for rows.Next() { usr := new(User) app := new(App) usrFields := usr.Fields() appFields := app.Fields() if err := rows.Scan(append(usrFields, appFields...)...); err != nil { rows.Close() return err } usr.Apply(usrFields...) app.Apply(appFields...) // // работа с заполненными пользователем и приложением // }