乐趣区

关于golang:Go-ORM-干啥的

​胖 sir:接着,给你一个馅饼儿

兵长:来嘞!!
一篇来自 ORM 的整顿笔记 …

1 什么是 ORM?为什么要⽤ ORM?

什么是 ORM,即 Object-Relationl Mapping,它的作⽤是在关系型数据库和对象之间作⼀个映射,

这样,咱们在具体的 操作数据库的时候,就不须要再去和简单的 SQL 语句打交道,只有像平时操作对象⼀样操作它就能够了。

ORM 解决的次要问题是 对象关系的映射。域模型和关系模型别离是建⽴在概念模型的根底上的。

  • 域模型是⾯向对 象的
  • 关系模型是⾯向关系的

⼀般状况下,⼀个长久化类和⼀个表对应,类的每个实例对应表中的⼀条记录,

类的每个属性对应表的每个字段。

ORM 技术特点:

  • 提⾼了开发效率。

    因为 ORM 能够⾃动对 Entity 对象与数据库中的 Table 进⾏字段与属性的映射,所以咱们理论 可能曾经不须要⼀个专⽤的、庞⼤的数据拜访层。

  • ORM 提供了对数据库的映射,不⽤ sql 间接编码,可能像操作对象⼀样从数据库获取数据。

ORM 的毛病

ORM 的毛病是会就义程序的执⾏效率和会固定思维模式。

从系统结构上来看, 采⽤ ORM 的零碎⼀般都是多层零碎,零碎的档次多了,效率就会升高。ORM 是⼀种齐全的 ⾯向对象的做法,⽽⾯向对象的做法也会对性能产⽣⼀定的影响。

2 ORM 操作数据

package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// UserInfo 用户信息
//orm 会默认依据构造体创立 table,orm 采纳的是 linux 命名形式  即小写加下划线,且会在名字前面加 s
// 会创立 user_infos 表
type UserInfo struct {
    gorm.Model
    ID     uint
    Name   string
    Gender string
    Hobby  string
}

//  truncate table 表名
//  数据库须要提前创立好  例如 mygorm
//  parseTime 是查问后果是否⾃动解析为工夫
//  loc 是 MySQL 的时区设置
//  charset 是编码方式
func main() {fmt.Println("try open mysql connection....")
    db, err := gorm.Open("mysql", "root:123456@(localhost:3306)/mygorm?charset=utf8mb4&parseTime=True&loc=Local")
    if err != nil {panic(err)
    }
    fmt.Println("successful")
    defer db.Close()
    // 主动迁徙
    // 若该表不存在则创立该表,若该表存在且构造体发生变化则更新表构造
    db.AutoMigrate(&UserInfo{})
    u1 := UserInfo{gorm.Model{}, 1, "xiaozhu", "man", "playing"}
    // 创立记录
    result := db.Create(&u1)
    fmt.Println("result:", result.RowsAffected)
    // 查问
    var u = new(UserInfo)
    // 查问一条记录
    db.First(u)
    fmt.Printf("First: %#v\n", u)
    // 依照条件查问
    var uu UserInfo
    db.Find(&uu, "name=?", "xiaozhu")
    fmt.Printf("Find: %#v\n", uu)
    // 更新
    db.Model(&uu).Update("hobby", "sing")
    // 删除,此处删除记录,是不会将数据表中的数据删除掉,而是 deleted_at 会更新删除工夫
    db.Delete(&uu)
}
  • 应用 gorm 必须要先创立好数据库
  • gorm 会主动创立数据表,且表构造能够动态变化
  • gorm 创立的表命名形式为 代码中构造体命名的转换,例如 构造体命名为 UserInfo,则 table 会命名为 user_infos
  • gorm 批改表构造十分的容易
  • gorm 是齐全面向对象的思维

3 模型定义

模型是规范的 struct,由 Go 的根本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成

例如:

type User struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}

自定义模型

遵循 GORM 已有的约定,能够缩小您的配置和代码量。

type User struct {
   gorm.Model   // 内嵌
   Name         string
   Age          sql.NullInt64
   Birthday     *time.Time
   Email        string  `gorm:"type:varchar(100);uniqueIndex"`
   Role         string  `gorm:"size:255"`        // 设置字段大小为 255
   MemberNumber *string `gorm:"unique;not null"` // 设置会员号(member number)惟一并且不为空
   Num          int     `gorm:"AUTO_INCREMENT"`  // 设置 num 为自增类型
   Address      string  `gorm:"index:addr"`      // 给 address 字段创立名为 addr 的索引
   IgnoreMe     int     `gorm:"-"`               // 疏忽本字段
}

字段标签

申明 model 时,tag 是可选的,GORM 反对以下 tag:tag 名大小写不敏感,但倡议应用 camelCase 格调

标签名 阐明
column 指定 db 列名
type 列数据类型,举荐应用兼容性好的通用类型,例如:所有数据库都反对 bool、int、uint、float、string、time、bytes 并且能够和其余标签一起应用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是反对的。在应用指定数据库数据类型时,它须要是残缺的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INSTREMENT
size 指定列大小,例如:size:256
primaryKey 指定列为主键
unique 指定列为惟一
default 指定列的默认值
precision 指定列的精度
scale 指定列大小
not null 指定列为 NOT NULL
autoIncrement 指定列为主动增长
autoIncrementIncrement 主动步长,管制间断记录之间的距离
embedded 嵌套字段
embeddedPrefix 嵌入字段的列名前缀
autoCreateTime 创立时追踪以后工夫,对于 int 字段,它会追踪秒级工夫戳,您能够应用 nano/milli 来追踪纳秒、毫秒工夫戳,例如:autoCreateTime:nano
autoUpdateTime 创立 / 更新时追踪以后工夫,对于 int 字段,它会追踪秒级工夫戳,您能够应用 nano/milli 来追踪纳秒、毫秒工夫戳,例如:autoUpdateTime:milli
index 依据参数创立索引,多个字段应用雷同的名称则创立复合索引,查看 索引 获取详情
uniqueIndex index 雷同,但创立的是惟一索引
check 创立查看束缚,例如 check:age > 13,查看 束缚 获取详情
<- 设置字段写入的权限,<-:create 只创立、<-:update 只更新、<-:false 无写入权限、<- 创立和更新权限
-> 设置字段读的权限,->:false 无读权限
疏忽该字段,- 无读写权限
comment 迁徙时为字段增加正文

关联标签

标签 形容
foreignKey 指定以后模型的列作为连贯表的外键
references 指定援用表的列名,其将被映射为连贯表外键
polymorphic 指定多态类型,比方模型名
polymorphicValue 指定多态值、默认表名
many2many 指定连贯表表名
joinForeignKey 指定连贯表的外键列名,其将被映射到以后表
joinReferences 指定连贯表的外键列名,其将被映射到援用表
constraint 关系束缚,例如:OnUpdateOnDelete

4 主键、表名、列名的约定

主键(Primary Key)

GORM 默认会使⽤名为 ID 的字段作为表的主键。

 type User struct { 

 ID string // 名为 `ID` 的字段会默认作为表的主键 

 Name string 

 }
// 使⽤ `AnimalID` 作为主键 

type Animal struct { 

AnimalID int64 `gorm:"primary_key"` 

Name string 

Age int64 

} 

表名(Table Name)

表名默认就是构造体名称的复数,例如:

type User struct {} // 默认表名是 `users` 

// 将 User 的表名设置为 `profiles` 

func (User) TableName() string {return "profiles"} 

func (u User) TableName() string { 

    if u.Role == "admin" {return "admin_users"} else {return "users"} 

} 

// 禁⽤默认表名的复数模式,如果置为 true,则 `User` 的默认表名是 `user` 

db.SingularTable(true) 

也能够通过 Table() 指定表名:

// 使⽤ User 构造体创立名为 `deleted_users` 的表 

db.Table("deleted_users").CreateTable(&User{}) 

var deleted_users []User 

db.Table("deleted_users").Find(&deleted_users) 

// SELECT * FROM deleted_users; 

db.Table("deleted_users").Where("name = ?", "jinzhu").Delete() 

// DELETE FROM deleted_users WHERE name = 'jinzhu'; 

GORM 还⽀持更改默认表名称规定:

gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string {return "prefix_" + defaultTableName;} 

列名(Column Name)

列名由字段名称进⾏下划线宰割来⽣成

type User struct { 

ID uint // column name is `id` 

Name string // column name is `name` 

Birthday time.Time // column name is `birthday` 

CreatedAt time.Time // column name is `created_at` 

} 

能够使⽤构造体 tag 指定列名:

type Animal struct { 

AnimalId int64 `gorm:"column:beast_id"` // set column name to `beast_id` 

Birthday time.Time `gorm:"column:day_of_the_beast"` // set co lumn name to `day_of_the_beast` 

Age int64 `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast` 

} 

工夫戳跟踪

CreatedAt

如果模型有 CreatedAt 字段,该字段的值将会是首次创立记录的工夫。

db.Create(&user) // `CreatedAt` 将会是以后工夫 

// 能够使⽤ `Update` ⽅法来扭转 `CreateAt` 的值 

db.Model(&user).Update("CreatedAt", time.Now()) 

UpdatedAt

如果模型有 UpdatedAt 字段,该字段的值将会是每次更新记录的工夫。

db.Save(&user) // `UpdatedAt` 将会是以后工夫 

db.Model(&user).Update("name", "jinzhu") // `UpdatedAt` 将会是以后工夫 

DeletedAt

如果模型有 DeletedAt 字段,调⽤ Delete 删除该记录时,将会设置 DeletedAt 字段为以后工夫,⽽

不是间接将记录从数据库中删除。

5 CURD

创立记录

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // 通过数据的指针来创立

user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

批量插入

要无效地插入大量记录,请将一个 slice 传递给 Create 办法。将切片数据传递给 Create 办法,GORM 将生成一个繁多的 SQL 语句来插入所有数据,并回填主键的值,钩子办法也会被调用。

var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)

for _, user := range users {user.ID // 1,2,3}

应用 CreateInBatches 创立时,你还能够指定创立的数量,例如:

var users = []User{{name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}

// 数量为 100
db.CreateInBatches(users, 100)

默认值

您能够通过标签 default 为字段定义默认值,如:

type User struct {
  ID   int64
  Name string `gorm:"default:galeone"`
  Age  int64  `gorm:"default:18"`
}

插入记录到数据库时,默认值 会被用于 填充值为 零值 的字段

查问

检索单个对象

GORM 提供了 FirstTakeLast 办法,以便从数据库中检索单个对象。当查询数据库时它增加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 谬误

// 获取第一条记录(主键升序)db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 获取最初一条记录(主键降序)db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error

// 查看 ErrRecordNotFound 谬误
errors.Is(result.Error, gorm.ErrRecordNotFound)

如果你想防止 ErrRecordNotFound 谬误,你能够应用 Find,比方db.Limit(1).Find(&user)Find 办法能够承受 struct 和 slice 的数据。

First, Last办法将按主键排序查找第一 / 最初一条记录,只有在用 struct 查问或提供 model value 时才无效,如果以后 model 没有定义主键,将按第一个字段排序,例如:

var user User
var users []User

// 能够
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// 能够
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// 不行
result := map[string]interface{}{}
db.Table("users").First(&result)

// 但能够配合 Take 应用
result := map[string]interface{}{}
db.Table("users").Take(&result)

// 依据第一个字段排序
type Language struct {
  Code string
  Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1

用主键检索

如果主键是数值类型,也能够通过 内联条件 传入主键来检索对象。如果主键是 string 类型,要小心防止 SQL 注入,查看 平安 获取详情

db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

如果主键是像 uuid 这样的字符串,您须要这要写:

db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";

检索全副对象

// 获取全副记录
result := db.Find(&users)
// SELECT * FROM users;

result.RowsAffected // 返回找到的记录数,相当于 `len(users)`
result.Error        // returns error

条件

String 条件
// 获取第一条匹配的记录
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// 获取全副匹配的记录
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

Struct & Map 条件

// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// 主键切片条件
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

留神 当应用构造作为条件查问时,GORM 只会查问非零值字段。这意味着如果您的字段值为 0''false 或其余 零值,该字段不会被用于构建查问条件,例如:

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";

你能够应用 map 来构建查问条件,它会应用所有的值,例如:

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

或查看 指定构造体查问字段 获取详情

指定构造体查问字段

当应用构造体进行查问时,你能够应用它的字段名或其 dbname 列名作为参数来指定查问的字段,例如:

db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

db.Where(&User{Name: "jinzhu"}, "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;

内联条件

用法与 Where 相似

// SELECT * FROM users WHERE id = 23;
// 依据主键获取记录,如果是非整型主键
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';

// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;

// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;

Not 条件

构建 NOT 条件,用法与 Where 相似

db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;

// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");

// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;

// 不在主键切片中的记录
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;

Or 条件

db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name ='jinzhu'").Or(User{Name:"jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

// Map
db.Where("name ='jinzhu'").Or(map[string]interface{}{"name":"jinzhu 2","age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

您还能够查看高级查问中的 分组条件,它被用于编写简单 SQL

抉择特定字段

抉择您想从数据库中检索的字段,默认状况下会抉择全副字段

db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;

db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;

db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age,'42') FROM users;

还能够看一看 智能抉择字段

Order

指定从数据库检索记录时的排序形式

db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// 多个 order
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

db.Clauses(clause.OrderBy{Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)

Limit & Offset

Limit 指定获取记录的最大数量 Offset 指定在开始返回记录之前要跳过的记录数量

db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// 通过 -1 打消 Limit 条件
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)

db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;

db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;

// 通过 -1 打消 Offset 条件
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)

查看 Pagination 学习如何写一个分页器

Group & Having

type result struct {
  Date  time.Time
  Total int
}

db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name`


db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
for rows.Next() {...}

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
for rows.Next() {...}

type Result struct {
  Date  time.Time
  Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)

Distinct

从模型中抉择不雷同的值

db.Distinct("name", "age").Order("name, age desc").Find(&results)

Distinct 也能够配合 Pluck, Count 应用

Joins

指定 Joins 条件

type result struct {
  Name  string
  Email string
}
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {...}

db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)

// 带参数的多表连贯
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)

Joins 预加载

您能够应用 Joins 实现单条 SQL 预加载关联记录,例如:

db.Joins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;

Scan

Scan 后果至 struct,用法与 Find 相似

type Result struct {
  Name string
  Age  int
}

var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)

// 原生 SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)

处理错误

GORM 的错误处理与常见的 Go 代码不同,因为 GORM 提供的是链式 API。

如果遇到任何谬误,GORM 会设置 *gorm.DBError 字段,您须要像这样查看它:

if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {// 处理错误...}

或者

if result := db.Where("name = ?", "jinzhu").First(&user); result.Error != nil {// 处理错误...}

ErrRecordNotFound

FirstLastTake 办法找不到记录时,GORM 会返回 ErrRecordNotFound 谬误。如果产生了多个谬误,你能够通过 errors.Is 判断谬误是否为 ErrRecordNotFound,例如:

// 查看谬误是否为 RecordNotFound
err := db.First(&user, 100).Error
errors.Is(err, gorm.ErrRecordNotFound)

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

更多高级用法和细节 能够查看 中文 GROM 网站

作者:小魔童哪吒

退出移动版