如何在接口类型中访问结构成员?

huangapple go评论75阅读模式
英文:

How can I reach struct member in interface type

问题

我必须在切片中保存多种类型的结构体并对它们进行初始化。我使用了接口类型的可变参数,并对它们进行了遍历。如果我调用接口的方法,它可以正常工作,但是当我尝试访问结构体时就不行了。我该如何解决这个问题?

注意:Seed() 方法返回数据的文件名。

接口:

type Seeder interface {
	Seed() string
}

方法:

func (AirportCodes) Seed() string {
	return "airport_codes.json"
}

SeederSlice:

seederModelList = []globals.Seeder{
		m.AirportCodes{},
		m.Term{},
	}

最后一个是 SeedSchema 函数:

func (db *Database) SeedSchema(models ...globals.Seeder) error {
	var (
		subjects []globals.Seeder
		fileByte []byte
		err      error
		// tempMember map[string]interface{}
	)
	if len(models) == 0 {
		subjects = seederModelList
	} else {
		subjects = models
	}
	for _, model := range subjects {
		fileName := model.Seed()
		fmt.Printf("%+v\n", model)
		if fileByte, err = os.ReadFile("db/seeds/" + fileName); err != nil {
			fmt.Println("asd", err)
			// return err
		}
		if err = json.Unmarshal(fileByte, &model); err != nil {
			fmt.Println("dsa", err)
			// return err
		}
		modelType := reflect.TypeOf(model).Elem()
		modelPtr2 := reflect.New(modelType)
		fmt.Printf("%s\n", modelPtr2) 
	}
	return nil
}

我可以访问到具体的模型,但无法创建成员和进行初始化。

英文:

I have to keep multi type struct in slice and seed them. I took with variadic parameter of interface type and foreach them. If I call the method of interface it works, but when I trying to reach to struct I can't. How can I solve that?

Note: Seed() method return the file name of datas.

The Interface:

type Seeder interface {
	Seed() string
}

Method:

func (AirportCodes) Seed() string {
	return "airport_codes.json"
}

SeederSlice:

seederModelList = []globals.Seeder{
		m.AirportCodes{},
		m.Term{},
	}

And the last one, SeedSchema function:

func (db *Database) SeedSchema(models ...globals.Seeder) error {
	var (
		subjects []globals.Seeder
		fileByte []byte
		err      error
		// tempMember map[string]interface{}
	)
	if len(models) == 0 {
		subjects = seederModelList
	} else {
		subjects = models
	}
	for _, model := range subjects {
		fileName := model.Seed()
		fmt.Printf("%+v\n", model)
		if fileByte, err = os.ReadFile("db/seeds/" + fileName); err != nil {
			fmt.Println("asd", err)
			// return err
		}
		if err = json.Unmarshal(fileByte, &model); err != nil {
			fmt.Println("dsa", err)
			// return err
		}
		modelType := reflect.TypeOf(model).Elem()
		modelPtr2 := reflect.New(modelType)
		fmt.Printf("%s\n", modelPtr2) 
	}
	return nil
}

I can reach exact model but can't create a member and seed.

答案1

得分: 1

在评论中经过一些来回之后,我将在这里发布这个最简化的答案。这并不是一个明确的“这就是你要做的”类型的答案,但我希望这能为你提供足够的信息来开始。为了达到这一点,我根据你提供的代码片段做出了一些假设,并且我假设你想通过某种命令(例如 your_bin seed)来填充数据库。因此,做出了以下假设:

  1. 模式和相应的模型/类型已存在(如 AirportCodes 等)
  2. 每个类型都有自己的源文件(名称来自 Seed() 方法,返回一个 .json 文件名)
  3. 因此,假设种子数据的格式类似于 [{"seed": "data"}, {"more": "data"}]
  4. 种子文件可以追加,如果模式发生变化,种子文件中的数据可以一起更改。这在目前来说不是很重要,但仍然是一个需要注意的假设。

好的,让我们从将所有 JSON 文件移动到可预测位置开始。在一个规模较大的实际应用程序中,你可以使用类似于 XDG 基本路径 的东西,但为了简洁起见,让我们假设你在一个从 / 开始的临时容器中运行,并且所有相关资产都已复制到该容器中。

在基本路径下的一个 seed_data 目录中放置所有种子文件是有意义的。每个文件包含特定表的种子数据,因此文件中的所有数据都可以很好地映射到单个模型。暂时忽略关系数据。我们只假设,目前,这些文件中的数据至少在内部上是一致的,并且任何 X-to-X 关系数据都必须具有正确的 ID 字段,以允许 JOIN 等操作。


让我们开始

所以我们有了我们的模型和 JSON 文件中的数据。现在我们可以创建一个包含这些模型的切片,确保你希望/需要在插入其他数据之前存在的数据作为较高的条目(较低的索引)表示。就像这样:

seederModelList = []globals.Seeder{
    m.AirportCodes{}, // 在 Term 之前填充
    m.Term{},         // 在 AirportCodes 之后填充
}

但是,不要从这个 Seed 方法中返回文件名,为什么不传入连接并让模型自己处理数据呢:

func (_ AirportCodes) Seed(db *gorm.DB) error {
    // 我们知道这个模型使用哪个文件
    data, err := os.ReadFile("seed_data/airport_codes.json")
    if err != nil {
        return err
    }
    // 我们有了数据,我们可以将其解组为 AirportCode 实例
    codes := []*AirportCodes{}
    if err := json.Unmarshal(data, &codes); err != nil {
        return err
    }
    // 现在进行 INSERT、UPDATE 或 UPSERT:
    db.Clauses(clause.OnConflict{
        UpdateAll: true,
    }).Create(&codes)
}

对于其他模型,如 Terms,也是同样的做法:

func (_ Terms) Seed(db *gorm.DB) error {
    // 我们知道这个模型使用哪个文件
    data, err := os.ReadFile("seed_data/terms.json")
    if err != nil {
        return err
    }
    // 我们有了数据,我们可以将其解组为 Terms 实例
    terms := []*Terms{}
    if err := json.Unmarshal(data, &terms); err != nil {
        return err
    }
    // 现在进行 INSERT、UPDATE 或 UPSERT:
    return db.Clauses(clause.OnConflict{
        UpdateAll: true,
    }).Create(&terms)
}

当然,这会导致一些混乱,因为我们在模型中有了数据库访问,而实际上模型应该只是一个 DTO。在错误处理方面也有很多需要改进的地方,但基本思路是这样的:

func main() {
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) // 为简洁起见省略了错误处理
    seeds := []interface{
        Seed(*gorm.DB) error
    }{
        model.AirportCodes{},
        model.Terms{},
        // 等等...
    }
    for _, m := range seeds {
        if err := m.Seed(db); err != nil {
            panic(err)
        }
    }
    db.Close()
}

好的,这样我们就可以开始了,但是让我们将所有这些内容整理成更好的形式:

  1. 将整个数据库交互从 DTO/模型中移出
  2. 将事务封装起来,以便在出现错误时可以回滚
  3. 对初始切片进行一些更新,使其更清晰

所以,如前所述,我假设你有类似于 repositories 的东西来处理数据库交互。我们应该依赖于我们的 repositories,而不是在模型上调用 Seed 方法并将数据库连接传递给它们:

db, _ := gorm.Open() // 与之前相同
acs := repo.NewAirportCodes(db) // 传入连接
tms := repo.NewTerms(db) // 同样...

现在我们的模型仍然可以返回 JSON 文件名,或者我们可以将其作为 repos 中的 const。在这一点上,它并不 真正 重要。主要的事情是,我们可以在 repositories 中完成实际的数据插入。

如果你愿意,你可以将种子切片更改为这样:

calls := []func() error{
    acs.Seed, // 假设你的 repo 有一个 Seed 函数来执行它应该执行的操作
    tms.Seed,
}

然后在循环中执行所有的种子操作:

for _, c := range calls {
    if err := c(); err != nil {
        panic(err)
    }
}

现在,我们只剩下事务的问题了。幸运的是,gorm 让这变得非常简单:

db, _ := gorm.Open()
db.Transaction(func(tx *gorm.DB) error {
    acs := repo.NewAirportCodes(tx) // 创建 repo,但使用 TX 作为连接
    if err := acs.Seed(); err != nil {
        return err // 返回错误将自动回滚事务
    }
    tms := repo.NewTerms(tx)
    if err := tms.Seed(); err != nil {
        return err
    }
    return nil // 提交事务
})

在这里还有很多可以调整的地方,比如创建可以单独提交的相关数据批次,可以添加更精确的错误处理和更详细的日志记录,更好地处理冲突(区分 CREATE 和 UPDATE 等)。然而,最重要的是要记住一件事:

Gorm 有一个迁移系统

我必须承认,我已经有一段时间没有使用 gorm 了,但我IRC,如果模型发生变化,你可以自动迁移表,并在启动时运行自定义的 Go 代码和/或 SQL 文件,这些文件可以很容易地用于填充数据。值得考虑一下是否可行...

英文:

After some back and forth in the comments, I'll just post this minimal answer here. It's by no means a definitive "this is what you do" type answer, but I hope this can at least provide you with enough information to get you started. To get to this point, I've made a couple of assumptions based on the snippets of code you've provided, and I'm assuming you want to seed the DB through a command of sorts (e.g. your_bin seed). That means the following assumptions have been made:

  1. The Schemas and corresponding models/types are present (like AirportCodes and the like)
  2. Each type has its own source file (name comes from Seed() method, returning a .json file name)
  3. Seed data is, therefore, assumed to be in a format like [{"seed": "data"}, {"more": "data"}].
  4. The seed files can be appended, and should the schema change, the data in the seed files could be changed all together. This is of less importance ATM, but still, it's an assumption that should be noted.

OK, so let's start by moving all of the JSON files in a predictable location. In a sizeable, real world application you'd use something like XDG base path, but for the sake of brevity, let's assume you're running this in a scratch container from / and all relevant assets have been copied in to said container.

It'd make sense to have all seed files in the base path under a seed_data directory. Each file contains the seed data for a specific table, and therefore all the data within a file maps neatly onto a single model. Let's ignore relational data for the time being. We'll just assume that, for now, the data in these files is at least internally consistent, and any X-to-X relational data will have to right ID fields allowing for JOIN's and the like.


Let's start

So we have our models, and the data in JSON files. Now we can just create a slice of said models, making sure that data that you want/need to be present before other data is inserted is represented as a higher entry (lower index) than the other. Kind of like this:

seederModelList = []globals.Seeder{
    m.AirportCodes{}, // seeds before Term
    m.Term{},         // seeds after AirportCodes
}

But instead or returning the file name from this Seed method, why not pass in the connection and have the model handle its own data like this:

func (_ AirportCodes) Seed(db *gorm.DB) error {
    // we know what file this model uses
    data, err := os.ReadFile("seed_data/airport_codes.json")
    if err != nil {
        return err
    }
    // we have the data, we can unmarshal it as AirportCode instances
    codes := []*AirportCodes{}
    if err := json.Unmarshal(data, &codes); err != nil {
        return err
    }
    // now INSERT, UPDATE, or UPSERT:
    db.Clauses(clause.OnConflict{
        UpdateAll: true,
    }).Create(&codes)
}

Do the same for other models, like Terms:

func (_ Terms) Seed(db *gorm.DB) error {
    // we know what file this model uses
    data, err := os.ReadFile("seed_data/terms.json")
    if err != nil {
        return err
    }
    // we have the data, we can unmarshal it as Terms instances
    terms := []*Terms{}
    if err := json.Unmarshal(data, &terms); err != nil {
        return err
    }
    // now INSERT, UPDATE, or UPSERT:
    return db.Clauses(clause.OnConflict{
        UpdateAll: true,
    }).Create(&terms)
}

Of course, this does result in a bit of a mess considering we have DB access in a model, which should really be just a DTO if you ask me. This also leaves a lot to be desired in terms of error handling, but the basic gist of it would be this:

func main() {
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) // omitted error handling for brevity
    seeds := []interface{
        Seed(*gorm.DB) error
    }{
        model.AirportCodes{},
        model.Terms{},
        // etc...
    }
    for _, m := range seeds {
        if err := m.Seed(db); err != nil {
            panic(err)
        }
    }
    db.Close()
}

OK, so this should get us started, but let's just move this all into something a bit nicer by:

  1. Moving the whole DB interaction out of the DTO/model
  2. Wrap things into a transaction, so we can roll back on error
  3. Update the initial slice a bit to make things cleaner

So as mentioned earlier, I'm assuming you have something like repositories to handle DB interactions in a separate package. Rather than calling Seed on the model, and passing the DB connection into those, we should instead rely on our repositories:

db, _ := gorm.Open() // same as before
acs := repo.NewAirportCodes(db) // pass in connection
tms := repo.NewTerms(db) // again...

Now our model can still return the JSON file name, or we can have that as a const in the repos. At this point, it doesn't really matter. The main thing is, we can have the actual inserting of data done in the repositories.

You can, if you want, change your seed slice thing to something like this:

calls := []func() error{
    acs.Seed, // assuming your repo has a Seed function that does what it's supposed to do
    tms.Seed,
}

Then perform all the seeding in a loop:

for _, c := range calls {
    if err := c(); err != nil {
        panic(err)
    }
}

Now, this just leaves us with the issue of the transaction stuff. Thankfully, gorm makes this really rather simple:

db, _ := gorm.Open()
db.Transaction(func(tx *gorm.DB) error {
    acs := repo.NewAirportCodes(tx) // create repo's, but use TX for connection
    if err := acs.Seed(); err != nil {
        return err // returning an error will automatically rollback the transaction
    }
    tms := repo.NewTerms(tx)
    if err := tms.Seed(); err != nil {
        return err
    }
    return nil // commit transaction
})

There's a lot more you can fiddle with here like creating batches of related data that can be committed separately, you can add more precise error handling and more informative logging, handle conflicts better (distinguish between CREATE and UPDATE etc...). Above all else, though, something worth keeping in mind:

Gorm has a migration system

I have to confess that I've not dealt with gorm in quite some time, but IIRC, you can have the tables be auto-migrated if the model changes, and run either custom go code and or SQL files on startup which can be used, rather easily, to seed the data. Might be worth looking at the feasibility of that...

huangapple
  • 本文由 发表于 2022年9月20日 16:03:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/73783407.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定