Golang微服框架Kratos與它的小伙伴系列 - ORM框架 - GORM

Golang微服框架Kratos與它的小伙伴系列 - ORM框架 - GORM

什么是ORM岸裙?

面向?qū)ο缶幊毯完P(guān)系型數(shù)據(jù)庫,都是目前最流行的技術(shù)年叮,但是它們的模型是不一樣的瘸洛。

面向?qū)ο缶幊贪阉袑?shí)體看成對(duì)象(object),關(guān)系型數(shù)據(jù)庫則是采用實(shí)體之間的關(guān)系(relation)連接數(shù)據(jù)。很早就有人提出呆盖,關(guān)系也可以用對(duì)象表達(dá)拖云,這樣的話,就能使用面向?qū)ο缶幊逃τ郑瑏聿僮麝P(guān)系型數(shù)據(jù)庫宙项。

簡(jiǎn)單說,ORM 就是通過實(shí)例對(duì)象的語法株扛,完成關(guān)系型數(shù)據(jù)庫的操作的技術(shù)尤筐,是"對(duì)象-關(guān)系映射"(Object/Relational Mapping) 的縮寫。

ORM 把數(shù)據(jù)庫映射成對(duì)象洞就。

  • 數(shù)據(jù)庫的表(table) --> 類(class)
  • 記錄(record盆繁,行數(shù)據(jù))--> 對(duì)象(object)
  • 字段(field)--> 對(duì)象的屬性(attribute)

舉例來說,下面是一行 SQL 語句旬蟋。

SELECT id, first_name, last_name, phone, birth_date, sex
FROM persons 
WHERE id = 10

程序直接運(yùn)行 SQL油昂,操作數(shù)據(jù)庫的寫法如下。

res = db.execSql(sql);
name = res[0]["FIRST_NAME"];

改成 ORM 的寫法如下倾贰。

p = Person.get(10);
name = p.first_name;

一比較就可以發(fā)現(xiàn)冕碟,ORM 使用對(duì)象,封裝了數(shù)據(jù)庫操作匆浙,因此可以不碰 SQL 語言安寺。開發(fā)者只使用面向?qū)ο缶幊蹋c數(shù)據(jù)對(duì)象直接交互首尼,不用關(guān)心底層數(shù)據(jù)庫挑庶。

ORM 有下面這些優(yōu)點(diǎn):

  • 數(shù)據(jù)模型都在一個(gè)地方定義,更容易更新和維護(hù)饰恕,也利于重用代碼。
  • ORM 有現(xiàn)成的工具井仰,很多功能都可以自動(dòng)完成埋嵌,比如數(shù)據(jù)消毒、預(yù)處理俱恶、事務(wù)等等雹嗦。
  • 它迫使你使用 MVC 架構(gòu),ORM 就是天然的 Model合是,最終使代碼更清晰了罪。
  • 基于 ORM 的業(yè)務(wù)代碼比較簡(jiǎn)單,代碼量少聪全,語義性好泊藕,容易理解。
  • 你不必編寫性能不佳的 SQL难礼。

ORM 也有很突出的缺點(diǎn):

  • ORM 庫不是輕量級(jí)工具娃圆,需要花很多精力學(xué)習(xí)和設(shè)置玫锋。
  • 對(duì)于復(fù)雜的查詢,ORM 要么是無法表達(dá)讼呢,要么是性能不如原生的 SQL撩鹿。
  • ORM 抽象掉了數(shù)據(jù)庫層,開發(fā)者無法了解底層的數(shù)據(jù)庫操作悦屏,也無法定制一些特殊的 SQL节沦。

什么是GORM?

GORM 是基于Go語言實(shí)現(xiàn)的ORM庫础爬,它是Golang目前比較熱門的數(shù)據(jù)庫ORM操作庫甫贯,對(duì)開發(fā)者也比較友好,使用非常方便簡(jiǎn)單幕帆。

最重要的是获搏,它是一個(gè)正經(jīng)的國(guó)產(chǎn)開源庫。支持國(guó)產(chǎn)失乾!

特性

  • 全功能 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ù)加載
  • 事務(wù),嵌套事務(wù)穴吹,Save Point幽勒,Rollback To Saved Point
  • Context、預(yù)編譯模式港令、DryRun 模式
  • 批量插入啥容,F(xiàn)indInBatches,F(xiàn)ind/Create with Map顷霹,使用 SQL 表達(dá)式咪惠、Context Valuer 進(jìn)行 CRUD
  • SQL 構(gòu)建器,Upsert淋淀,數(shù)據(jù)庫鎖遥昧,Optimizer/Index/Comment Hint,命名參數(shù),子查詢
  • 復(fù)合主鍵渠鸽,索引叫乌,約束
  • Auto Migration
  • 自定義 Logger
  • 靈活的可擴(kuò)展插件 API:Database Resolver(多數(shù)據(jù)庫,讀寫分離)徽缚、Prometheus…
  • 每個(gè)特性都經(jīng)過了測(cè)試的重重考驗(yàn)
  • 開發(fā)者友好

安裝庫

go get -u gorm.io/gorm

除此以外憨奸,還需要安裝數(shù)據(jù)庫的驅(qū)動(dòng):

# 安裝SQLite驅(qū)動(dòng)
go get -u gorm.io/driver/sqlite

# 安裝MySQL驅(qū)動(dòng)
go get -u gorm.io/driver/mysql

# 安裝PostgreSQL驅(qū)動(dòng)
go get -u gorm.io/driver/postgres

# 安裝SQL Server驅(qū)動(dòng)
go get -u gorm.io/driver/sqlserver

# 安裝Clickhouse驅(qū)動(dòng)(Clickhouse兼容MySQL的協(xié)議,所以直接用MySQL驅(qū)動(dòng)連接也是一樣的)
go get -u gorm.io/driver/clickhouse

GORM的一些數(shù)據(jù)庫基本操作

因?yàn)閿?shù)據(jù)庫是復(fù)雜的凿试,SQL是復(fù)雜的排宰,復(fù)雜到能夠出好幾本書,所以是絕不可能在簡(jiǎn)單的篇幅里面講完整那婉,只能夠?qū)⒊S玫囊恍┎僮鳎ㄟB接數(shù)據(jù)庫板甘、CURD)拿來舉例講講。

連接數(shù)據(jù)庫

MySQL

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

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{})

PostgreSQL

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

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{})

SQLite

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

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})

SQL Server

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

dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})

Clickhouse

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

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{})

自動(dòng)遷移 Automatic Migration

db.AutoMigrate(&User{})

自動(dòng)遷移功能详炬,會(huì)創(chuàng)建表盐类、缺失的外鍵、約束呛谜、列和索引在跳。

定義模型

type User struct {
  gorm.Model
  UserName string
  NickName string
}

gorm.Model則是包含了通用的一些字段,比如:id隐岛、創(chuàng)建時(shí)間猫妙、更新時(shí)間、刪除時(shí)間等……

// gorm.Model definition
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

在默認(rèn)的情況下:

  • 表名聚凹,將會(huì)被轉(zhuǎn)換為 復(fù)數(shù)形式 以及 蛇形命名法(snake_case)割坠,比如:User轉(zhuǎn)換為users
  • 字段名妒牙,將被轉(zhuǎn)換為 蛇形命名法(snake_case) 字符串彼哼,比如:UserName被轉(zhuǎn)換為user_name

當(dāng)然湘今,你也可以用column標(biāo)明字段名的輸出:

type User struct {
  gorm.Model
  UserName string `gorm:"column:username"`
  NickName string `gorm:"column:nickname"`
}

定義TableName()方法控制表名的輸出:

func (u User) TableName() string {
    return "users"
}

增 Create

db.Create(&User{UserName: "TestUserName", NickName: "TestNickName"})

刪 Delete

// 軟刪除
// UPDATE users SET deleted_at="2020-03-13 10:23" WHERE id = user.id;
db.Delete(&user, 1)

db.Delete(&user)

// 批量軟刪除
db.Where("age = ?", 20).Delete(&User{})

// 物理刪除
// DELETE FROM users WHERE id=10;
db.Unscoped().Delete(&user)

改 Update

db.Model(&user).Update("nick_name", "NewNickName")

// Update - 更新多個(gè)字段
db.Model(&user).Updates(User{UserName: "NewUserName", NickName: "NewNickName"})
db.Model(&user).Updates(map[string]interface{}{"user_name": "NewUserName", "nick_name": "NewNickName"})

查 Read

var user User

// 獲取第一條記錄(主鍵升序)
// SELECT * FROM users ORDER BY id LIMIT 1;
db.First(&user)

// 根據(jù)整型主鍵查找
// SELECT * FROM users WHERE id = 10;
db.First(&user, 10)
db.First(&user, "10")

// 根據(jù)主鍵獲取記錄敢朱,如果是非整型主鍵
// SELECT * FROM users WHERE user_name = 'TestUserName';
db.First(&user, "user_name = ?", "TestUserName")

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

// 獲取一條記錄,沒有指定排序字段
// SELECT * FROM users LIMIT 1;
db.Take(&user)

// 獲取最后一條記錄(主鍵降序)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
db.Last(&user)

與Kratos攜起手來

官方推薦的包結(jié)構(gòu)是這樣的:

|- data
|- biz
|- service
|- server

那么象浑,我們可以把模型定義做成一個(gè)package蔫饰,放到data文件夾下面去:

|- data
| |- modal
|- biz
|- service
|- server

創(chuàng)建數(shù)據(jù)庫客戶端

data/data.go文件中添加創(chuàng)建Gorm數(shù)據(jù)庫客戶端的方法NewGormClient

import (
    "gorm.io/driver/clickhouse"
    "gorm.io/driver/mysql"
    "gorm.io/driver/postgres"
    "gorm.io/driver/sqlite"
    "gorm.io/driver/sqlserver"

    "gorm.io/gorm"
)

// Data .
type Data struct {
    db  *gorm.Client
}

// NewGormClient 創(chuàng)建數(shù)據(jù)庫客戶端
func NewGormClient(cfg *conf.Bootstrap, logger log.Logger) *gorm.DB {
    l := log.NewHelper(log.With(logger, "module", "ent/data/user-service"))

    var driver gorm.Dialector
    switch cfg.Data.Database.Driver {
    default:
        fallthrough
    case "mysql":
        driver = mysql.Open(cfg.Data.Database.Source)
        break
    case "postgres":
        driver = postgres.Open(cfg.Data.Database.Source)
        break
    case "clickhouse":
        driver = clickhouse.Open(cfg.Data.Database.Source)
        break
    case "sqlite":
        driver = sqlite.Open(cfg.Data.Database.Source)
        break
    case "sqlserver":
        driver = sqlserver.Open(cfg.Data.Database.Source)
        break
    }

    client, err := gorm.Open(driver, &gorm.Config{})
    if err != nil {
        l.Fatalf("failed opening connection to db: %v", err)
    }

    // 運(yùn)行數(shù)據(jù)庫遷移工具
    if cfg.Data.Database.Migrate {
        if err := client.AutoMigrate(
            &models.User{},
        ); err != nil {
            l.Fatalf("failed creating schema resources: %v", err)
        }
    }
    return client
}

并將之注入到ProviderSet

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
    NewGormClient,
    ...
)

需要說明的是數(shù)據(jù)庫遷移工具琅豆,如果數(shù)據(jù)庫中不存在表愉豺,遷移工具會(huì)創(chuàng)建一個(gè);如果字段存在改變茫因,遷移工具會(huì)對(duì)字段進(jìn)行修改蚪拦。

創(chuàng)建UseCase

在biz文件夾下創(chuàng)建user.go

package biz

type UserRepo interface {
    ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error)
    GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error)
    CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error)
    UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error)
    DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (bool, error)
}

type UserUseCase struct {
    repo UserRepo
    log  *log.Helper
}

func NewUserUseCase(repo UserRepo, logger log.Logger) *UserUseCase {
    l := log.NewHelper(log.With(logger, "module", "user/usecase"))
    return &UserUseCase{repo: repo, log: l}
}

func (uc *UserUseCase) ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) {
    return uc.repo.ListUser(ctx, req)
}

func (uc *UserUseCase) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) {
    return uc.repo.GetUser(ctx, req)
}

func (uc *UserUseCase) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) {
    return uc.repo.CreateUser(ctx, req)
}

func (uc *UserUseCase) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) {
    return uc.repo.UpdateUser(ctx, req)
}

func (uc *UserUseCase) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) {
    return uc.repo.DeleteUser(ctx, req)
}

注入到biz.ProviderSet

package biz

// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(
    NewUserUseCase,
    ...
)

創(chuàng)建Repo

data文件夾下創(chuàng)建user.go文件,實(shí)際操作數(shù)據(jù)庫的操作都在此處。

package data

var _ biz.UserRepo = (*UserRepo)(nil)

type UserRepo struct {
    data *Data
    log  *log.Helper
}

func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
    l := log.NewHelper(log.With(logger, "module", "User/repo"))
    return &UserRepo{
        data: data,
        log:  l,
    }
}

func (r *UserRepo) convertModelToProto(in *models.User) *v1.User {
    if in == nil {
        return nil
    }
    return &v1.User{
        Id:         uint32(in.ID),
        UserName:   &in.UserName,
        NickName:   &in.NickName,
        Password:   &in.Password,
        CreateTime: util.TimeToTimeString(&in.CreatedAt),
        UpdateTime: util.TimeToTimeString(&in.UpdatedAt),
    }
}

func (r *UserRepo) List(_ context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) {
    var results []models.User

    result := r.data.db.
        Limit(int(req.GetPageSize())).
        Offset(int(req.GetPageSize() * (req.GetPage() - 1))).
        Find(&results)
    if result.Error != nil {
        return nil, result.Error
    }

    items := make([]*v1.User, 0, len(results))
    for _, res := range results {
        item := r.convertModelToProto(&res)
        items = append(items, item)
    }

    var count int64
    result = r.data.db.Model(&models.User{}).
        Count(&count)
    if result.Error != nil {
        return nil, result.Error
    }

    return &v1.ListUserResponse{
        Total: int32(count),
        Items: items,
    }, nil
}

func (r *UserRepo) Get(_ context.Context, req *v1.GetUserRequest) (*v1.User, error) {
    res := &models.User{}
    r.data.db.First(res, "id = ?", req.GetId())
    return r.convertModelToProto(res), nil
}

func (r *UserRepo) Create(_ context.Context, req *v1.CreateUserRequest) (*v1.User, error) {
    cryptoPassword, err := crypto.HashPassword(req.User.GetPassword())
    if err != nil {
        return nil, err
    }

    res := &models.User{
        UserName: req.User.GetUserName(),
        NickName: req.User.GetNickName(),
        Password: cryptoPassword,
    }

    result := r.data.db.Create(res)
    if result.Error != nil {
        return nil, result.Error
    }

    return r.convertModelToProto(res), err
}

func (r *UserRepo) Update(_ context.Context, req *v1.UpdateUserRequest) (*v1.User, error) {
    var cryptoPassword string
    var err error
    if req.User.Password != nil {
        cryptoPassword, err = crypto.HashPassword(req.User.GetPassword())
        if err != nil {
            return nil, err
        }
    }

    res := &models.User{
        UserName: req.User.GetUserName(),
        NickName: req.User.GetNickName(),
        Password: cryptoPassword,
    }

    result := r.data.db.Model(res).Updates(res)
    if result.Error != nil {
        return nil, result.Error
    }

    return r.convertModelToProto(res), err
}

func (r *UserRepo) Delete(_ context.Context, req *v1.DeleteUserRequest) (bool, error) {
    result := r.data.db.Delete(&models.User{}, req.GetId())
    if result.Error != nil {
        return false, result.Error
    }
    return true, nil
}

注入到data.ProviderSet

package data

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
    NewUserRepo,
    ...
)

在Service中調(diào)用

package service

type UserService struct {
    v1.UnimplementedUserServiceServer

    uc  *biz.UserUseCase
    log *log.Helper
}

func NewUserService(logger log.Logger, uc *biz.UserUseCase) *UserService {
    l := log.NewHelper(log.With(logger, "module", "service/user"))
    return &UserService{
        log: l,
        uc:  uc,
    }
}

// ListUser 獲取用戶列表
func (s *UserService) ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) {
    return s.uc.ListUser(ctx, req)
}

// GetUser 獲取一個(gè)用戶
func (s *UserService) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) {
    return s.uc.GetUser(ctx, req)
}

// CreateUser 創(chuàng)建一個(gè)用戶
func (s *UserService) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) {
    return s.uc.CreateUser(ctx, req)
}

// UpdateUser 更新一個(gè)用戶
func (s *UserService) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) {
    return s.uc.UpdateUser(ctx, req)
}

// DeleteUser 刪除一個(gè)用戶
func (s *UserService) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (*emptypb.Empty, error) {
    _, err := s.uc.DeleteUser(ctx, req)
    if err != nil {
        return nil, err
    }
    return &emptypb.Empty{}, nil
}

注入到service.ProviderSet

package service

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
    NewUserService,
    ...
)

將服務(wù)注冊(cè)到gRPC服務(wù)器當(dāng)中去:

package server

// NewGRPCServer new a gRPC server.
func NewGRPCServer(cfg *conf.Bootstrap, logger log.Logger,
    userSvc *service.UserService,
) *grpc.Server {
    srv := bootstrap.CreateGrpcServer(cfg, logging.Server(logger))

    userV1.RegisterUserServiceServer(srv, userSvc)

    return srv
}

這樣驰贷,我們就有了一個(gè)完整的用戶服務(wù)盛嘿。

實(shí)例代碼

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市括袒,隨后出現(xiàn)的幾起案子次兆,更是在濱河造成了極大的恐慌,老刑警劉巖锹锰,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芥炭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡恃慧,警方通過查閱死者的電腦和手機(jī)园蝠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痢士,“玉大人彪薛,你說我怎么就攤上這事〉□澹” “怎么了善延?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)褥蚯。 經(jīng)常有香客問我挚冤,道長(zhǎng),這世上最難降的妖魔是什么赞庶? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任训挡,我火速辦了婚禮,結(jié)果婚禮上歧强,老公的妹妹穿的比我還像新娘澜薄。我一直安慰自己,他們只是感情好摊册,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布肤京。 她就那樣靜靜地躺著,像睡著了一般茅特。 火紅的嫁衣襯著肌膚如雪忘分。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天白修,我揣著相機(jī)與錄音妒峦,去河邊找鬼。 笑死兵睛,一個(gè)胖子當(dāng)著我的面吹牛肯骇,可吹牛的內(nèi)容都是我干的窥浪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼笛丙,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼漾脂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胚鸯,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤骨稿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后姜钳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啊终,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年傲须,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蓝牲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泰讽,死狀恐怖例衍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情已卸,我是刑警寧澤佛玄,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站累澡,受9級(jí)特大地震影響梦抢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愧哟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一奥吩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蕊梧,春花似錦霞赫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至甘改,卻和暖如春旅东,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背十艾。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工抵代, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疟羹。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓主守,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親榄融。 傳聞我的和親對(duì)象是個(gè)殘疾皇子参淫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容