Gorm 入門介紹與基本使用

一、ORM簡介

1.1 什么是ORM

ORM(Object-Relational Mapping)是一種編程技術(shù),它將對象和關(guān)系數(shù)據(jù)庫之間的映射抽象出來,使得開發(fā)者可以通過面向?qū)ο蟮姆绞讲僮鲾?shù)據(jù)庫,而不用直接處理SQL語句,相當(dāng)于在業(yè)務(wù)邏輯層和數(shù)據(jù)庫層之間一座橋梁。在Golang中,有一款優(yōu)秀的ORM框架叫做Gorm,它提供了強大的功能,使得數(shù)據(jù)庫操作變得更加簡單和靈活。

1.2 使用ORM的好處

使用ORM的好處主要包括:

1.2.1 避免直接操作SQL語句

ORM框架可以屏蔽底層數(shù)據(jù)庫的細(xì)節(jié),開發(fā)者不需要編寫復(fù)雜的SQL語句,從而降低了開發(fā)的難度。

1.2.2 提高代碼的可維護性

通過使用ORM,代碼變得更加清晰、簡潔,易于理解和維護。ORM框架通常提供了模型定義、數(shù)據(jù)驗證等功能,使得開發(fā)者可以更專注于業(yè)務(wù)邏輯。

1.2.3 跨數(shù)據(jù)庫兼容性

ORM框架通常提供了對多種數(shù)據(jù)庫的支持,開發(fā)者可以輕松切換數(shù)據(jù)庫而無需修改大量代碼。

1.3 使用ORM的缺點

使用ORM也有一些缺點,主要包括:

1.3.1 學(xué)習(xí)成本

學(xué)習(xí)使用ORM框架需要一定的時間,尤其是對于初學(xué)者來說,需要掌握框架的各種功能和用法。

1.3.2 性能開銷

ORM框架可能引入一些性能開銷,尤其是在處理大量數(shù)據(jù)時。開發(fā)者需要在性能和開發(fā)效率之間做出權(quán)衡。

1.4 ORM解析過程

ORM框架的解析過程包括以下步驟:

1.4.1 模型定義

開發(fā)者需要定義數(shù)據(jù)模型,通常是一個結(jié)構(gòu)體,表示數(shù)據(jù)庫中的表結(jié)構(gòu)。

1.4.2 數(shù)據(jù)驗證

ORM框架通常提供了數(shù)據(jù)驗證的功能,確保數(shù)據(jù)的合法性和完整性。

1.4.3 映射關(guān)系

ORM框架會建立數(shù)據(jù)模型與數(shù)據(jù)庫表之間的映射關(guān)系,將結(jié)構(gòu)體的字段與表的列進行對應(yīng)。

1.4.4 CRUD操作

開發(fā)者可以通過ORM框架進行CRUD(Create、Read、Update、Delete)操作,而不用直接編寫SQL語句。

1.4.5 SQL生成與執(zhí)行

最終,ORM框架會根據(jù)開發(fā)者的操作生成相應(yīng)的SQL語句,并執(zhí)行在數(shù)據(jù)庫中。

通過以上步驟,開發(fā)者可以使用ORM框架方便地進行數(shù)據(jù)庫操作,提高開發(fā)效率。

在接下來的部分,我們將深入學(xué)習(xí)Gorm框架的使用,從入門到精通。

二、Gorm 介紹與安裝

2.1 介紹

Gorm是一款用于Golang的ORM框架,它提供了豐富的功能,包括模型定義、數(shù)據(jù)驗證、關(guān)聯(lián)查詢等。Gorm的設(shè)計目標(biāo)是簡潔而強大,使得開發(fā)者能夠更輕松地進行數(shù)據(jù)庫操作。

一些Gorm的特性包括:

  1. 模型定義和操作:
    • 全功能 ORM
    • 關(guān)聯(lián) (Has One,Has Many,Belongs To,Many To Many,多態(tài),單表繼承)
    • Create,Save,Update,Delete,F(xiàn)ind 中的鉤子方法
    • 支持 Preload、Joins 的預(yù)加載
    • 批量插入,F(xiàn)indInBatches,F(xiàn)ind/Create with Map,使用 SQL 表達(dá)式、Context Valuer 進行 CRUD
    • 復(fù)合主鍵,索引,約束
  2. 事務(wù)處理和數(shù)據(jù)庫操作:
    • 事務(wù),嵌套事務(wù),Save Point,Rollback To Saved Point
    • Context、預(yù)編譯模式、DryRun 模式
    • SQL 構(gòu)建器,Upsert,數(shù)據(jù)庫鎖,Optimizer/Index/Comment Hint,命名參數(shù),子查詢
    • Auto Migration
  3. 其他功能:
    • 自定義 Logger
    • 靈活的可擴展插件 API:Database Resolver(多數(shù)據(jù)庫,讀寫分離)、Prometheus…
    • 每個特性都經(jīng)過了測試的重重考驗
    • 開發(fā)者友好

最新版本2.x,比1.x有較大改動,注意:Gorm最新地址為https://github.com/go-gorm/gorm,之前https://github.com/jinzhu/gorm地址為v1舊版本

2.2 相關(guān)文檔

2.3 安裝

要使用Gorm,首先需要安裝它??梢允褂肎o的包管理工具go get進行安裝:

# 安裝gorm
go get -u gorm.io/gorm
# 如果要使用`mysql`, `GORM` 做了二次 封裝,安裝對應(yīng)數(shù)據(jù)庫的驅(qū)動
go get -u gorm.io/driver/mysql

安裝完成后,可以在項目中引入Gorm:

import "gorm.io/gorm"

接下來,我們將學(xué)習(xí)如何連接數(shù)據(jù)庫并開始使用Gorm。

三、Gorm 連接數(shù)據(jù)庫

3.1 快速連接 MySQL

連接MySQL數(shù)據(jù)庫是Gorm的常見用法。以下是一個簡單的例子:

package main

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

func main() {
    dsn := "root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // 遷移 schema
    db.AutoMigrate(&Product{})

    // Create
    db.Create(&Product{Code: "D42", Price: 100})

    // Read
    var product Product
    db.First(&product, 1)                 // 根據(jù)整型主鍵查找
    db.First(&product, "code = ?", "D42") // 查找 code 字段值為 D42 的記錄

    // Update - 將 product 的 price 更新為 200
    db.Model(&product).Update("Price", 200)
    // Update - 更新多個字段
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 僅更新非零值字段
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

    // Delete - 刪除 product
    db.Delete(&product, 1)
}

請注意替換username、passworddbname為你自己的數(shù)據(jù)庫信息。這里使用了MySQL數(shù)據(jù)庫,你也可以根據(jù)需要選擇其他數(shù)據(jù)庫。

3.2 MySQL數(shù)據(jù)庫配置解析

dsn := "username:password@tcp(localhost:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"

上述連接字符串(DSN)中的參數(shù)有:

  • username:password:數(shù)據(jù)庫用戶名和密碼。
  • tcp(localhost:3306):數(shù)據(jù)庫地址和端口。
  • /dbname:數(shù)據(jù)庫名稱。
  • charset=utf8mb4:設(shè)置字符集為UTF-8。
  • parseTime=True:啟用時間解析。
  • loc=Local:設(shè)置時區(qū)。

你可以根據(jù)實際情況調(diào)整這些參數(shù)。

3.3 自定義 MySQL 驅(qū)動

GORM 允許通過 DriverName 選項自定義 MySQL 驅(qū)動,例如:

package main

import (
    _ "example.com/my_mysql_driver"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func main() {
    db, err := gorm.Open(mysql.New(mysql.Config{
        DriverName: "my_mysql_driver",
        DSN:        "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", // data source name, 詳情參考:https://github.com/go-sql-driver/mysql#dsn-data-source-name
    }), &gorm.Config{})
}

3.4 現(xiàn)有的數(shù)據(jù)庫連接mysql

GORM 允許通過一個現(xiàn)有的數(shù)據(jù)庫連接來初始化 *gorm.DB

import (
    "database/sql"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func main() {
    sqlDB, err := sql.Open("mysql", "mydb_dsn")
    gormDB, err := gorm.Open(mysql.New(mysql.Config{
        Conn: sqlDB,
    }), &gorm.Config{})
}

3.5 切換數(shù)據(jù)庫驅(qū)動

Gorm支持多種數(shù)據(jù)庫,你可以根據(jù)需要選擇不同的數(shù)據(jù)庫驅(qū)動。上面的例子中我們使用了MySQL的驅(qū)動,如果要連接其他數(shù)據(jù)庫,只需更改導(dǎo)入的數(shù)據(jù)庫驅(qū)動即可。

例如,如果要連接SQLite數(shù)據(jù)庫,可以使用以下驅(qū)動:

import "gorm.io/driver/sqlite"

然后在gorm.Open()中使用sqlite.Open()。

3.6 編寫新驅(qū)動

GORM 官方支持的數(shù)據(jù)庫類型有:MySQL,PostgreSQL,SQLite, SQL ServerTiDB

有些數(shù)據(jù)庫可能兼容 mysql、postgres 的方言,在這種情況下,你可以直接使用這些數(shù)據(jù)庫的方言。

對于其它不兼容的情況,您可以自行編寫一個新驅(qū)動,這需要實現(xiàn) 方言接口。

參考官網(wǎng)文檔:【編寫新驅(qū)動】

3.7 連接PostgreSQL

3.7.1 連接PostgreSQ舉例

package main

import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name string
    Age  int
}

func main() {
    dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
    defer db.DB()

    // Migrate the schema
    db.AutoMigrate(&User{})
}

我們使用 pgx 作為 postgres 的 database/sql 驅(qū)動,默認(rèn)情況下,它會啟用 prepared statement 緩存,你可以這樣禁用它:

// https://github.com/go-gorm/postgres
db, err := gorm.Open(postgres.New(postgres.Config{
  DSN: "user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai",
  PreferSimpleProtocol: true, // disables implicit prepared statement usage
}), &gorm.Config{})

3.7.2 連接PostgreSQL配置解析

dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"

上述示例中的 DSN 解析如下:

  • user=gorm:數(shù)據(jù)庫用戶名。
  • password=gorm:數(shù)據(jù)庫密碼。
  • dbname=gorm:數(shù)據(jù)庫名稱。
  • port=9920:數(shù)據(jù)庫連接端口。
  • sslmode=disable:禁用 SSL 模式。
  • TimeZone=Asia/Shanghai:設(shè)置時區(qū)。

根據(jù)實際情況,你需要替換這些值為你的 PostgreSQL 數(shù)據(jù)庫連接信息。

3.7.3 自定義 PostgreSQL 驅(qū)動

GORM 允許通過 DriverName 選項自定義 PostgreSQL 驅(qū)動,例如:

import (
  _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
  "gorm.io/gorm"
)

db, err := gorm.Open(postgres.New(postgres.Config{
  DriverName: "cloudsqlpostgres",
  DSN: "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable",
})

3.7.4 現(xiàn)有的數(shù)據(jù)庫連接PostgreSQL

GORM 允許通過一個現(xiàn)有的數(shù)據(jù)庫連接來初始化 *gorm.DB

import (
  "database/sql"
  "gorm.io/driver/postgres"
  "gorm.io/gorm"
)

sqlDB, err := sql.Open("pgx", "mydb_dsn")
gormDB, err := gorm.Open(postgres.New(postgres.Config{
  Conn: sqlDB,
}), &gorm.Config{})

3.8 連接SQLite

3.8.1 連接SQLite舉例

package main

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

func main() {
    dsn := "test.db" // SQLite數(shù)據(jù)庫文件路徑
    db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
    defer db.DB()

    // 遷移 schema
    db.AutoMigrate(&Product{})

    // 創(chuàng)建記錄
    db.Create(&Product{Code: "D42", Price: 100})

    // 查詢記錄
    var product Product
    db.First(&product, 1)
    db.First(&product, "code = ?", "D42")

    // 更新記錄
    db.Model(&product).Update("Price", 200)
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"})
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

    // 刪除記錄
    db.Delete(&product, 1)
}

3.8.2 連接SQLite配置解析

dsn := "test.db"

上述示例中的 test.db 是 SQLite 數(shù)據(jù)庫文件的路徑。你可以根據(jù)實際情況調(diào)整文件路徑。

3.9 連接SQL Server

3.9.1 連接SQL Server舉例

package main

import (
    "gorm.io/driver/sqlserver"
    "gorm.io/gorm"
)
type Product struct {
    gorm.Model
    Code  string
    Price uint
}

func main() {
    dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
    db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
    defer db.DB()

    // 遷移 schema
    db.AutoMigrate(&Product{})

    // 創(chuàng)建記錄
    db.Create(&Product{Code: "D42", Price: 100})

    // 查詢記錄
    var product Product
    db.First(&product, 1)
    db.First(&product, "code = ?", "D42")

    // 更新記錄
    db.Model(&product).Update("Price", 200)
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"})
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

    // 刪除記錄
    db.Delete(&product, 1)
}

3.9.2 連接SQL Server配置解析

dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"

上述示例中的 DSN 解析如下:

  • gorm:LoremIpsum86:用戶名和密碼。
  • localhost:9930:數(shù)據(jù)庫服務(wù)器地址和端口。
  • database=gorm:數(shù)據(jù)庫名稱。

3.10 連接TiDB

3.10.1 連接TiDB舉例

iDB 兼容 MySQL 協(xié)議。 因此你可以按照 MySQL 一節(jié)來創(chuàng)建與 TiDB 的連接。

在使用 TiDB 時有一些值得注意的內(nèi)容:

  • 您可以在結(jié)構(gòu)體中使用 gorm:"primaryKey;default:auto_random()" 標(biāo)簽從而調(diào)用 TiDB 的 AUTO_RANDOM 功能。
  • TiDB supported SAVEPOINT from v6.2.0, please notice the version of TiDB when you use this feature.
  • TiDB supported FOREIGN KEY from v6.6.0, please notice the version of TiDB when you use this feature.
package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type Product struct {
    ID    uint `gorm:"primaryKey;default:auto_random()"`
    Code  string
    Price uint
}

func main() {
    dsn := "root:@tcp(127.0.0.1:4000)/test"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    db.AutoMigrate(&Product{})

    insertProduct := &Product{Code: "D42", Price: 100}

    db.Create(insertProduct)
    fmt.Printf("insert ID: %d, Code: %s, Price: %d\n",
        insertProduct.ID, insertProduct.Code, insertProduct.Price)

    readProduct := &Product{}
    db.First(&readProduct, "code = ?", "D42") // find product with code D42

    fmt.Printf("read ID: %d, Code: %s, Price: %d\n",
        readProduct.ID, readProduct.Code, readProduct.Price)
}

3.10.2 連接TiDB配置解析

dsn := "root:@tcp(127.0.0.1:4000)/test"
  • root: 數(shù)據(jù)庫用戶名。在這里,用戶名是 "root"。
  • @: 分隔用戶名和密碼的分隔符。
  • "": 數(shù)據(jù)庫密碼。在這里,密碼是空字符串,表示沒有密碼。
  • tcp(127.0.0.1:4000): 數(shù)據(jù)庫服務(wù)器的地址和端口。在這里,MySQL 服務(wù)器位于本地主機(127.0.0.1),端口是 4000。
  • /test: 數(shù)據(jù)庫名稱。在這里,數(shù)據(jù)庫名是 "test"。

3.10 連接Clickhouse

3.10.1 連接Clickhouse舉例

package main

import (
    "gorm.io/driver/clickhouse"
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name string
    Age  int
}

func main() {
    dsn := "tcp://localhost:9000?database=gorm&username=gorm&password=gorm&read_timeout=10&write_timeout=20"
    db, err := gorm.Open(clickhouse.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // 自動遷移 (這是GORM自動創(chuàng)建表的一種方式--譯者注)
    db.AutoMigrate(&User{})
    // 設(shè)置表選項
    db.Set("gorm:table_options", "ENGINE=Distributed(cluster, default, hits)").AutoMigrate(&User{})

    // 插入
    db.Create(&user)

    // 查詢
    db.Find(&user, "id = ?", 10)

    // 批量插入
    var users = []User{user1, user2, user3}
    db.Create(&users)
    // ...
}

3.10.2 連接Clickhouse配置解析

dsn := "tcp://localhost:9000?database=gorm&username=gorm&password=gorm&read_timeout=10&write_timeout=20"

上述示例中的 DSN 解析如下:

  • tcp://localhost:9000: ClickHouse 服務(wù)器的地址和端口。在這里,ClickHouse 服務(wù)器位于本地主機(localhost),端口是 9000。這是 ClickHouse 的默認(rèn)端口。
  • ?: 連接參數(shù)的起始標(biāo)志。
  • database=gorm: 數(shù)據(jù)庫名稱。在這里,數(shù)據(jù)庫名是 "gorm"。
  • username=gorm: 數(shù)據(jù)庫用戶名。在這里,用戶名是 "gorm"。
  • password=gorm: 數(shù)據(jù)庫密碼。在這里,密碼是 "gorm"。
  • read_timeout=10: 讀取超時時間。在這里,設(shè)置為 10 秒。
  • write_timeout=20: 寫入超時時間。在這里,設(shè)置為 20 秒。

你需要根據(jù)實際情況替換這些值。

四、連接池

GORM 使用 database/sql 來維護連接池

sqlDB, err := db.DB()

// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
sqlDB.SetMaxIdleConns(10)

// SetMaxOpenConns sets the maximum number of open connections to the database.
sqlDB.SetMaxOpenConns(100)

// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
sqlDB.SetConnMaxLifetime(time.Hour)

五、MySQL 其他配置

注意:想要正確的處理 time.Time ,您需要帶上 parseTime 參數(shù), (更多參數(shù)) 要支持完整的 UTF-8 編碼,您需要將 charset=utf8 更改為 charset=utf8mb4 查看 此文章 獲取詳情

MySQl 驅(qū)動程序提供了 一些高級配置 可以在初始化過程中使用,例如:

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 類型字段的默認(rèn)長度
  DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的數(shù)據(jù)庫不支持
  DontSupportRenameIndex: true, // 重命名索引時采用刪除并新建的方式,MySQL 5.7 之前的數(shù)據(jù)庫和 MariaDB 不支持重命名索引
  DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的數(shù)據(jù)庫和 MariaDB 不支持重命名列
  SkipInitializeWithVersion: false, // 根據(jù)當(dāng)前 MySQL 版本自動配置
}), &gorm.Config{})

六、加入日志打印sql

6.1 打印日志

Gorm 有一個 默認(rèn) logger 實現(xiàn),默認(rèn)情況下,它會打印慢 SQL 和錯誤

Logger 接受的選項不多,您可以在初始化時自定義它,例如:

newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志輸出的目標(biāo),前綴和日志包含的內(nèi)容——譯者注)
  logger.Config{
    SlowThreshold: time.Second,   // 慢 SQL 閾值
    LogLevel:      logger.Silent, // 日志級別
    IgnoreRecordNotFoundError: true,   // 忽略ErrRecordNotFound(記錄未找到)錯誤
    Colorful:      false,         // 禁用彩色打印
  },
)

// 全局模式
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
  Logger: newLogger,
})

// 新建會話模式
tx := db.Session(&Session{Logger: newLogger})
tx.First(&user)
tx.Model(&user).Update("Age", 18)

6.2 日志級別

GORM 定義了這些日志級別:Silent、Error、Warn、Info

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
  Logger: logger.Default.LogMode(logger.Silent),
})

6.3 Debug

Debug 單個操作,將當(dāng)前操作的 log 級別調(diào)整為 logger.Info

db.Debug().Where("name = ?", "jinzhu").First(&User{})

6.4 具體代碼

package main

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "log"
    "os"
    "time"
)

type  User struct {
    ID int
}
func main() {
    // 日志配置
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志輸出的目標(biāo),前綴和日志包含的內(nèi)容——譯者注)
        logger.Config{
            SlowThreshold:             time.Second, // 慢 SQL 閾值
            LogLevel:                  logger.Info, // 日志級別為info
            IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
            Colorful:                  true,        // 彩色打印
        },
    )

    dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: newLogger,
    })
    if err != nil {
        panic(err) // 如果數(shù)據(jù)庫不存在會報錯
    }
    db.AutoMigrate(&User{})
}

七、參考文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容