Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flatten child struct to the parent struct #218

Open
lucasoares opened this issue Jun 15, 2023 · 5 comments
Open

Flatten child struct to the parent struct #218

lucasoares opened this issue Jun 15, 2023 · 5 comments

Comments

@lucasoares
Copy link

lucasoares commented Jun 15, 2023

Hello.

It's very common to have structs mapped as columns in the owner's table to avoid complexity in small projects and to be able to create indexes. This is also useful for many other use cases where creating another table brings other problems. I think we could have a way to flatten (idk if this is the right word for this) the child struct to the parent's table automatically using struct tags.

Example:

// package user
type User struct {
	Id                         string
	Location              *location.Location     `dynamo:"location,flatten"`
}

// package location
type Location struct {
	Country           *string
	StateProvince  *string
	City                  *string
        GeoHash         *string
	PostalCode      *string
	StreetAddress  *string
	Latitude            *float64
	Longitude         *float64
}

Would generate the following result in DynamoDB:

Id LocationName LocationCountry LocationStateProvince LocationCity LocationPostalCode LocationStreetAddress LocationLatitude LocationLongitude
foo bar foo bar foo bar foo bar foo
bar foo bar foo bar foo bar foo bar

Currently I'm doing the above approach to be able to receive in my API the structured structured inside the location field (json) but saving it as individual root fields in dynamodb. Too much boilerplate:

type User struct {
	Id                string       `json:"id"`
	// Location fields
	Location              *location.Location     `dynamo:"-"`
	LocationCountry       *string                `json:"-" dynamo:"LocationCountry,omitempty"`
	LocationStateProvince *string                `json:"-" dynamo:"LocationStateProvince,omitempty"`
	LocationCity          *string                `json:"-" dynamo:"LocationCity,omitempty"`
	LocationPostalCode    *string                `json:"-" dynamo:"LocationPostalCode,omitempty"`
	LocationStreetAddress *string                `json:"-" dynamo:"LocationStreetAddress,omitempty"`
	LocationLatitude      *float64               `json:"-" dynamo:"LocationLatitude,omitempty"`
	LocationLongitude     *float64               `json:"-" dynamo:"LocationLongitude,omitempty"`
	LocationGeohash       *string                `json:"-" dynamo:"LocationGeohash,omitempty"`
}

func (u *User) GetLocation() *location.Location {
	if u.Location != nil {
		return u.Location
	}

	return &location.Location{
		Country:       u.LocationCountry,
		StateProvince: u.LocationStateProvince,
		City:          u.LocationCity,
		PostalCode:    u.LocationPostalCode,
		StreetAddress: u.LocationStreetAddress,
		Latitude:      u.LocationLatitude,
		Longitude:     u.LocationLongitude,
		Geohash:       u.LocationGeohash,
	}
}

func (u *User) UpdateLocationFields() {
	if u.Location == nil {
		u.Location = u.GetLocation()

		return
	}

	if u.Location.Latitude != nil && u.Location.Longitude != nil {
		geohash := geohash.Encode(*u.Location.Latitude, *u.Location.Longitude)

		u.Location.Geohash = &geohash
	} else {
		u.Location.Latitude = nil
		u.Location.Longitude = nil
	}

	u.LocationCountry = u.Location.Country
	u.LocationStateProvince = u.Location.StateProvince
	u.LocationCity = u.Location.City
	u.LocationPostalCode = u.Location.PostalCode
	u.LocationStreetAddress = u.Location.StreetAddress
	u.LocationLatitude = u.Location.Latitude
	u.LocationLongitude = u.Location.Longitude
	u.LocationGeohash = u.Location.Geohash
}

Then I call UpdateLocationFields before saving to DynamoDB. I don't like my own approach but nothing I can do if this feature isn't native supported by this library.

I think theencoding/json library can't also do this and it will probably be a difficult thing to do, but it doesn't hurt to ask. Or even if there is a better way to do what I need. Thanks.

Thanks!

@guregu
Copy link
Owner

guregu commented Jun 15, 2023

If you embed the location type in your struct it will have the behavior you want (it works the same as the encoding/json package).

type User struct {
	Id string
	*location.Location
}

@lucasoares
Copy link
Author

lucasoares commented Jun 16, 2023

If you embed the location type in your struct it will have the behavior you want (it works the same as the encoding/json package).

type User struct {
	Id string
	*location.Location
}

You're correct that I can embed the struct, but in this case my JSON structure (and struct API) will also change, which is something I can't have it changed.

I need my json API and also my struct API to remain the same with the embedded struct representing the location. =(

@guregu
Copy link
Owner

guregu commented Jun 16, 2023

Hmm, I see what you're saying. You can kind of work around it with the "aux pattern", defining custom marshalers/unmarshalers for either dynamo or json.
For JSON you can do it like this: https://go.dev/play/p/4bObSiEvmCM
There's still boilerplate of course. For dynamo there's some tests here https://github.com/guregu/dynamo/blob/master/decode_aux_test.go
I would just define two structs probably, one embedded and one not. You can do user.Location = response.Location instead of setting each field if you embed it.

Anyway, I'll think about it a bit. I think the stdlib xml package has something like what you proposed. Writing the boilerplate to flatten things is pretty annoying.

@lucasoares
Copy link
Author

I see. I will think more about your suggestion, the problem is because I would have to implement custom marshalers/unmarshalers for each struct I have (so many haha). At least the boilerplate is smaller and I can make a script to create these functions for me.

If you think something else I'm all ears but of course a native approach would be the best scenario.

Thanks.

@guregu
Copy link
Owner

guregu commented Nov 15, 2023

Hello again,
I'm working on improving/optimizing (rewriting) the marshaling stuff in preparation for v2 (it's one of the last things I'd like to do before making breaking changes). I'll experiment with some flattening stuff. I think the xml package has something kind of similar. Might take some time but I would like to support this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants