Add 'Scan to arbitrary destination' wiki page.

This commit is contained in:
go-jet 2019-07-05 15:13:00 +02:00
parent 1ab3ee4be6
commit fb5bf7dd40
5 changed files with 538 additions and 8 deletions

View file

@ -314,6 +314,179 @@ LIMIT 15;
assert.Equal(t, len(filmsPerLanguage[0].Film), limit) assert.Equal(t, len(filmsPerLanguage[0].Film), limit)
} }
func TestExecution1(t *testing.T) {
stmt := City.
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
SELECT(
City.CityID,
City.City,
Address.AddressID,
Address.Address,
Customer.CustomerID,
Customer.LastName,
).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
assertStatementSql(t, stmt, `
SELECT city.city_id AS "city.city_id",
city.city AS "city.city",
address.address_id AS "address.address_id",
address.address AS "address.address",
customer.customer_id AS "customer.customer_id",
customer.last_name AS "customer.last_name"
FROM dvds.city
INNER JOIN dvds.address ON (address.city_id = city.city_id)
INNER JOIN dvds.customer ON (customer.address_id = address.address_id)
WHERE (city.city = 'London') OR (city.city = 'York')
ORDER BY city.city_id, address.address_id, customer.customer_id;
`, "London", "York")
var dest []struct {
model.City
Customers []struct {
model.Customer
Address model.Address
}
}
err := stmt.Query(db, &dest)
assert.NilError(t, err)
assert.Equal(t, len(dest), 2)
assert.Equal(t, dest[0].City.City, "London")
assert.Equal(t, dest[1].City.City, "York")
assert.Equal(t, len(dest[0].Customers), 2)
assert.Equal(t, dest[0].Customers[0].LastName, "Hoffman")
assert.Equal(t, dest[0].Customers[1].LastName, "Vines")
}
func TestExecution2(t *testing.T) {
type MyAddress struct {
ID int32 `sql:"primary_key"`
AddressLine string
}
type MyCustomer struct {
ID int32 `sql:"primary_key"`
LastName *string
Address MyAddress
}
type MyCity struct {
ID int32 `sql:"primary_key"`
Name string
Customers []MyCustomer
}
dest := []MyCity{}
stmt := City.
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
SELECT(
City.CityID.AS("my_city.id"),
City.City.AS("myCity.Name"),
Address.AddressID.AS("My_Address.id"),
Address.Address.AS("my address.address line"),
Customer.CustomerID.AS("my_customer.id"),
Customer.LastName.AS("my_customer.last_name"),
).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
assertStatementSql(t, stmt, `
SELECT city.city_id AS "my_city.id",
city.city AS "myCity.Name",
address.address_id AS "My_Address.id",
address.address AS "my address.address line",
customer.customer_id AS "my_customer.id",
customer.last_name AS "my_customer.last_name"
FROM dvds.city
INNER JOIN dvds.address ON (address.city_id = city.city_id)
INNER JOIN dvds.customer ON (customer.address_id = address.address_id)
WHERE (city.city = 'London') OR (city.city = 'York')
ORDER BY city.city_id, address.address_id, customer.customer_id;
`, "London", "York")
err := stmt.Query(db, &dest)
assert.NilError(t, err)
assert.Equal(t, len(dest), 2)
assert.Equal(t, dest[0].Name, "London")
assert.Equal(t, dest[1].Name, "York")
assert.Equal(t, len(dest[0].Customers), 2)
assert.Equal(t, *dest[0].Customers[0].LastName, "Hoffman")
assert.Equal(t, *dest[0].Customers[1].LastName, "Vines")
}
func TestExecution3(t *testing.T) {
var dest []struct {
CityID int32 `sql:"primary_key"`
CityName string
Customers []struct {
CustomerID int32 `sql:"primary_key"`
LastName *string
Address struct {
AddressID int32 `sql:"primary_key"`
AddressLine string
}
}
}
stmt := City.
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
SELECT(
City.CityID.AS("city_id"),
City.City.AS("city_name"),
Customer.CustomerID.AS("customer_id"),
Customer.LastName.AS("last_name"),
Address.AddressID.AS("address_id"),
Address.Address.AS("address_line"),
).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
assertStatementSql(t, stmt, `
SELECT city.city_id AS "city_id",
city.city AS "city_name",
customer.customer_id AS "customer_id",
customer.last_name AS "last_name",
address.address_id AS "address_id",
address.address AS "address_line"
FROM dvds.city
INNER JOIN dvds.address ON (address.city_id = city.city_id)
INNER JOIN dvds.customer ON (customer.address_id = address.address_id)
WHERE (city.city = 'London') OR (city.city = 'York')
ORDER BY city.city_id, address.address_id, customer.customer_id;
`, "London", "York")
err := stmt.Query(db, &dest)
assert.NilError(t, err)
assert.Equal(t, len(dest), 2)
assert.Equal(t, dest[0].CityName, "London")
assert.Equal(t, dest[1].CityName, "York")
assert.Equal(t, len(dest[0].Customers), 2)
assert.Equal(t, *dest[0].Customers[0].LastName, "Hoffman")
assert.Equal(t, *dest[0].Customers[1].LastName, "Vines")
}
func TestJoinQuerySliceWithPtrs(t *testing.T) { func TestJoinQuerySliceWithPtrs(t *testing.T) {
type FilmsPerLanguage struct { type FilmsPerLanguage struct {
Language model.Language Language model.Language

View file

@ -2,16 +2,16 @@
## SQL Builder ## SQL Builder
SQL Builder files are Go files, containing types necessary to write type safe SQL queries in Go. They are SQL Builder files are Go files, containing types necessary to write type safe SQL queries in Go. They are
autogenerated from database tables and enums. autogenerated from database tables and enums. File names are snake case of the table name or enum name.
### Table SQL Builder files ### Table SQL Builder files
Following rules are applied to generate table SQL Builder files: Following rules are applied to generate table SQL Builder files:
- for every table there is one Go SQL Builder file generated. File name is in snake case of the table name. - for every table there is one Go SQL Builder file generated.
- every file contains one type - struct with nested jet.Table. Type name is a camel case of table name. - every file contains one type - struct with nested jet.Table.
- for every column of table there is a field column in SQL Builder table type. Field name is camel case of column name. - for every column of table there is a field column in SQL Builder table type.
See below table for type mapping. Field name is camel case of column name. See below table for type mapping.
- `AllColumns` is used as shorthand notation for list of all columns. - `AllColumns` is used as shorthand notation for list of all columns.
- `MutableColumns` are all columns minus primary key columns _(Useful in INSERT or UPDATE statements)_. - `MutableColumns` are all columns minus primary key columns _(Useful in INSERT or UPDATE statements)_.
@ -82,7 +82,7 @@ type AddressTable struct {
Following rules are applied to generate enum SQL Builder files: Following rules are applied to generate enum SQL Builder files:
- for every enum there is one Go SQL Builder file generated. File name is in snake case of the enum name. - for every enum there is one Go SQL Builder file generated.
- every file contains one type. Type name is a camel case of enum name. - every file contains one type. Type name is a camel case of enum name.
- for every enum value there is a field in SQL Builder enum struct. Field name is camel case of enum value. Type is - for every enum value there is a field in SQL Builder enum struct. Field name is camel case of enum value. Type is
jet.StringExpression, meaning it can be used by string expressions methods. jet.StringExpression, meaning it can be used by string expressions methods.

View file

@ -0,0 +1,349 @@
## Scan to arbitrary destination
Statements `Query` and `QueryContext` methods perform scan and grouping of row result to arbitrary `destination` structure.
- `Query(db execution.DB, destination interface{}) error` - executes statements over database connection db and stores row result in destination.
- `QueryContext(db execution.DB, context context.Context, destination interface{}) error` - executes statement with a context over database connection db and stores row result in destination.
### How scan works?
The easiest way to understand how scan works is by an example.
Lets say we want to retrieve list of cities, with list of customers for each city, and address for each customer.
For simplicity we will narrow the choice to 'London' and 'York'.
Go SQL builder select statement:
```
stmt := City.
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
SELECT(
City.CityID,
City.City,
Address.AddressID,
Address.Address,
Customer.CustomerID,
Customer.LastName,
).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
```
_Note that we are using jet select statement format([TODO]())_
Debug sql of above statement:
```
SELECT city.city_id AS "city.city_id",
city.city AS "city.city",
address.address_id AS "address.address_id",
address.address AS "address.address",
customer.customer_id AS "customer.customer_id",
customer.last_name AS "customer.last_name"
FROM dvds.city
INNER JOIN dvds.address ON (address.city_id = city.city_id)
INNER JOIN dvds.customer ON (customer.address_id = address.address_id)
WHERE (city.city = 'London') OR (city.city = 'York')
ORDER BY city.city_id, address.address_id, customer.customer_id;
```
Every column is aliased by default. Format is "`table_name`.`column_name`"
Above statement will produce following result set:
|_row_| city.city_id | city.city | address.address_id | address.address | customer.customer_id | customer.last_name |
|---| ------------ | ------------- | ------------------- | -------------------- | -------------------- | ------------------ |
| _1_| 312 | "London" | 256 | "1497 Yuzhou Drive" | 252 | "Hoffman"|
| _2_| 312 | "London" | 517 | "548 Uruapan Street"| 512 | "Vines" |
| _3_| 589 | "York" | 502 | "1515 Korla Way" | 497 | "Sledge" |
Lets execute statement and scan result set to destination `dest`:
```
var dest []struct {
model.City
Customers []struct{
model.Customer
Address model.Address
}
}
err := stmt.Query(db, &dest)
```
`Query` uses reflection to introspect destination type structure, and result set column names(aliases), to be able to map result set data to destination object.
Note that camel case of result set column names(aliases) is the same as `model type name`.`field name`.
For instance `city.city_id` -> `City.CityID`. This is being used to find appropriate column for each destination model field.
It is not an error if there is not a column for each destination model field.
Lets see in general how `Query` works row by row:
- ROW 1:
- dest is slice of structs, so new struct object is initialized and scan proceeds to next step.
- `city.city_id` and `city.city` columns (values `312` and `"London"`) are used to initialize `CityID` and `City` fields of `model.City` object.
- `Customers` is a slice of structs, so new struct object is initialized and scan proceeds to next step.
- `customer.customer_id` and `customer.last_name` is used to initialize fields in `model.Customer` object.
- `address.address_id` and `address.address` is used to initialize fields in `Address model.Address`
- because at least one field of struct is being initialized struct is added to `Customers []struct` and cached by parent and
struct primary key fields([more about primary key fields](TODO)). Primary keys used for caching are `CityID`, `CustomerID` and `AddressID` of `model.City`, `model.Customer`
and `model.Address`
- because at least one field of struct is being initialized struct is added to `var dest []struct` and cached by
struct primary key fields. Primary keys used for caching is only `CityID` from `model.City`
- ROW 2:
- Does not initialize new struct object for `dest []struct` but pulls one from the cache, because `city` with `city_id` of `312` has
already being processed. Following steps are the same as above, new objects are created, stored in slice and cached.
- ROW 3:
- steps would be similar as for the first step. Nothing is pulled from he cache, stored in slice and cached.
Lets print `dest` as a json, to visualize `Query` result:
```
[
{
"CityID": 312,
"City": "London",
"CountryID": 0,
"LastUpdate": "0001-01-01T00:00:00Z",
"Customers": [
{
"CustomerID": 252,
"StoreID": 0,
"FirstName": "",
"LastName": "Hoffman",
"Email": null,
"AddressID": 0,
"Activebool": false,
"CreateDate": "0001-01-01T00:00:00Z",
"LastUpdate": null,
"Active": null,
"Address": {
"AddressID": 256,
"Address": "1497 Yuzhou Drive",
"Address2": null,
"District": "",
"CityID": 0,
"PostalCode": null,
"Phone": "",
"LastUpdate": "0001-01-01T00:00:00Z"
}
},
{
"CustomerID": 512,
"StoreID": 0,
"FirstName": "",
"LastName": "Vines",
"Email": null,
"AddressID": 0,
"Activebool": false,
"CreateDate": "0001-01-01T00:00:00Z",
"LastUpdate": null,
"Active": null,
"Address": {
"AddressID": 517,
"Address": "548 Uruapan Street",
"Address2": null,
"District": "",
"CityID": 0,
"PostalCode": null,
"Phone": "",
"LastUpdate": "0001-01-01T00:00:00Z"
}
}
]
},
{
"CityID": 589,
"City": "York",
"CountryID": 0,
"LastUpdate": "0001-01-01T00:00:00Z",
"Customers": [
{
"CustomerID": 497,
"StoreID": 0,
"FirstName": "",
"LastName": "Sledge",
"Email": null,
"AddressID": 0,
"Activebool": false,
"CreateDate": "0001-01-01T00:00:00Z",
"LastUpdate": null,
"Active": null,
"Address": {
"AddressID": 502,
"Address": "1515 Korla Way",
"Address2": null,
"District": "",
"CityID": 0,
"PostalCode": null,
"Phone": "",
"LastUpdate": "0001-01-01T00:00:00Z"
}
}
]
}
]
```
All the fields missing source column in result set are initialized with empty value.
City of `London` has two customers, which is the product of object reuse in `ROW 2` processing.
### Custom model files
**Programmes are not limited to just model files, any destination will work, as long as camel case of result set column
is equal to `model type name`.`field name`.**
#### Named types
Lets rewrite above example to use custom named model files:
```
type MyAddress struct {
ID int32 `sql:"primary_key"`
AddressLine string
}
type MyCustomer struct {
ID int32 `sql:"primary_key"`
LastName *string
Address MyAddress
}
type MyCity struct {
ID int32 `sql:"primary_key"`
Name string
Customers []MyCustomer
}
dest2 := []MyCity{}
stmt2 := City.
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
SELECT(
City.CityID.AS("my_city.id"), //snake case
City.City.AS("myCity.Name"), //camel case
Address.AddressID.AS("My_Address.id"), //mixed case
Address.Address.AS("my address.address line"), //with spaces
Customer.CustomerID.AS("my_customer.id"),
Customer.LastName.AS("my_customer.last_name"),
).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
err := stmt2.Query(db, &dest2)
```
Destination type names and field names are now changed. Every type has 'My' prefix, every primary key column is named `ID`,
`FirstName` is now string pointer etc.
Because we are using custom types with changed identifier, every column has to be aliased.
For instance: `City.CityID.AS("my_city.id")`, ` City.City.AS("myCity.Name")` etc.
**Table names, column names and aliases doesn't have to be in a snake case. CamelCase, PascalCase or some other mixed space is also supported,
but it is strongly recommended to use snake case for database identifiers.**
Json of new destination is also changed:
```
[
{
"ID": 312,
"Name": "London",
"Customers": [
{
"ID": 252,
"LastName": "Hoffman",
"Address": {
"ID": 256,
"AddressLine": "1497 Yuzhou Drive"
}
},
{
"ID": 512,
"LastName": "Vines",
"Address": {
"ID": 517,
"AddressLine": "548 Uruapan Street"
}
}
]
},
{
"ID": 589,
"Name": "York",
"Customers": [
{
"ID": 497,
"LastName": "Sledge",
"Address": {
"ID": 502,
"AddressLine": "1515 Korla Way"
}
}
]
}
]
```
#### Antonymous types
There is no need to create new named type for every custom model.
Destination type can be declared inline without naming any type.
```
var dest []struct {
CityID int32 `sql:"primary_key"`
CityName string
Customers []struct {
CustomerID int32 `sql:"primary_key"`
LastName string
Address struct {
AddressID int32 `sql:"primary_key"`
AddressLine string
}
}
}
stmt := City.
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
SELECT(
City.CityID.AS("city_id"),
City.City.AS("city_name"),
Customer.CustomerID.AS("customer_id"),
Customer.LastName.AS("last_name"),
Address.AddressID.AS("address_id"),
Address.Address.AS("address_line"),
).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
err := stmt.Query(db, &dest)
```
Aliasing is now simplified. Alias contains only (column/field) name.
On the other hand, we can not have 3 fields named `ID`, because aliases have to be unique.
### Combining autogenerated and custom model files
It is allowed to combine autogenerated and custom model files.
For instance:
```
type MyCustomer struct {
ID int32 `sql:"primary_key"`
LastName string
Address model.Address //model.Address is autogenerated model type
}
type MyCity struct {
ID int32 `sql:"primary_key"`
Name string
Customers []MyCustomer
}
```

View file

@ -9,15 +9,23 @@ Following statements are supported:
_This list might be extended with feature Jet releases._ _This list might be extended with feature Jet releases._
There is a common set of action that can be performed for each statement type: Statements SQL can be debugged in two ways:
- `Sql() (query string, args []interface{}, err error)` - retrieves parametrized sql query with list of arguments - `Sql() (query string, args []interface{}, err error)` - retrieves parametrized sql query with list of arguments
- `DebugSql() (query string, err error)` - retrieves debug query where every parametrized placeholder is replaced with its argument. - `DebugSql() (query string, err error)` - retrieves debug query where every parametrized placeholder is replaced with its argument.
Statements can be executed by following methods:
- `Query(db execution.DB, destination interface{}) error` - executes statements over database connection db and stores row result in destination. - `Query(db execution.DB, destination interface{}) error` - executes statements over database connection db and stores row result in destination.
- `QueryContext(db execution.DB, context context.Context, destination interface{}) error` - executes statement with a context over database connection db and stores row result in destination. - `QueryContext(db execution.DB, context context.Context, destination interface{}) error` - executes statement with a context over database connection db and stores row result in destination.
- `Exec(db execution.DB) (sql.Result, error)` - executes statement over db connection without returning any rows. - `Exec(db execution.DB) (sql.Result, error)` - executes statement over db connection without returning any rows.
- `ExecContext(db execution.DB, context context.Context) (sql.Result, error)` - executes statement with context over db connection without returning any rows. - `ExecContext(db execution.DB, context context.Context) (sql.Result, error)` - executes statement with context over db connection without returning any rows.
Each execution method first creates parametrized sql query with list of arguments and then initiates query on database connection.
Exec and ExecContext are just a wrappers around database `Exec` and `ExecContext`.
`Query` and `QueryContext` are bit more complex, the are wrappers around database `Query` and `QueryContext`,
but they also perform grouping of row result to arbitrary `destination` structure.
Database connection can be of any type that implements following interface: Database connection can be of any type that implements following interface:
```go ```go

View file

@ -10,4 +10,4 @@
* [UPDATE](https://github.com/go-jet/jet/wiki/UPDATE) * [UPDATE](https://github.com/go-jet/jet/wiki/UPDATE)
* [DELETE](https://github.com/go-jet/jet/wiki/DELETE) * [DELETE](https://github.com/go-jet/jet/wiki/DELETE)
* [LOCK](https://github.com/go-jet/jet/wiki/LOCK) * [LOCK](https://github.com/go-jet/jet/wiki/LOCK)
* [Execution](https://github.com/go-jet/jet/wiki/Execution) * [Scan to arbitrary destination](https://github.com/go-jet/jet/wiki/Scan-to-arbitrary-destination.md)