1. 程式人生 > >Go ORM框架 - GORM 踩坑指南

Go ORM框架 - GORM 踩坑指南

今天聊聊目前業界使用比較多的 ORM 框架:GORM。GORM 相關的文件原作者已經寫得非常的詳細,具體可以看[這裡](https://gorm.io/zh_CN/docs/connecting_to_the_database.html),這一篇主要做一些 GORM 使用過程中關鍵功能的介紹,GORM 約定的一些配置資訊說明,防止大家在使用過程中踩坑。 以下示例程式碼都可以在 Github : [gorm-demo](https://github.com/rickiyang/gorm-demo) 中找到。 --------- GORM 官方支援的資料庫型別有: MySQL, PostgreSQL, SQlite, SQL Server。 連線 MySQL 的示例: ```go import ( "gorm.io/driver/mysql" "gorm.io/gorm" ) func main() { // 參考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 獲取詳情 dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) } ``` MySQl 驅動程式提供了一些高階配置可以在初始化過程中使用,例如: ```go db, err := gorm.Open(mysql.New(mysql.Config{ DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name DefaultStringSize: 256, // string 型別欄位的預設長度 DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的資料庫不支援 DontSupportRenameIndex: true, // 重新命名索引時採用刪除並新建的方式,MySQL 5.7 之前的資料庫和 MariaDB 不支援重新命名索引 DontSupportRenameColumn: true, // 用 `change` 重新命名列,MySQL 8 之前的資料庫和 MariaDB 不支援重新命名列 SkipInitializeWithVersion: false, // 根據當前 MySQL 版本自動配置 }), &gorm.Config{}) ``` 注意到 *gorm.Open(dialector Dialector, opts ...Option)* 函式的第二個引數是接收一個 *gorm.Config{}* 型別的引數,這裡就是 gorm 在資料庫建立連線後框架本身做的一些預設配置,**請注意這裡如果沒有配置好,後面你的資料庫操作將會很痛苦!** GORM 提供的配置可以在初始化時使用: ```go type Config struct { SkipDefaultTransaction bool NamingStrategy schema.Namer Logger logger.Interface NowFunc func() time.Time DryRun bool PrepareStmt bool DisableNestedTransaction bool AllowGlobalUpdate bool DisableAutomaticPing bool DisableForeignKeyConstraintWhenMigrating bool } ``` 這些引數我們一個一個來說: **SkipDefaultTransaction** 跳過預設開啟事務模式。為了確保資料一致性,GORM 會在事務裡執行寫入操作(建立、更新、刪除)。如果沒有這方面的要求,可以在初始化時禁用它。 ```go db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ SkipDefaultTransaction: true, }) ``` **NamingStrategy** 表名稱的命名策略,下面會說。GORM 允許使用者通過覆蓋預設的`NamingStrategy`來更改命名約定,這需要實現介面 `Namer`: ```go type Namer interface { TableName(table string) string ColumnName(table, column string) string JoinTableName(table string) string RelationshipFKName(Relationship) string CheckerName(table, column string) string IndexName(table, column string) string } ``` 預設 `NamingStrategy` 也提供了幾個選項,如: ```go db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ NamingStrategy: schema.NamingStrategy{ TablePrefix: "t_", // 表名字首,`User`表為`t_users` SingularTable: true, // 使用單數表名,啟用該選項後,`User` 表將是`user` NameReplacer: strings.NewReplacer("CID", "Cid"), // 在轉為資料庫名稱之前,使用NameReplacer更改結構/欄位名稱。 }, }) ``` **一般來說這裡是一定要配置 *SingularTable: true* 這一項的。** **Logger** 允許通過覆蓋此選項更改 GORM 的預設 logger。 **NowFunc** 更改建立時間使用的函式: ```go db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ NowFunc: func() time.Time { return time.Now().Local() }, }) ``` **DryRun** 生成 `SQL` 但不執行,可以用於準備或測試生成的 SQL: ```go db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ DryRun: false, }) ``` **PrepareStmt** `PreparedStmt` 在執行任何 SQL 時都會建立一個 prepared statement 並將其快取,以提高後續的效率: ```go db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ PrepareStmt: false, }) ``` #### GORM 約定配置 使用別人的框架就要受制別人的約束,在 GORM 中有很多的約定,如果你沒有遵循這些約定可能你認為正常的程式碼跑起來會發生意想不到的問題。 ##### 模型定義 預設情況下,GORM 會使用 `ID` 作為表的主鍵。 ```go type User struct { ID string // 預設情況下,名為 `ID` 的欄位會作為表的主鍵 Name string } ``` 如果你當前的表主鍵不是 id 欄位,那麼你可以通過 `primaryKey`標籤將其它欄位設為主鍵: ```go // 將 `UUID` 設為主鍵 type Animal struct { ID int64 UUID string `gorm:"primaryKey"` Name string Age int64 } ``` 如果你的表採用了複合主鍵,那也沒關係: ```go type Product struct { ID string `gorm:"primaryKey"` LanguageCode string `gorm:"primaryKey"` Code string Name string } ``` **注意:**預設情況下,整型 `PrioritizedPrimaryField` 啟用了 `AutoIncrement`,要禁用它,您需要為整型欄位關閉 `autoIncrement`: ```go type Product struct { CategoryID uint64 `gorm:"primaryKey;autoIncrement:false"` TypeID uint64 `gorm:"primaryKey;autoIncrement:false"` } ``` ##### GORM 標籤 GORM 通過在 struct 上定義自定義的 gorm 標籤來實現自動化建立表的功能: ```go type User struct { Name string `gorm:"size:255"` //string預設長度255,size重設長度 Age int `gorm:"column:my_age"` //設定列名為my_age Num int `gorm:"AUTO_INCREMENT"` //自增 IgnoreMe int `gorm:"-"` // 忽略欄位 Email string `gorm:"type:varchar(100);unique_index"` //type設定sql型別,unique_index為該列設定唯一索引 Address string `gorm:"not null;unique"` //非空 No string `gorm:"index:idx_no"` // 建立索引並命名,如果有其他同名索引,則建立組合索引 Remark string `gorm:"default:''"` //預設值 } ``` 定義完這些標籤之後,你可以使用 *AutoMigrate* 在 MySQL 建立連線之後建立表: ```go func main() { db, err := gorm.Open("mysql", "root:123456789@/test_db?charset=utf8&parseTime=True&loc=Local") if err != nil { fmt.Println("connect db error: ", err) } db.AutoMigrate(&model.User{}) } ``` *AutoMigrate* 用於自動遷移你的 schema,保持 schema 是最新的。 該 API 會建立表、缺失的外來鍵、約束、列和索引。 如果大小、精度、是否為空可以更改,則 *AutoMigrate* 會改變列的型別。 出於保護資料的目的,它 **不會** 刪除未使用的列。 ##### 預設模型 **GORM 定義一個 `gorm.Model` 結構體,其包括欄位 `ID`、`CreatedAt`、`UpdatedAt`、`DeletedAt`。** ```go // gorm.Model 的定義 type Model struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` } ``` 如果你覺得上面這幾個欄位名欄位名是你想要的,那麼你完全可以在你的模型中引入它: ```go type User struct { gorm.Model Id int64 `json:"id"` Name string `json:"name"` Age int32 `json:"age"` Sex int8 `json:"sex"` Phone string `json:"phone"` } ``` 反之如果不是你需要的,就沒必要多此一舉。 ##### 表名 這裡是一個很大的坑。**GORM 使用結構體名的 `蛇形命名` 作為表名。對於結構體 `User`,根據約定,其表名為 `users`**。 當然我們的表名肯定不會是這樣設定的,所以為什麼作者要採用這種設定實在是難以捉摸。 這裡有兩種方式去修改表名:第一種就是去掉這個預設設定;第二種就是在保留預設設定的基礎上通過重新設定表名來替換。 先說如何通過重新設定表名來替換,可以實現 `Tabler` 介面來更改預設表名,例如: ```go type Tabler interface { TableName() string } // TableName 會將 User 的表名重寫為 `user_new_name` func (User) TableName() string { return "user_new_name" } ``` 通過去掉預設配置上面已經有提,配置 *SingularTable: true* 這選項即可。 ##### 列名覆蓋 預設情況下列名遵循普通 struct 的規則: ```go type User struct { ID uint // 列名是 `id` Name string // 列名是 `name` Birthday time.Time // 列名是 `birthday` CreatedAt time.Time // 列名是 `created_at` } ``` 如果你的列名和欄位不匹配的時候,可以通過如下方式重新指定: ```go type Animal struct { AnimalID int64 `gorm:"column:beast_id"` // 將列名設為 `beast_id` Birthday time.Time `gorm:"column:day_of_the_beast"` // 將列名設為 `day_of_the_beast` Age int64 `gorm:"column:age_of_the_beast"` // 將列名設為 `age_of_the_beast` } ``` ##### 日期欄位時間型別設定 GORM 約定使用 `CreatedAt`、`UpdatedAt` 追蹤建立/更新時間。如果你定義了這種欄位,GORM 在建立、更新時會自動填充。 如果想要儲存 UNIX(毫/納)秒時間戳而不是 time,只需簡單地將 `time.Time` 修改為 `int` 即可: ```go type User struct { CreatedAt time.Time // 在建立時,如果該欄位值為零值,則使用當前時間填充 UpdatedAt int // 在建立時該欄位值為零值或者在更新時,使用當前時間戳秒數填充 Updated int64 `gorm:"autoUpdateTime:nano"` // 使用時間戳填納秒數充更新時間 Updated int64 `gorm:"autoUpdateTime:milli"` // 使用時間戳毫秒數填充更新時間 Created int64 `gorm:"autoCreateTime"` // 使用時間戳秒數填充建立時間 } ``` ##### 嵌入結構體 對於匿名欄位,GORM 會將其欄位包含在父結構體中,例如: ```go type User struct { gorm.Model Name string } // 等效於 type User struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` Name string } ``` 對於正常的結構體欄位,你也可以通過標籤 `embedded` 將其嵌入,例如: ```go type Author struct { Name string Email string } type Blog struct { ID int Author Author `gorm:"embedded"` Upvotes int32 } // 等效於 type Blog struct { ID int64 Name string Email string Upvotes int32 } ``` #### CRUD操作 ##### 新增相關 單行插入,gorm 會返回插入之後的主鍵資訊: ```go func InsertOneUser(user model.User) (id int64, err error) { tx := constants.GVA_DB.Create(&user) if tx.Error != nil { constants.GVA_LOG.Error("InsertOne err", zap.Any("err", tx.Error)) return 0, tx.Error } return user.Id, nil } ``` 批量插入,批量插入也會同步返回插入之後的主鍵資訊: ```go func BatchInsertUsers(users []model.User) (ids []int64, err error) { tx := constants.GVA_DB.CreateInBatches(users, len(users)) if tx.Error != nil { constants.GVA_LOG.Error("BatchInsert err", zap.Any("err", tx.Error)) return []int64{}, tx.Error } ids = []int64{} for idx, user := range users { ids[idx] = user.Id } return ids, nil } ``` 插入衝突操作-Upsert: 如果你的表設定了唯一索引的情況下,插入可能會出現主鍵衝突的情況,MySQL 本身是提供了相關的操作命令 *ON DUPLICATE KEY UPDATE*,那麼對應到 Gorm 中的函式是 Upsert: ```go // 在衝突時,什麼都不做 constants.GVA_DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) // 在`id`衝突時,將列更新為預設值 constants.GVA_DB.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.Assignments(map[string]interface{}{"name": "","age":0, "sex": 1}), }).Create(&user) // 在`id`衝突時,將列更新為新值 constants.GVA_DB.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"name", "age", "sex", "phone"}), }).Create(&user) // 在衝突時,更新除主鍵以外的所有列到新值。 constants.GVA_DB.Clauses(clause.OnConflict{UpdateAll: true,}).Create(&user) ``` ##### 刪除相關 根據主鍵刪除: ```go //根據 id 刪除資料 func DeleteUserById(id int64) (err error) { user := model.User{Id: id} err = constants.GVA_DB.Delete(&user).Error if err != nil { constants.GVA_LOG.Error("DeleteUserById err", zap.Any("err", err)) return err } return nil } ``` 根據條件刪除: ```go constants.GVA_DB.Where("sex = ?", 0).Delete(model.User{}) ``` 批量刪除: ```go //根據 id 批量刪除資料 func BatchDeleteUserByIds(ids []int64) (err error) { if ids == nil || len(ids) == 0 { return } //刪除方式1 err = constants.GVA_DB.Where("id in ?", ids).Delete(model.User{}).Error if err != nil { constants.GVA_LOG.Error("DeleteUserById err", zap.Any("err", err)) return err } //刪除方式 2 //constants.GVA_DB.Delete(model.User{}, "id in ?", ids) return nil } ``` 對於全域性刪除的阻止設定 如果在沒有任何條件的情況下執行批量刪除,GORM 不會執行該操作,並返回 `ErrMissingWhereClause` 錯誤。對此,你必須加一些條件,或者使用原生 SQL,或者啟用 `AllowGlobalUpdate` 模式,例如: ```go // DELETE FROM `user` WHERE 1=1 constants.GVA_DB.Where("1 = 1").Delete(&model.User{}) //原生sql刪除 constants.GVA_DB.Exec("DELETE FROM user") //跳過設定 constants.GVA_DB.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.User{}) ``` ##### 更新操作 全量更新 struct 的所有欄位,包括零值: ```go //根據id更新資料,全量欄位更新,即使欄位是0值 func UpdateUserById(user model.User) (err error) { err = constants.GVA_DB.Save(&user).Error if err != nil { constants.GVA_LOG.Error("UpdateUserById err", zap.Any("err", err)) return err } return nil } ``` 更新指定列: ```go //更新指定列 //update user set `columnName` = v where id = id; func UpdateSpecialColumn(id int64, columnName string, v interface{}) (err error) { err = constants.GVA_DB.Model(&model.User{Id: id}).Update(columnName, v).Error if err != nil { constants.GVA_LOG.Error("UpdateSpecialColumn err", zap.Any("err", err)) return err } return nil } ``` 更新非0值的欄位: ```go //更新- 根據 `struct` 更新屬性,只會更新非零值的欄位 //update user set `columnName` = v where id = id; //當通過 struct 更新時,GORM 只會更新非零欄位。 如果您想確保指定欄位被更新,你應該使用 Select 更新選定欄位,或使用 map 來完成更新操作 func UpdateSelective(user model.User) (effected int64, err error) { tx := constants.GVA_DB.Model(&user).Updates(&model.User{ Id: user.Id, Name: user.Name, Age: user.Age, Sex: user.Sex, Phone: user.Phone, }) } ``` 如果你想更新0值的欄位,那麼可以使用 Select 函式先選擇指定的列名,或者使用 map 來完成: ```go //map 方式會更新0值欄位 tx = constants.GVA_DB.Model(&user).Updates(map[string]interface{}{ "Id": user.Id, "Name": user.Name, "Age": user.Age, "Sex": user.Sex, "Phone": user.Phone, }) ``` Select 方式指定列名: ```go //Select 方式指定列名 tx = constants.GVA_DB.Model(&user).Select("Name", "Age", "Phone").Updates(&model.User{ Id: user.Id, Name: user.Name, Age: user.Age, Sex: user.Sex, Phone: user.Phone, }) ``` Select 選定所有列名: ```go // Select 所有欄位(查詢包括零值欄位的所有欄位) tx = constants.GVA_DB.Model(&user).Select("*").Updates(&model.User{ Id: user.Id, Name: user.Name, Age: user.Age, Sex: user.Sex, Phone: user.Phone, }) ``` Select 排除指定列名: ```go // Select 除 Phone 外的所有欄位(包括零值欄位的所有欄位) tx = constants.GVA_DB.Model(&user).Select("*").Omit("Phone").Updates(&model.User{ Id: user.Id, Name: user.Name, Age: user.Age, Sex: user.Sex, Phone: user.Phone, }) ``` 根據條件批量更新: ```go //根據 條件 批量更新 func BatchUpdateByIds(ids []int64, user model.User) (effected int64, err error) { if ids == nil || len(ids) == 0 { return } tx := constants.GVA_DB.Model(model.User{}).Where("id in ?", ids).Updates(&user) if tx.Error != nil { return 0, tx.Error } return tx.RowsAffected, nil } ``` ##### 查詢操作 查詢是重頭戲放在最後。 Gorm 提供的便捷查詢: First:獲取第一條記錄(主鍵升序) ```go // SELECT * FROM user ORDER BY id LIMIT 1; constants.GVA_DB.First(&user) ``` 獲取一條記錄,沒有指定排序欄位: ```go // SELECT * FROM user LIMIT 1; constants.GVA_DB.Take(&user) ``` 獲取最後一條記錄(主鍵降序): ```go // SELECT * FROM user ORDER BY id DESC LIMIT 1; constants.GVA_DB.Last(&user) ``` 使用主鍵的方式查詢: ```go // SELECT * FROM user WHERE id = 10; constants.GVA_DB.First(&user, 10) // SELECT * FROM user WHERE id = 10; constants.GVA_DB.First(&user, "10") // SELECT * FROM user WHERE id IN (1,2,3); constants.GVA_DB.Find(&user, []int{1,2,3}) ``` 條件查詢: ```go // 獲取第一條匹配的記錄 // SELECT * FROM user WHERE name = 'xiaoming' ORDER BY id LIMIT 1; constants.GVA_DB.Where("name = ?", "xiaoming").First(&user) // 獲取全部匹配的記錄 // SELECT * FROM user WHERE name <> 'xiaoming'; constants.GVA_DB.Where("name <> ?", "xiaoming").Find(&user) // IN // SELECT * FROM user WHERE name IN ('xiaoming','xiaohong'); constants.GVA_DB.Where("name IN ?", []string{"xiaoming", "xiaohong"}).Find(&user) // LIKE // SELECT * FROM user WHERE name LIKE '%ming%'; constants.GVA_DB.Where("name LIKE ?", "%ming%").Find(&user) // AND // SELECT * FROM user WHERE name = 'xiaoming' AND age >= 33; constants.GVA_DB.Where("name = ? AND age >= ?", "xiaoming", 33).Find(&user) // Time // SELECT * FROM user WHERE updated_at > '2021-03-10 15:44:23'; constants.GVA_DB.Where("updated_at > ?", "2021-03-10 15:44:23").Find(&user) // BETWEEN // SELECT * FROM user WHERE created_at BETWEEN ''2021-03-07 15:44:23' AND '2021-03-10 15:44:23'; constants.GVA_DB.Where("created_at BETWEEN ? AND ?", "2021-03-07 15:44:23", "2021-03-10 15:44:23").Find(&user) ``` not 條件操作: ```go // SELECT * FROM user WHERE NOT name = "xiaoming" ORDER BY id LIMIT 1; constants.GVA_DB.Not("name = ?", "xiaoming").First(&user) // Not In // SELECT * FROM user WHERE name NOT IN ("xiaoming", "xiaohong"); constants.GVA_DB.Not(map[string]interface{}{"name": []string{"xiaoming", "xiaohong"}}).Find(&user) // Struct // SELECT * FROM user WHERE name <> "xiaoming" AND age <> 20 ORDER BY id LIMIT 1; constants.GVA_DB.Not(model.User{Name: "xiaoming", Age: 20}).First(&user) // 不在主鍵切片中的記錄 // SELECT * FROM user WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1; constants.GVA_DB.Not([]int64{1,2,3}).First(&user) ``` or 操作: ```go // SELECT * FROM user WHERE name = 'xiaoming' OR name = 'xiaohong'; constants.GVA_DB.Where("name = ?", "xiaoming").Or("name = ?", "xiaohong").Find(&user) // Struct // SELECT * FROM user WHERE name = 'xiaoming' OR (name = 'xiaohong' AND age = 20); constants.GVA_DB.Where("name = 'xiaoming'").Or(model.User{Name: "xiaohong", Age: 20}).Find(&user) // Map // SELECT * FROM user WHERE name = 'xiaoming' OR (name = 'xiaohong' AND age = 20); constants.GVA_DB.Where("name = 'xiaoming'").Or(map[string]interface{}{"name": "xiaohong", "age": 20}).Find(&user) ``` 查詢返回指定欄位: 如果你只要要查詢特定的欄位,可以使用 Select 來指定返回欄位: ```go // SELECT name, age FROM user; constants.GVA_DB.Select("name", "age").Find(&user) // SELECT name, age FROM user; constants.GVA_DB.Select([]string{"name", "age"}).Find(&user) // SELECT COALESCE(age,'20') FROM user; constants.GVA_DB.Table("user").Select("COALESCE(age,?)", 20).Rows() ``` 指定排序方式: ```go // SELECT * FROM users ORDER BY age desc, name; constants.GVA_DB.Order("age desc, name").Find(&users) // 多個 order // SELECT * FROM users ORDER BY age desc, name; constants.GVA_DB.Order("age desc").Order("name").Find(&users) // SELECT * FROM users ORDER BY FIELD(id,1,2,3) constants.GVA_DB.Clauses(clause.OrderBy{ Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true}, }).Find(&model.User{}) ``` 分頁查詢: ```go // SELECT * FROM user LIMIT 10; constants.GVA_DB.Limit(10).Find(&user) // SELECT * FROM user OFFSET 10; constants.GVA_DB.Offset(10).Find(&user) // SELECT * FROM user OFFSET 0 LIMIT 10; constants.GVA_DB.Limit(10).Offset(0).Find(&user) ``` 分組查詢-Group & Having: ```go // SELECT name, sum(age) as total FROM `users` WHERE name LIKE "ming%" GROUP BY `name` constants.GVA_DB.Model(&model.User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result) // SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group" constants.GVA_DB.Model(&model.User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result) ``` Distinct 使用: ```go //SELECT distinct(name, age) from user order by name, age desc constants.GVA_DB.Distinct("name", "age").Order("name, age desc").Find(&user) ``` #### 事務操作 如同在 MySQL 中操作事務一樣,事務的開始是以 Begin 開始,以 Commit 結束: ```go //事務測試 func TestGormTx(user model.User) (err error) { tx := constants.GVA_DB.Begin() // 注意,一旦你在一個事務中,使用tx作為資料庫控制代碼 if err := tx.Create(&model.User{ Name: "liliya", Age: 13, Sex: 0, Phone: "15543212346", }).Error; err != nil { tx.Rollback() return err } if err := tx.Updates(&model.User{ Id: user.Id, Name: user.Name, Age: user.Age, Sex: user.Sex, Phone: user.Phone, }).Error; err != nil { tx.Rollback() return err } tx.Commit() return nil } ``` 以上就是關於 GORM 使用相關的操作說明以及可能會出現的問題,關於 Gorm 的使用還有一些高階特性,這裡就不做全面的演示,還是先熟悉基本 api 的操作等需要用到高階特性的時候再去看看也不遲。示例程式碼都已經上傳到 Github,大家可以下載下來練習一下。 ![](https://img2020.cnblogs.com/blog/1607781/202103/1607781-20210311122049187-575612263.jpg)