1. Design Background

Everyone knows that usability and maintainability have always been the focus of goframe, and it’s also a significant difference between goframe and other frameworks and components. goframe does not adopt other common ORM model association designs like BelongsTo, HasOne, HasMany, ManyToMany, which are cumbersome to maintain due to foreign key constraints, additional tag annotations, etc., imposing a certain cognitive load on developers. Therefore, the framework is inclined not to inject overly complex tag content, associated attributes, or methods into model structures and consistently tries to simplify the design with the goal of making model association queries as understandable and easy to use as possible. Before learning more about the With feature, it is recommended to first understand Model Association - ScanList.

Through a series of project practices, we found that although ScanList maintains model associations from a runtime business logic perspective, this association maintenance is not as straightforward as expected. Therefore, we continue to improve and introduce the With model association feature, which can easily maintain the association relationships through models. Of course, this feature is still dedicated to enhancing the usability and maintainability of the overall framework, and it can be seen as a combination and improvement of ScanList and model association maintenance.

Model Association - With - 图1warning

The With feature is currently experimental.

2. An Example

Let’s start with a simple example to help better understand the With feature, which is an improved version of the same example from the previous ScanList section.

1. Data Structure

  1. # User Table
  2. CREATE TABLE `user` (
  3. id int(10) unsigned NOT NULL AUTO_INCREMENT,
  4. name varchar(45) NOT NULL,
  5. PRIMARY KEY (id)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  7. # User Detail
  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 Scores
  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. PRIMARY KEY (id)
  19. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2. Data Structure

Based on the table definitions, we can tell:

  1. The user table and user details have a 1:1 relationship.
  2. The user table and user scores have a 1:N relationship.
  3. We did not demonstrate a N:N relationship here because, compared to a 1:N query, it’s just an additional association or one more query, and the final processing method is similar to 1:N.

The Golang model can be defined as follows:

  1. // User Detail
  2. type UserDetail struct {
  3. g.Meta `orm:"table:user_detail"`
  4. Uid int `json:"uid"`
  5. Address string `json:"address"`
  6. }
  7. // User Scores
  8. type UserScores struct {
  9. g.Meta `orm:"table:user_scores"`
  10. Id int `json:"id"`
  11. Uid int `json:"uid"`
  12. Score int `json:"score"`
  13. }
  14. // User Information
  15. type User struct {
  16. g.Meta `orm:"table:user"`
  17. Id int `json:"id"`
  18. Name string `json:"name"`
  19. UserDetail *UserDetail `orm:"with:uid=id"`
  20. UserScores []*UserScores `orm:"with:uid=id"`
  21. }

3. Data Insertion

To simplify the example, we create 5 user records, using transactional operations:

  • User information, id ranges from 1-5, name ranges from name_1 to name_5.
  • Simultaneously create 5 user detail records, where address data ranges from address_1 to address_5.
  • Each user has 5 score entries, scoring 1-5.
  1. g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
  2. for i := 1; i <= 5; i++ {
  3. // User.
  4. user := User{
  5. Name: fmt.Sprintf(`name_%d`, i),
  6. }
  7. lastInsertId, err := g.Model(user).Data(user).OmitEmpty().InsertAndGetId()
  8. if err != nil {
  9. return err
  10. }
  11. // Detail.
  12. userDetail := UserDetail{
  13. Uid: int(lastInsertId),
  14. Address: fmt.Sprintf(`address_%d`, lastInsertId),
  15. }
  16. _, err = g.Model(userDetail).Data(userDetail).OmitEmpty().Insert()
  17. if err != nil {
  18. return err
  19. }
  20. // Scores.
  21. for j := 1; j <= 5; j++ {
  22. userScore := UserScores{
  23. Uid: int(lastInsertId),
  24. Score: j,
  25. }
  26. _, err = g.Model(userScore).Data(userScore).OmitEmpty().Insert()
  27. if err != nil {
  28. return err
  29. }
  30. }
  31. }
  32. return nil
  33. })

After execution, the database data is as follows:

  1. mysql> show tables;
  2. +----------------+
  3. | Tables_in_test |
  4. +----------------+
  5. | user |
  6. | user_detail |
  7. | user_score |
  8. +----------------+
  9. 3 rows in set (0.01 sec)
  10. mysql> select * from `user`;
  11. +----+--------+
  12. | id | name |
  13. +----+--------+
  14. | 1 | name_1 |
  15. | 2 | name_2 |
  16. | 3 | name_3 |
  17. | 4 | name_4 |
  18. | 5 | name_5 |
  19. +----+--------+
  20. 5 rows in set (0.01 sec)
  21. mysql> select * from `user_detail`;
  22. +-----+-----------+
  23. | uid | address |
  24. +-----+-----------+
  25. | 1 | address_1 |
  26. | 2 | address_2 |
  27. | 3 | address_3 |
  28. | 4 | address_4 |
  29. | 5 | address_5 |
  30. +-----+-----------+
  31. 5 rows in set (0.00 sec)
  32. mysql> select * from `user_score`;
  33. +----+-----+-------+
  34. | id | uid | score |
  35. +----+-----+-------+
  36. | 1 | 1 | 1 |
  37. | 2 | 1 | 2 |
  38. | 3 | 1 | 3 |
  39. | 4 | 1 | 4 |
  40. | 5 | 1 | 5 |
  41. | 6 | 2 | 1 |
  42. | 7 | 2 | 2 |
  43. | 8 | 2 | 3 |
  44. | 9 | 2 | 4 |
  45. | 10 | 2 | 5 |
  46. | 11 | 3 | 1 |
  47. | 12 | 3 | 2 |
  48. | 13 | 3 | 3 |
  49. | 14 | 3 | 4 |
  50. | 15 | 3 | 5 |
  51. | 16 | 4 | 1 |
  52. | 17 | 4 | 2 |
  53. | 18 | 4 | 3 |
  54. | 19 | 4 | 4 |
  55. | 20 | 4 | 5 |
  56. | 21 | 5 | 1 |
  57. | 22 | 5 | 2 |
  58. | 23 | 5 | 3 |
  59. | 24 | 5 | 4 |
  60. | 25 | 5 | 5 |
  61. +----+-----+-------+
  62. 25 rows in set (0.00 sec)

4. Data Query

With the new With feature, data querying is quite straightforward. For example, to query one record:

  1. // Redefine it to avoid scrolling
  2. // User Detail
  3. type UserDetail struct {
  4. g.Meta `orm:"table:user_detail"`
  5. Uid int `json:"uid"`
  6. Address string `json:"address"`
  7. }
  8. // User Scores
  9. type UserScores struct {
  10. g.Meta `orm:"table:user_scores"`
  11. Id int `json:"id"`
  12. Uid int `json:"uid"`
  13. Score int `json:"score"`
  14. }
  15. // User Information
  16. type User struct {
  17. g.Meta `orm:"table:user"`
  18. Id int `json:"id"`
  19. Name string `json:"name"`
  20. UserDetail *UserDetail `orm:"with:uid=id"`
  21. UserScores []*UserScores `orm:"with:uid=id"`
  22. }
  23. var user *User
  24. // WithAll will query fields with with tags, in this example, it will query tables corresponding to the UserDetail and UserScores structures
  25. g.Model(tableUser).WithAll().Where("id", 3).Scan(&user)

The above statement will query information for a user with ID 3, including user information, user details, and user score information. The above statement will automatically execute the following SQL statements in the database:

  1. 2021-05-02 22:29:52.634 [DEBU] [ 2 ms] [default] SHOW FULL COLUMNS FROM `user`
  2. 2021-05-02 22:29:52.635 [DEBU] [ 1 ms] [default] SELECT * FROM `user` WHERE `id`=3 LIMIT 1
  3. 2021-05-02 22:29:52.636 [DEBU] [ 1 ms] [default] SHOW FULL COLUMNS FROM `user_detail`
  4. 2021-05-02 22:29:52.637 [DEBU] [ 1 ms] [default] SELECT `uid`,`address` FROM `user_detail` WHERE `uid`=3 LIMIT 1
  5. 2021-05-02 22:29:52.643 [DEBU] [ 6 ms] [default] SHOW FULL COLUMNS FROM `user_score`
  6. 2021-05-02 22:29:52.644 [DEBU] [ 0 ms] [default] SELECT `id`,`uid`,`score` FROM `user_score` WHERE `uid`=3

After execution, the information printed by g.Dump(user) is as follows:

  1. {
  2. Id: 3,
  3. Name: "name_3",
  4. UserDetail: {
  5. Uid: 3,
  6. Address: "address_3",
  7. },
  8. UserScores: [
  9. {
  10. Id: 11,
  11. Uid: 3,
  12. Score: 1,
  13. },
  14. {
  15. Id: 12,
  16. Uid: 3,
  17. Score: 2,
  18. },
  19. {
  20. Id: 13,
  21. Uid: 3,
  22. Score: 3,
  23. },
  24. {
  25. Id: 14,
  26. Uid: 3,
  27. Score: 4,
  28. },
  29. {
  30. Id: 15,
  31. Uid: 3,
  32. Score: 5,
  33. },
  34. ],
  35. }

5. List Query

Let’s see an example of querying a list through the With feature:

  1. var users []*User
  2. // With(UserDetail{}) only queries the table corresponding to UserDetail in the User structure
  3. g.Model(users).With(UserDetail{}).Where("id>?", 3).Scan(&users)

After execution, the data printed by g.Dump(users) is as follows:

  1. [
  2. {
  3. Id: 4,
  4. Name: "name_4",
  5. UserDetail: {
  6. Uid: 4,
  7. Address: "address_4",
  8. },
  9. UserScores: [],
  10. },
  11. {
  12. Id: 5,
  13. Name: "name_5",
  14. UserDetail: {
  15. Uid: 5,
  16. Address: "address_5",
  17. },
  18. UserScores: [],
  19. },
  20. ]

6. Conditions and Sorting

When associating with the With feature, additional association conditions can be specified, as well as sorting rules for multiple data results. For example:

  1. type User struct {
  2. g.Meta `orm:"table:user"`
  3. Id int `json:"id"`
  4. Name string `json:"name"`
  5. UserDetail *UserDetail `orm:"with:uid=id, where:uid > 3"`
  6. UserScores []*UserScores `orm:"with:uid=id, where:score>1 and score<5, order:score desc"`
  7. }

Use the where sub-tag and order sub-tag in the orm tag to specify additional association conditions and sorting rules.

7. unscoped Tag

The with struct tag supports the unscoped feature, for example:

  1. type User struct {
  2. g.Meta `orm:"table:user"`
  3. Id int `json:"id"`
  4. Name string `json:"name"`
  5. UserDetail *UserDetail `orm:"with:uid=id, unscoped:true"`
  6. UserScores []*UserScore `orm:"with:uid=id, unscoped:true"`
  7. }

3. Detailed Explanation

You might be curious about some of the usages above, such as the gmeta package, the WithAll method, the with statement in the orm tag, and the Model method’s struct parameter recognizing table names, etc. That’s right, let’s talk about them in detail.

1. gmeta Package

In the above data structures, you can see a g.Meta struct is embedded in an embed way, like:

  1. type UserDetail struct {
  2. g.Meta `orm:"table:user_detail"`
  3. Uid int `json:"uid"`
  4. Address string `json:"address"`
  5. }

Within the GoFrame framework, there are many such small component packages for implementing specific functions. The gmeta package is mainly used to embed into user-defined structures, and using tags to mark the struct (like g.Meta) in the gmeta package with custom tag content (such as `orm:"table:user_detail"` ), and can dynamically obtain these custom tag contents with specific methods at runtime. For more details, refer to the chapter: Metadata.

Therefore, embedding g.Meta here is to label the data table name associated with the struct.

2. Model Association Specification

In the following structure:

  1. type User struct {
  2. g.Meta `orm:"table:user"`
  3. Id int `json:"id"`
  4. Name string `json:"name"`
  5. UserDetail *UserDetail `orm:"with:uid=id"`
  6. UserScores []*UserScore `orm:"with:uid=id"`
  7. }

We bind the orm tag to the specified struct property, and specify the association relationship between the current struct (table) and the target struct (table) through the with statement in the orm tag. The syntax of the with statement is as follows:

  1. with:target_table_association_field=current_structure_association_field

The field names are case-insensitive and ignore special characters. For example, the following forms of associations can all be automatically recognized:

  1. with:UID=ID
  2. with:Uid=Id
  3. with:U_ID=id

If the association fields of both tables have the same name, you can just write one, such as:

  1. with:uid

In this example, the table corresponding to the UserDetail property is user_detail, and the table corresponding to the UserScores property is user_score. Both are associated with the user table of the current User struct using uid, and the associated field of the target user table is id.

3. With/WithAll

1) Introduction

By default, even if the properties in our struct have orm tags with with statements, the ORM component will not enable the With feature for association queries by default. It needs to be enabled by the With/WithAll method.

  • With: Specify the association query tables by specifying the property objects.
  • WithAll: Enable association queries for all property structures with with statements in the operating object.

In our example, the WithAll method is used, so all property model association queries in the User table are automatically enabled. As long as the property struct is associated with a table and the orm tag contains a with statement, it will automatically query data and bind data according to the model structure association relationship. If we only enable association queries for some properties rather than all property models, we can use the With method to specify. And the With method can specify multiple associated model automatic queries. The WithAll in this example is equivalent to:

  1. var user *User
  2. g.Model(tableUser).With(UserDetail{}, UserScore{}).Where("id", 3).Scan(&user)

Or like this:

  1. var user *User
  2. g.Model(tableUser).With(User{}.UserDetail, User{}.UserScore).Where("id", 3).Scan(&user)

2) Only Associate User Detail Model

If we only need to query user details and not user scores, we can use the With method to enable association queries for the specified object corresponding tables, such as:

  1. var user *User
  2. g.Model(tableUser).With(UserDetail{}).Where("id", 3).Scan(&user)

Or like this:

  1. var user *User
  2. g.Model(tableUser).With(User{}.UserDetail).Where("id", 3).Scan(&user)

After execution, the data printed by g.Dump(user) is:

  1. {
  2. "id": 3,
  3. "name": "name_3",
  4. "UserDetail": {
  5. "uid": 3,
  6. "address": "address_3"
  7. },
  8. "UserScores": []
  9. }

3) Only Associate User Score Model

We can also associate and query only user score information, such as:

  1. var user *User
  2. g.Model(tableUser).With(UserScore{}).Where("id", 3).Scan(&user)

Or like this:

  1. var user *User
  2. g.Model(tableUser).With(User{}.UserScore).Where("id", 3).Scan(&user)

After execution, the data printed by g.Dump(user) is:

  1. {
  2. "id": 3,
  3. "name": "name_3",
  4. "UserDetail": null,
  5. "UserScores": [
  6. {
  7. "id": 11,
  8. "uid": 3,
  9. "score": 1
  10. },
  11. {
  12. "id": 12,
  13. "uid": 3,
  14. "score": 2
  15. },
  16. {
  17. "id": 13,
  18. "uid": 3,
  19. "score": 3
  20. },
  21. {
  22. "id": 14,
  23. "uid": 3,
  24. "score": 4
  25. },
  26. {
  27. "id": 15,
  28. "uid": 3,
  29. "score": 5
  30. }
  31. ]
  32. }

4) Do Not Associate Any Model Query

If we do not need any association query, it’s simpler, for example:

  1. var user *User
  2. g.Model(tableUser).Where("id", 3).Scan(&user)

After execution, the data printed by g.Dump(user) is:

  1. {
  2. "id": 3,
  3. "name": "name_3",
  4. "UserDetail": null,
  5. "UserScores": []
  6. }

4. Usage Restrictions

1. Field Query and Filtering

As seen in our example above, we have not specified the fields to query, but in the SQL logs printed, the query statement is not a simple SELECT * but executed concrete field queries. Under the With feature, automatic field query mapping according to the associated model object properties will happen, and it will automatically filter out fields that cannot be mapped.

Therefore, under the With feature, we cannot query only some corresponding properties’ fields. To query and assign only specific fields, it is recommended to trim the model data structure according to business scenarios and create data structures that meet specific business scenarios, rather than using one data structure to fit multiple different scenarios.

Let’s use an example for better illustration. Suppose we have an entity object data structure Content, a common content model in a CMS system as follows, which corresponds to the fields of the data table:

  1. type Content struct {
  2. Id uint `orm:"id,primary" json:"id"` // Auto-increment ID
  3. Key string `orm:"key" json:"key"` // Unique key name, generally not commonly used
  4. Type string `orm:"type" json:"type"` // Content model: topic, ask, article, etc., defined by the program
  5. CategoryId uint `orm:"category_id" json:"category_id"` // Category ID
  6. UserId uint `orm:"user_id" json:"user_id"` // User ID
  7. Title string `orm:"title" json:"title"` // Title
  8. Content string `orm:"content" json:"content"` // Content
  9. Sort uint `orm:"sort" json:"sort"` // Sort order, lower value means higher priority, default is the timestamp when added, can be used to pin
  10. Brief string `orm:"brief" json:"brief"` // Summary
  11. Thumb string `orm:"thumb" json:"thumb"` // Thumbnail
  12. Tags string `orm:"tags" json:"tags"` // Tag names list, stored in JSON
  13. Referer string `orm:"referer" json:"referer"` // Content Source, e.g., GitHub/Gitee
  14. Status uint `orm:"status" json:"status"` // Status 0: Normal, 1: Disabled
  15. ReplyCount uint `orm:"reply_count" json:"reply_count"` // Reply count
  16. ViewCount uint `orm:"view_count" json:"view_count"` // View count
  17. ZanCount uint `orm:"zan_count" json:"zan_count"` // Likes
  18. CaiCount uint `orm:"cai_count" json:"cai_count"` // Dislikes
  19. CreatedAt *gtime.Time `orm:"created_at" json:"created_at"` // Creation time
  20. UpdatedAt *gtime.Time `orm:"updated_at" json:"updated_at"` // Update time
  21. }

The content list page does not need to display such detailed content, especially the Content field, which is very large. We only need to query a few fields for the list page. Therefore, we can define a separate data structure for list returns (field trimming) instead of directly using the data table entity object data structure. For example:

  1. type ContentListItem struct {
  2. Id uint `json:"id"` // Auto-increment ID
  3. CategoryId uint `json:"category_id"` // Category ID
  4. UserId uint `json:"user_id"` // User ID
  5. Title string `json:"title"` // Title
  6. CreatedAt *gtime.Time `json:"created_at"` // Creation time
  7. UpdatedAt *gtime.Time `json:"updated_at"` // Update time
  8. }

2. Must Exist Association Field Property

The With feature is achieved by recognizing data structure associations and automatically executing multiple SQL queries, so associated fields must exist as object properties for automatic retrieval of association field values. Simply put, the fields in the with tag must be present in the attributes of the associated object.

5. Recursive Association

If the associated model properties also have with tags, recursive association querying will occur. The With feature supports unlimited levels of recursive association. The following example is for reference only:

  1. // User Detail
  2. type UserDetail struct {
  3. g.Meta `orm:"table:user_detail"`
  4. Uid int `json:"uid"`
  5. Address string `json:"address"`
  6. }
  7. // User Scores - Required Courses
  8. type UserScoresRequired struct {
  9. g.Meta `orm:"table:user_scores"`
  10. Id int `json:"id"`
  11. Uid int `json:"uid"`
  12. Score int `json:"score"`
  13. }
  14. // User Scores - Elective Courses
  15. type UserScoresOptional struct {
  16. g.Meta `orm:"table:user_scores"`
  17. Id int `json:"id"`
  18. Uid int `json:"uid"`
  19. Score int `json:"score"`
  20. }
  21. // User Scores
  22. type UserScores struct {
  23. g.Meta `orm:"table:user_scores"`
  24. Id int `json:"id"`
  25. Uid int `json:"uid"`
  26. Required []UserScoresRequired `orm:"with:id, where:type=1"`
  27. Optional []UserScoresOptional `orm:"with:id, where:type=2"`
  28. }
  29. // User Information
  30. type User struct {
  31. g.Meta `orm:"table:user"`
  32. Id int `json:"id"`
  33. Name string `json:"name"`
  34. UserDetail *UserDetail `orm:"with:uid=id"`
  35. UserScores []*UserScores `orm:"with:uid=id"`
  36. }

6. Model Examples

Based on the current data tables, more model writing examples are provided for reference.

1. Nested Associated Models

  1. type UserDetail struct {
  2. g.Meta `orm:"table:user_detail"`
  3. Uid int `json:"uid"`
  4. Address string `json:"address"`
  5. }
  6. type UserScores struct {
  7. g.Meta `orm:"table:user_scores"`
  8. Id int `json:"id"`
  9. Uid int `json:"uid"`
  10. Score int `json:"score"`
  11. }
  12. type User struct {
  13. g.Meta `orm:"table:user"`
  14. *UserDetail `orm:"with:uid=id"`
  15. Id int `json:"id"`
  16. Name string `json:"name"`
  17. UserScores []*UserScores `orm:"with:uid=id"`
  18. }

Nested models also support nesting to allow automatic data assignment for embedded structures, such as:

  1. type UserDetail struct {
  2. Uid int `json:"uid"`
  3. Address string `json:"address"`
  4. }
  5. type UserDetailEmbedded struct {
  6. UserDetail
  7. }
  8. type UserScores struct {
  9. Id int `json:"id"`
  10. Uid int `json:"uid"`
  11. Score int `json:"score"`
  12. }
  13. type User struct {
  14. *UserDetailEmbedded `orm:"with:uid=id"`
  15. Id int `json:"id"`
  16. Name string `json:"name"`
  17. UserScores []*UserScores `orm:"with:uid=id"`
  18. }

2. Basic Model Nesting

  1. type UserDetail struct {
  2. g.Meta `orm:"table:user_detail"`
  3. Uid int `json:"uid"`
  4. Address string `json:"address"`
  5. }
  6. type UserScores struct {
  7. g.Meta `orm:"table:user_scores"`
  8. Id int `json:"id"`
  9. Uid int `json:"uid"`
  10. Score int `json:"score"`
  11. }
  12. type UserEmbedded struct {
  13. Id int `json:"id"`
  14. Name string `json:"name"`
  15. }
  16. type User struct {
  17. g.Meta `orm:"table:user"`
  18. UserEmbedded
  19. *UserDetail `orm:"with:uid=id"`
  20. UserScores []*UserScores `orm:"with:uid=id"`
  21. }

3. Models Without meta Information

The meta structure in the model is crucial for specifying the table name. When there is no meta information, the table name for query will automatically use the CaseSnake name of the struct. For example, UserDetail will automatically use the user_detail table name, and UserScores will automatically use the user_scores table name.

  1. type UserDetail struct {
  2. Uid int `json:"uid"`
  3. Address string `json:"address"`
  4. }
  5. type UserScores struct {
  6. Id int `json:"id"`
  7. Uid int `json:"uid"`
  8. Score int `json:"score"`
  9. }
  10. type User struct {
  11. *UserDetail `orm:"with:uid=id"`
  12. Id int `json:"id"`
  13. Name string `json:"name"`
  14. UserScores []*UserScores `orm:"with:uid=id"`
  15. }

7. Future Improvements

  • Currently, the With feature is only implemented for query operations and does not support write or update operations.