name: Advanced Queries

sort: 4

Advanced Queries

ORM uses QuerySeter to organize queries. Every method that returns QuerySeter will give you a new QuerySeter object.

Basic Usage:

  1. o := orm.NewOrm()
  2. // Get a QuerySeter object. User is table name
  3. qs := o.QueryTable("user")
  4. // Can also use object as table name
  5. user := new(User)
  6. qs = o.QueryTable(user) // return a QuerySeter

expr

expr describes fields and SQL operators in QuerySeter.

Field combination orders are decided by the relationship of tables. For example, User has a foreign key to Profile, so if you want to use Profile.Age as the condition, you have to use the expression Profile__Age. Note that the separator is double under scores __. Expr can also append operators at the end to execute related SQL. For example, Profile__Age__gt represents condition query Profile.Age > 18.

Comments below describe SQL statements that are similar to the expr, but may not be the exact generated results.

  1. qs.Filter("id", 1) // WHERE id = 1
  2. qs.Filter("profile__age", 18) // WHERE profile.age = 18
  3. qs.Filter("Profile__Age", 18) // key name and field name are both valid
  4. qs.Filter("profile__age", 18) // WHERE profile.age = 18
  5. qs.Filter("profile__age__gt", 18) // WHERE profile.age > 18
  6. qs.Filter("profile__age__gte", 18) // WHERE profile.age >= 18
  7. qs.Filter("profile__age__in", 18, 20) // WHERE profile.age IN (18, 20)
  8. qs.Filter("profile__age__in", 18, 20).Exclude("profile__lt", 1000)
  9. // WHERE profile.age IN (18, 20) AND NOT profile_id < 1000

Operators

The supported operators:

The operators that start with i ignore case.

exact

Default values of Filter, Exclude and Condition expr

  1. qs.Filter("name", "slene") // WHERE name = 'slene'
  2. qs.Filter("name__exact", "slene") // WHERE name = 'slene'
  3. // using = , case sensitive or not is depending on which collation database table is used
  4. qs.Filter("profile", nil) // WHERE profile_id IS NULL

iexact

  1. qs.Filter("name__iexact", "slene")
  2. // WHERE name LIKE 'slene'
  3. // Case insensitive, will match any name that equals to 'slene'

contains

  1. qs.Filter("name__contains", "slene")
  2. // WHERE name LIKE BINARY '%slene%'
  3. // Case sensitive, only match name that contains 'slene'

icontains

  1. qs.Filter("name__icontains", "slene")
  2. // WHERE name LIKE '%slene%'
  3. // Case insensitive, will match any name that contains 'slene'

in

  1. qs.Filter("profile__age__in", 17, 18, 19, 20)
  2. // WHERE profile.age IN (17, 18, 19, 20)

gt / gte

  1. qs.Filter("profile__age__gt", 17)
  2. // WHERE profile.age > 17
  3. qs.Filter("profile__age__gte", 18)
  4. // WHERE profile.age >= 18

lt / lte

  1. qs.Filter("profile__age__lt", 17)
  2. // WHERE profile.age < 17
  3. qs.Filter("profile__age__lte", 18)
  4. // WHERE profile.age <= 18

startswith

  1. qs.Filter("name__startswith", "slene")
  2. // WHERE name LIKE BINARY 'slene%'
  3. // Case sensitive, only match name that starts with 'slene'

istartswith

  1. qs.Filter("name__istartswith", "slene")
  2. // WHERE name LIKE 'slene%'
  3. // Case insensitive, will match any name that starts with 'slene'

endswith

  1. qs.Filter("name__endswith", "slene")
  2. // WHERE name LIKE BINARY '%slene'
  3. // Case sensitive, only match name that ends with 'slene'

iendswith

  1. qs.Filter("name__iendswith", "slene")
  2. // WHERE name LIKE '%slene'
  3. // Case insensitive, will match any name that ends with 'slene'

isnull

  1. qs.Filter("profile__isnull", true)
  2. qs.Filter("profile_id__isnull", true)
  3. // WHERE profile_id IS NULL
  4. qs.Filter("profile__isnull", false)
  5. // WHERE profile_id IS NOT NULL

Advanced Query API

QuerySeter is the API of advanced queries. Here are its methods:

  1. type QuerySeter interface {
  2. // add condition expression to QuerySeter.
  3. // for example:
  4. // filter by UserName == 'slene'
  5. // qs.Filter("UserName", "slene")
  6. // sql : left outer join profile on t0.id1==t1.id2 where t1.age == 28
  7. // Filter("profile__Age", 28)
  8. // // time compare
  9. // qs.Filter("created", time.Now())
  10. Filter(string, ...interface{}) QuerySeter
  11. // add raw sql to querySeter.
  12. // for example:
  13. // qs.FilterRaw("user_id IN (SELECT id FROM profile WHERE age>=18)")
  14. // //sql-> WHERE user_id IN (SELECT id FROM profile WHERE age>=18)
  15. FilterRaw(string, string) QuerySeter
  16. // add NOT condition to querySeter.
  17. // have the same usage as Filter
  18. Exclude(string, ...interface{}) QuerySeter
  19. // set condition to QuerySeter.
  20. // sql's where condition
  21. // cond := orm.NewCondition()
  22. // cond1 := cond.And("profile__isnull", false).AndNot("status__in", 1).Or("profile__age__gt", 2000)
  23. // //sql-> WHERE T0.`profile_id` IS NOT NULL AND NOT T0.`Status` IN (?) OR T1.`age` > 2000
  24. // num, err := qs.SetCond(cond1).Count()
  25. SetCond(*Condition) QuerySeter
  26. // get condition from QuerySeter.
  27. // sql's where condition
  28. // cond := orm.NewCondition()
  29. // cond = cond.And("profile__isnull", false).AndNot("status__in", 1)
  30. // qs = qs.SetCond(cond)
  31. // cond = qs.GetCond()
  32. // cond := cond.Or("profile__age__gt", 2000)
  33. // //sql-> WHERE T0.`profile_id` IS NOT NULL AND NOT T0.`Status` IN (?) OR T1.`age` > 2000
  34. // num, err := qs.SetCond(cond).Count()
  35. GetCond() *Condition
  36. // add LIMIT value.
  37. // args[0] means offset, e.g. LIMIT num,offset.
  38. // if Limit <= 0 then Limit will be set to default limit ,eg 1000
  39. // if QuerySeter doesn't call Limit, the sql's Limit will be set to default limit, eg 1000
  40. // for example:
  41. // qs.Limit(10, 2)
  42. // // sql-> limit 10 offset 2
  43. Limit(limit interface{}, args ...interface{}) QuerySeter
  44. // add OFFSET value
  45. // same as Limit function's args[0]
  46. Offset(offset interface{}) QuerySeter
  47. // add GROUP BY expression
  48. // for example:
  49. // qs.GroupBy("id")
  50. GroupBy(exprs ...string) QuerySeter
  51. // add ORDER expression.
  52. // "column" means ASC, "-column" means DESC.
  53. // for example:
  54. // qs.OrderBy("-status")
  55. OrderBy(exprs ...string) QuerySeter
  56. // add FORCE INDEX expression.
  57. // for example:
  58. // qs.ForceIndex(`idx_name1`,`idx_name2`)
  59. // ForceIndex, UseIndex , IgnoreIndex are mutually exclusive
  60. ForceIndex(indexes ...string) QuerySeter
  61. // add USE INDEX expression.
  62. // for example:
  63. // qs.UseIndex(`idx_name1`,`idx_name2`)
  64. // ForceIndex, UseIndex , IgnoreIndex are mutually exclusive
  65. UseIndex(indexes ...string) QuerySeter
  66. // add IGNORE INDEX expression.
  67. // for example:
  68. // qs.IgnoreIndex(`idx_name1`,`idx_name2`)
  69. // ForceIndex, UseIndex , IgnoreIndex are mutually exclusive
  70. IgnoreIndex(indexes ...string) QuerySeter
  71. // set relation model to query together.
  72. // it will query relation models and assign to parent model.
  73. // for example:
  74. // // will load all related fields use left join .
  75. // qs.RelatedSel().One(&user)
  76. // // will load related field only profile
  77. // qs.RelatedSel("profile").One(&user)
  78. // user.Profile.Age = 32
  79. RelatedSel(params ...interface{}) QuerySeter
  80. // Set Distinct
  81. // for example:
  82. // o.QueryTable("policy").Filter("Groups__Group__Users__User", user).
  83. // Distinct().
  84. // All(&permissions)
  85. Distinct() QuerySeter
  86. // set FOR UPDATE to query.
  87. // for example:
  88. // o.QueryTable("user").Filter("uid", uid).ForUpdate().All(&users)
  89. ForUpdate() QuerySeter
  90. // return QuerySeter execution result number
  91. // for example:
  92. // num, err = qs.Filter("profile__age__gt", 28).Count()
  93. Count() (int64, error)
  94. // check result empty or not after QuerySeter executed
  95. // the same as QuerySeter.Count > 0
  96. Exist() bool
  97. // execute update with parameters
  98. // for example:
  99. // num, err = qs.Filter("user_name", "slene").Update(Params{
  100. // "Nums": ColValue(Col_Minus, 50),
  101. // }) // user slene's Nums will minus 50
  102. // num, err = qs.Filter("UserName", "slene").Update(Params{
  103. // "user_name": "slene2"
  104. // }) // user slene's name will change to slene2
  105. Update(values Params) (int64, error)
  106. // delete from table
  107. // for example:
  108. // num ,err = qs.Filter("user_name__in", "testing1", "testing2").Delete()
  109. // //delete two user who's name is testing1 or testing2
  110. Delete() (int64, error)
  111. // return a insert queryer.
  112. // it can be used in times.
  113. // example:
  114. // i,err := sq.PrepareInsert()
  115. // num, err = i.Insert(&user1) // user table will add one record user1 at once
  116. // num, err = i.Insert(&user2) // user table will add one record user2 at once
  117. // err = i.Close() //don't forget call Close
  118. PrepareInsert() (Inserter, error)
  119. // query all data and map to containers.
  120. // cols means the columns when querying.
  121. // for example:
  122. // var users []*User
  123. // qs.All(&users) // users[0],users[1],users[2] ...
  124. All(container interface{}, cols ...string) (int64, error)
  125. // query one row data and map to containers.
  126. // cols means the columns when querying.
  127. // for example:
  128. // var user User
  129. // qs.One(&user) //user.UserName == "slene"
  130. One(container interface{}, cols ...string) error
  131. // query all data and map to []map[string]interface.
  132. // expres means condition expression.
  133. // it converts data to []map[column]value.
  134. // for example:
  135. // var maps []Params
  136. // qs.Values(&maps) //maps[0]["UserName"]=="slene"
  137. Values(results *[]Params, exprs ...string) (int64, error)
  138. // query all data and map to [][]interface
  139. // it converts data to [][column_index]value
  140. // for example:
  141. // var list []ParamsList
  142. // qs.ValuesList(&list) // list[0][1] == "slene"
  143. ValuesList(results *[]ParamsList, exprs ...string) (int64, error)
  144. // query all data and map to []interface.
  145. // it's designed for one column record set, auto change to []value, not [][column]value.
  146. // for example:
  147. // var list ParamsList
  148. // qs.ValuesFlat(&list, "UserName") // list[0] == "slene"
  149. ValuesFlat(result *ParamsList, expr string) (int64, error)
  150. // query all rows into map[string]interface with specify key and value column name.
  151. // keyCol = "name", valueCol = "value"
  152. // table data
  153. // name | value
  154. // total | 100
  155. // found | 200
  156. // to map[string]interface{}{
  157. // "total": 100,
  158. // "found": 200,
  159. // }
  160. RowsToMap(result *Params, keyCol, valueCol string) (int64, error)
  161. // query all rows into struct with specify key and value column name.
  162. // keyCol = "name", valueCol = "value"
  163. // table data
  164. // name | value
  165. // total | 100
  166. // found | 200
  167. // to struct {
  168. // Total int
  169. // Found int
  170. // }
  171. RowsToStruct(ptrStruct interface{}, keyCol, valueCol string) (int64, error)
  172. }
  • Every API call that returns QuerySeter will give you a new QuerySeter object. It won’t affect the previous object.

  • Advanced queries use Filter and Exclude to do conditional queries. There are two filter rules - contain and exclude

Filter

Used to filter the result for the include conditions.

Use AND to connect multiple filters:

  1. qs.Filter("profile__isnull", true).Filter("name", "slene")
  2. // WHERE profile_id IS NULL AND name = 'slene'

Exclude

Used to filter the result for the exclude conditions.

Use NOT to exclude condition Use AND to connect multiple filters:

  1. qs.Exclude("profile__isnull", true).Filter("name", "slene")
  2. // WHERE NOT profile_id IS NULL AND name = 'slene'

SetCond

Custom conditions:

  1. cond := NewCondition()
  2. cond1 := cond.And("profile__isnull", false).AndNot("status__in", 1).Or("profile__age__gt", 2000)
  3. qs := orm.QueryTable("user")
  4. qs = qs.SetCond(cond1)
  5. // WHERE ... AND ... AND NOT ... OR ...
  6. cond2 := cond.AndCond(cond1).OrCond(cond.And("name", "slene"))
  7. qs = qs.SetCond(cond2).Count()
  8. // WHERE (... AND ... AND NOT ... OR ...) OR ( ... )

Limit

Limit maximum returned lines. The second param can set Offset

  1. var DefaultRowsLimit = 1000 // The default limit of ORM is 1000
  2. // LIMIT 1000
  3. qs.Limit(10)
  4. // LIMIT 10
  5. qs.Limit(10, 20)
  6. // LIMIT 10 OFFSET 20
  7. qs.Limit(-1)
  8. // no limit
  9. qs.Limit(-1, 100)
  10. // LIMIT 18446744073709551615 OFFSET 100
  11. // 18446744073709551615 is 1<<64 - 1. Used to set the condition which is no limit but with offset

Offset

Set offset lines:

  1. qs.Offset(20)
  2. // LIMIT 1000 OFFSET 20

GroupBy

  1. qs.GroupBy("id", "age")
  2. // GROUP BY id,age

OrderBy

Param uses expr

Using - at the beginning of expr stands for order by DESC

  1. qs.OrderBy("id", "-profile__age")
  2. // ORDER BY id ASC, profile.age DESC
  3. qs.OrderBy("-profile__age", "profile")
  4. // ORDER BY profile.age DESC, profile_id ASC

ForceIndex

Forcing DB to use the index.

You need to check your DB whether it support this feature.

qs.ForceIndex(`idx_name1`,`idx_name2`)

UseIndex

Suggest DB to user the index.

You need to check your DB whether it support this feature.

qs.UseIndex(`idx_name1`,`idx_name2`)

IgnoreIndex

Make DB ignore the index

You need to check your DB whether it support this feature.

qs.IgnoreIndex(`idx_name1`,`idx_name2`)

Distinct

Same as distinct statement in sql, return only distinct (different) values

qs.Distinct()
// SELECT DISTINCT

RelatedSel

Relational queries. Param uses expr

var DefaultRelsDepth = 5 // RelatedSel will query for maximum 5 level by default

qs := o.QueryTable("post")

qs.RelatedSel()
// INNER JOIN user ... LEFT OUTER JOIN profile ...

qs.RelatedSel("user")
// INNER JOIN user ... 
// Only query the fields set by expr

// For fields with null attribute will use LEFT OUTER JOIN

Count

Return line count based on the current query

cnt, err := o.QueryTable("user").Count() // SELECT COUNT(*) FROM USER
fmt.Printf("Count Num: %s, %s", cnt, err)

Exist

exist := o.QueryTable("user").Filter("UserName", "Name").Exist()
fmt.Printf("Is Exist: %s", exist)

Update

Execute batch updating based on the current query

num, err := o.QueryTable("user").Filter("name", "slene").Update(orm.Params{
    "name": "astaxie",
})
fmt.Printf("Affected Num: %s, %s", num, err)
// SET name = "astaixe" WHERE name = "slene"

Atom operation add field:

// Assume there is a nums int field in user struct
num, err := o.QueryTable("user").Update(orm.Params{
    "nums": orm.ColValue(orm.Col_Add, 100),
})
// SET nums = nums + 100

orm.ColValue supports:

Col_Add      // plus
Col_Minus    // minus 
Col_Multiply // multiply 
Col_Except   // divide

Delete

Execute batch deletion based on the current query

num, err := o.QueryTable("user").Filter("name", "slene").Delete()
fmt.Printf("Affected Num: %s, %s", num, err)
// DELETE FROM user WHERE name = "slene"

PrepareInsert

Use a prepared statement to increase inserting speed with multiple inserts.

var users []*User
...
qs := o.QueryTable("user")
i, _ := qs.PrepareInsert()
for _, user := range users {
    id, err := i.Insert(user)
    if err != nil {
        ...
    }
}
// PREPARE INSERT INTO user (`name`, ...) VALUES (?, ...)
// EXECUTE INSERT INTO user (`name`, ...) VALUES ("slene", ...)
// EXECUTE ...
// ...
i.Close() // Don't forget to close the statement

All

Return the related ResultSet

Param of All supports []Type and []*Type

var users []*User
num, err := o.QueryTable("user").Filter("name", "slene").All(&users)
fmt.Printf("Returned Rows Num: %s, %s", num, err)

All / Values / ValuesList / ValuesFlat will be limited by Limit. 1000 lines by default.

The returned fields can be specified:

type Post struct {
    Id      int
    Title   string
    Content string
    Status  int
}

// Only return Id and Title
var posts []Post
o.QueryTable("post").Filter("Status", 1).All(&posts, "Id", "Title")

The other fields of the object are set to the default value of the field’s type.

One

Try to return one record

var user User
err := o.QueryTable("user").Filter("name", "slene").One(&user)
if err == orm.ErrMultiRows {
    // Have multiple records
    fmt.Printf("Returned Multi Rows Not One")
}
if err == orm.ErrNoRows {
    // No result 
    fmt.Printf("Not row found")
}

The returned fields can be specified:

// Only return Id and Title
var post Post
o.QueryTable("post").Filter("Content__istartswith", "prefix string").One(&post, "Id", "Title")

The other fields of the object are set to the default value of the fields’ type.

Values

Return key => value of result set

key is Field name in Model. value type if string.

var maps []orm.Params
num, err := o.QueryTable("user").Values(&maps)
if err == nil {
    fmt.Printf("Result Nums: %d\n", num)
    for _, m := range maps {
        fmt.Println(m["Id"], m["Name"])
    }
}

Return specific fields:

TODO: doesn’t support recursive query. RelatedSel return Values directly

But it can specify the value needed by expr.

var maps []orm.Params
num, err := o.QueryTable("user").Values(&maps, "id", "name", "profile", "profile__age")
if err == nil {
    fmt.Printf("Result Nums: %d\n", num)
    for _, m := range maps {
        fmt.Println(m["Id"], m["Name"], m["Profile"], m["Profile__Age"])
    // There is no complicated nesting data in the map
    }
}

ValuesList

The result set will be stored as a slice

The order of the result is same as the Fields order in the Model definition.

The values are saved as strings.

var lists []orm.ParamsList
num, err := o.QueryTable("user").ValuesList(&lists)
if err == nil {
    fmt.Printf("Result Nums: %d\n", num)
    for _, row := range lists {
        fmt.Println(row)
    }
}

It can return specific fields by setting expr.

var lists []orm.ParamsList
num, err := o.QueryTable("user").ValuesList(&lists, "name", "profile__age")
if err == nil {
    fmt.Printf("Result Nums: %d\n", num)
    for _, row := range lists {
        fmt.Printf("Name: %s, Age: %s\m", row[0], row[1])
    }
}

ValuesFlat

Only returns a single values slice of a specific field.

var list orm.ParamsList
num, err := o.QueryTable("user").ValuesFlat(&list, "name")
if err == nil {
    fmt.Printf("Result Nums: %d\n", num)
    fmt.Printf("All User Names: %s", strings.Join(list, ", "))
}

Relational Query

Let’s see how to do a Relational Query by looking at Model Definition

User and Profile is OnToOne relation

Query Profile by known User object:

user := &User{Id: 1}
o.Read(user)
if user.Profile != nil {
    o.Read(user.Profile)
}

Cascaded query directly:

user := &User{}
o.QueryTable("user").Filter("Id", 1).RelatedSel().One(user)
// Get Profile automatically
fmt.Println(user.Profile)
// Because In Profile we defined reverse relation User, Profile's User is also auto assigned. Can directly use:
fmt.Println(user.Profile.User)

Reverse finding Profile by User:

var profile Profile
err := o.QueryTable("profile").Filter("User__Id", 1).One(&profile)
if err == nil {
    fmt.Println(profile)
}

Post and User are ManyToOne relation. i.e.: ForeignKey is User

type Post struct {
    Id    int
    Title string
    User  *User  `orm:"rel(fk)"`
    Tags  []*Tag `orm:"rel(m2m)"`
}
var posts []*Post
num, err := o.QueryTable("post").Filter("User", 1).RelatedSel().All(&posts)
if err == nil {
    fmt.Printf("%d posts read\n", num)
    for _, post := range posts {
        fmt.Printf("Id: %d, UserName: %d, Title: %s\n", post.Id, post.User.UserName, post.Title)
    }
}

Query related User by Post.Title:

While RegisterModel, ORM will create reverse relation for Post in User. So it can query directly:

var user User
err := o.QueryTable("user").Filter("Post__Title", "The Title").Limit(1).One(&user)
if err == nil {
    fmt.Printf(user)
}

Post and Tag are ManyToMany relation

After setting rel(m2m), ORM will create connecting table automatically.

type Post struct {
    Id    int
    Title string
    User  *User  `orm:"rel(fk)"`
    Tags  []*Tag `orm:"rel(m2m)"`
}
type Tag struct {
    Id    int
    Name  string
    Posts []*Post `orm:"reverse(many)"`
}

Query which post used the tag with tag name:

var posts []*Post
num, err := dORM.QueryTable("post").Filter("Tags__Tag__Name", "golang").All(&posts)

Query how many tags does the post have with post title:

var tags []*Tag
num, err := dORM.QueryTable("tag").Filter("Posts__Post__Title", "Introduce Beego ORM").All(&tags)

LoadRelated is used to load relation field of model. Including all rel/reverse - one/many relation.

Load ManyToMany relation field

// load related Tags
post := Post{Id: 1}
err := o.Read(&post)
num, err := o.LoadRelated(&post, "Tags")
// Load related Posts
tag := Tag{Id: 1}
err := o.Read(&tag)
num, err := o.LoadRelated(&tag, "Posts")

User is the ForeignKey of Post. Load related ReverseMany

type User struct {
    Id    int
    Name  string
    Posts []*Post `orm:"reverse(many)"`
}

user := User{Id: 1}
err := dORM.Read(&user)
num, err := dORM.LoadRelated(&user, "Posts")
for _, post := range user.Posts {
    //...
}

Handling ManyToMany relation

// QueryM2Mer model to model query struct
// all operations are on the m2m table only, will not affect the origin model table
type QueryM2Mer interface {
    // add models to origin models when creating queryM2M.
    // example:
    //     m2m := orm.QueryM2M(post,"Tag")
    //     m2m.Add(&Tag1{},&Tag2{})
    //      for _,tag := range post.Tags{}{ ... }
    // param could also be any of the follow
    //     []*Tag{{Id:3,Name: "TestTag1"}, {Id:4,Name: "TestTag2"}}
    //    &Tag{Id:5,Name: "TestTag3"}
    //    []interface{}{&Tag{Id:6,Name: "TestTag4"}}
    // insert one or more rows to m2m table
    // make sure the relation is defined in post model struct tag.
    Add(...interface{}) (int64, error)
    // remove models following the origin model relationship
    // only delete rows from m2m table
    // for example:
    // tag3 := &Tag{Id:5,Name: "TestTag3"}
    // num, err = m2m.Remove(tag3)
    Remove(...interface{}) (int64, error)
    // check model is existed in relationship of origin model
    Exist(interface{}) bool
    // clean all models in related of origin model
    Clear() (int64, error)
    // count all related models of origin model
    Count() (int64, error)
}

Create a QueryM2Mer object

o := orm.NewOrm()
post := Post{Id: 1}
m2m := o.QueryM2M(&post, "Tags")
// In the first param object must have primary key
// The second param is the M2M field will work with
// API of QueryM2Mer will used to Post with id equals 1

QueryM2Mer Add

tag := &Tag{Name: "golang"}
o.Insert(tag)

num, err := m2m.Add(tag)
if err == nil {
    fmt.Println("Added nums: ", num)
}

Add supports many types: Tag Tag []Tag []Tag []interface{}

var tags []*Tag
...
// After reading tags
...
num, err := m2m.Add(tags)
if err == nil {
    fmt.Println("Added nums: ", num)
}
// It can pass multiple params
// m2m.Add(tag1, tag2, tag3)

QueryM2Mer Remove

Remove tag from M2M relation:

Remove supports many types: Tag Tag []Tag []Tag []interface{}

var tags []*Tag
...
// After reading tags
...
num, err := m2m.Remove(tags)
if err == nil {
    fmt.Println("Removed nums: ", num)
}
// It can pass multiple params
// m2m.Remove(tag1, tag2, tag3)

QueryM2Mer Exist

Test if Tag is in M2M relation

if m2m.Exist(&Tag{Id: 2}) {
    fmt.Println("Tag Exist")
}

QueryM2Mer Clear

Clear all M2M relation

nums, err := m2m.Clear()
if err == nil {
    fmt.Println("Removed Tag Nums: ", nums)
}

QueryM2Mer Count

Count the number of Tags

nums, err := m2m.Count()
if err == nil {
    fmt.Println("Total Nums: ", nums)
}