The ORM of gf does not adopt common model association designs such as BelongsTo, HasOne, HasMany, ManyToMany found in other ORMs. Such association maintenance is quite cumbersome, involving foreign key constraints, additional tag annotations, etc., imposing a cognitive burden on developers. Therefore, the gf framework doesn’t favor injecting complex tag content, association attributes, or methods into model structs. It consistently strives to simplify the design, aiming to make model association queries as easy to understand and useful as possible.

Model Association - ScanList - 图1warning

The following implementation of model association provided by gf ORM is available from GoFrame v1.13.6 and is currently an experimental feature.

Let’s introduce the model association provided by gf ORM with an example.

Data Structure

To simplify the example, the tables we design here are as simple as possible, with each table containing only 3-4 fields to conveniently explain the associations.

  1. # User Table
  2. CREATE TABLE `user` (
  3. uid int(10) unsigned NOT NULL AUTO_INCREMENT,
  4. name varchar(45) NOT NULL,
  5. PRIMARY KEY (uid)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  7. # User Details
  8. CREATE TABLE `user_detail` (
  9. uid int(10) unsigned NOT NULL AUTO_INCREMENT,
  10. address varchar(45) NOT NULL,
  11. PRIMARY KEY (uid)
  12. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  13. # User Credits
  14. CREATE TABLE `user_scores` (
  15. id int(10) unsigned NOT NULL AUTO_INCREMENT,
  16. uid int(10) unsigned NOT NULL,
  17. score int(10) unsigned NOT NULL,
  18. course varchar(45) NOT NULL,
  19. PRIMARY KEY (id)
  20. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Data Model

Based on the table definition, we can infer:

  1. The user table and user details have a 1:1 relationship.
  2. The user table and user credits have a 1:N relationship.
  3. We don’t demonstrate the N:N relationship here, as it is similar to 1:N in terms of needing only one more association or query, with a similar final processing method.

The model in Golang can be defined as follows:

  1. // User Table
  2. type EntityUser struct {
  3. Uid int `orm:"uid"`
  4. Name string `orm:"name"`
  5. }
  6. // User Details
  7. type EntityUserDetail struct {
  8. Uid int `orm:"uid"`
  9. Address string `orm:"address"`
  10. }
  11. // User Credits
  12. type EntityUserScores struct {
  13. Id int `orm:"id"`
  14. Uid int `orm:"uid"`
  15. Score int `orm:"score"`
  16. Course string `orm:"course"`
  17. }
  18. // Composite Model, User Information
  19. type Entity struct {
  20. User *EntityUser
  21. UserDetail *EntityUserDetail
  22. UserScores []*EntityUserScores
  23. }

Here, EntityUser, EntityUserDetail, and EntityUserScores correspond to the data models for user table, user details, and user credits, respectively. Entity is a composite model representing all detailed information of a user.

Data Insertion

Data insertion involves simple database transactions.

  1. err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
  2. r, err := tx.Model("user").Save(EntityUser{
  3. Name: "john",
  4. })
  5. if err != nil {
  6. return err
  7. }
  8. uid, err := r.LastInsertId()
  9. if err != nil {
  10. return err
  11. }
  12. _, err = tx.Model("user_detail").Save(EntityUserDetail{
  13. Uid: int(uid),
  14. Address: "Beijing DongZhiMen #66",
  15. })
  16. if err != nil {
  17. return err
  18. }
  19. _, err = tx.Model("user_scores").Save(g.Slice{
  20. EntityUserScores{Uid: int(uid), Score: 100, Course: "math"},
  21. EntityUserScores{Uid: int(uid), Score: 99, Course: "physics"},
  22. })
  23. return err
  24. })

Data Query

Single Data Record

Querying a single model data is straightforward using the Scan method, which automatically identifies whether to bind query results to a single object attribute or an array object attribute. For example:

  1. // Define User List
  2. var user Entity
  3. // Query User Basic Data
  4. // SELECT * FROM `user` WHERE `name`='john'
  5. err := g.Model("user").Scan(&user.User, "name", "john")
  6. if err != nil {
  7. return err
  8. }
  9. // Query User Detail Data
  10. // SELECT * FROM `user_detail` WHERE `uid`=1
  11. err := g.Model("user_detail").Scan(&user.UserDetail, "uid", user.User.Uid)
  12. // Query User Credits Data
  13. // SELECT * FROM `user_scores` WHERE `uid`=1
  14. err := g.Model("user_scores").Scan(&user.UserScores, "uid", user.User.Uid)

This method has been introduced in previous sections, so I won’t repeat it here.

Multiple Data Records

To query multiple data records and bind the data to the data model array, you need to use the ScanList method. This method requires you to specify the relationship between the result fields and the model attributes, then iterates over the array and automatically binds the data. For example:

  1. // Define User List
  2. var users []Entity
  3. // Query User Basic Data
  4. // SELECT * FROM `user`
  5. err := g.Model("user").ScanList(&users, "User")
  6. // Query User Detail Data
  7. // SELECT * FROM `user_detail` WHERE `uid` IN(1,2)
  8. err := g.Model("user_detail").
  9. Where("uid", gdb.ListItemValuesUnique(users, "User", "Uid")).
  10. ScanList(&users, "UserDetail", "User", "uid:Uid")
  11. // Query User Credits Data
  12. // SELECT * FROM `user_scores` WHERE `uid` IN(1,2)
  13. err := g.Model("user_scores").
  14. Where("uid", gdb.ListItemValuesUnique(users, "User", "Uid")).
  15. ScanList(&users, "UserScores", "User", "uid:Uid")

This involves two important methods:

1. ScanList

Method definition:

  1. // ScanList converts <r> to struct slice which contains other complex struct attributes.
  2. // Note that the parameter <listPointer> should be type of *[]struct/*[]*struct.
  3. // Usage example:
  4. //
  5. // type Entity struct {
  6. // User *EntityUser
  7. // UserDetail *EntityUserDetail
  8. // UserScores []*EntityUserScores
  9. // }
  10. // var users []*Entity
  11. // or
  12. // var users []Entity
  13. //
  14. // ScanList(&users, "User")
  15. // ScanList(&users, "UserDetail", "User", "uid:Uid")
  16. // ScanList(&users, "UserScores", "User", "uid:Uid")
  17. // The parameters "User"/"UserDetail"/"UserScores" in the example codes specify the target attribute struct
  18. // that current result will be bound to.
  19. // The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational
  20. // struct attribute name. It automatically calculates the HasOne/HasMany relationship with given <relation>
  21. // parameter.
  22. // See the example or unit testing cases for clear understanding for this function.
  23. func (m *Model) ScanList(listPointer interface{}, attributeName string, relation ...string) (err error)

This method is used to bind the queried array data to the specified list, for example:

  • ScanList(&users, "User")

Indicates binding the queried user information array data to the User attribute of each item in the users list.

  • ScanList(&users, "UserDetail", "User", "uid:Uid")

Indicates binding the queried user detail array data to the UserDetail attribute of each item in the users list, and associating with another User object attribute through uid:Uid field:attribute relation. Internally, this will automatically handle data binding based on this association. Here uid:Uid specifies the uid field in the query result and Uid denotes the Uid attribute in the target associated object.

  • ScanList(&users, "UserScores", "User", "uid:Uid")

Indicates binding the queried user detail array data to the UserScores attribute of each item in the users list, and associating with another User object attribute through uid:Uid field:attribute relation. Internally, this method can automatically recognize that User to UserScores is essentially a 1:N relation due to UserScores being an array type []*EntityUserScores, and thus completes data binding.

Do note, if the corresponding association attribute data does not exist within the associated data, the attribute will not be initialized and will remain nil.

2. ListItemValues/ListItemValuesUnique

Method definition:

  1. // ListItemValues retrieves and returns the elements of all item struct/map with key <key>.
  2. // Note that the parameter <list> should be type of slice which contains elements of map or struct,
  3. // or else it returns an empty slice.
  4. //
  5. // The parameter <list> supports types like:
  6. // []map[string]interface{}
  7. // []map[string]sub-map
  8. // []struct
  9. // []struct:sub-struct
  10. // Note that the sub-map/sub-struct makes sense only if the optional parameter <subKey> is given.
  11. func ListItemValues(list interface{}, key interface{}, subKey ...interface{}) (values []interface{})
  12. // ListItemValuesUnique retrieves and returns the unique elements of all struct/map with key <key>.
  13. // Note that the parameter <list> should be type of slice which contains elements of map or struct,
  14. // or else it returns an empty slice.
  15. // See gutil.ListItemValuesUnique.
  16. func ListItemValuesUnique(list interface{}, key string, subKey ...interface{}) []interface{}

The difference between ListItemValuesUnique and ListItemValues is that the former filters out duplicate return values, ensuring the returned list does not contain duplicates. These functions are used to obtain specified attribute/key values from elements in a given list containing struct/map data items, constructing them into a []interface{} array to return. Example:

  • gdb.ListItemValuesUnique(users, "Uid") is used to obtain every Uid attribute from the users array, constructing it into a []interface{} array to return. This facilitates constructing a SELECT...IN... query based on uid.
  • gdb.ListItemValuesUnique(users, "User", "Uid") is used to obtain every Uid attribute from the User property item in the users array, constructing it into a []interface{} array to return. This facilitates constructing a SELECT...IN... query based on uid.