2013-10-25 12:24:29 +04:00
|
|
|
# GORM
|
|
|
|
|
2013-10-26 11:18:44 +04:00
|
|
|
Yet Another ORM library for Go, aims for developer friendly
|
2013-10-25 12:24:29 +04:00
|
|
|
|
2013-10-28 06:09:44 +04:00
|
|
|
## Overview
|
|
|
|
|
|
|
|
* Chainable API
|
2013-11-07 08:12:25 +04:00
|
|
|
* Relations
|
|
|
|
* Callbacks (before/after create/save/update/delete)
|
2013-10-29 07:01:51 +04:00
|
|
|
* Soft Delete
|
2013-11-07 08:12:25 +04:00
|
|
|
* Auto Migration
|
2013-11-11 09:16:08 +04:00
|
|
|
* Transaction
|
2013-11-11 13:50:27 +04:00
|
|
|
* Logger Support
|
2013-11-13 20:03:31 +04:00
|
|
|
* Bind struct with tag
|
2013-11-07 08:12:25 +04:00
|
|
|
* Every feature comes with tests
|
2013-11-03 06:09:56 +04:00
|
|
|
* Convention Over Configuration
|
2013-11-07 08:12:25 +04:00
|
|
|
* Developer Friendly
|
2013-10-28 06:09:44 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
## Conventions
|
|
|
|
|
2013-10-27 17:37:31 +04:00
|
|
|
```go
|
2013-11-03 07:38:53 +04:00
|
|
|
type User struct { // TableName: `users`, gorm will pluralize struct's name as table name
|
2013-11-03 06:49:09 +04:00
|
|
|
Id int64 // Id: Database Primary key
|
|
|
|
Birthday time.Time
|
|
|
|
Age int64
|
2013-11-13 20:03:31 +04:00
|
|
|
Name string `sql:"size:255"` // set this field's length and as not null with tag
|
2013-11-03 06:49:09 +04:00
|
|
|
CreatedAt time.Time // Time of record is created, will be insert automatically
|
|
|
|
UpdatedAt time.Time // Time of record is updated, will be updated automatically
|
2013-11-03 07:32:25 +04:00
|
|
|
DeletedAt time.Time // Time of record is deleted, refer `Soft Delete` for more
|
2013-11-03 06:18:16 +04:00
|
|
|
|
2013-11-14 14:59:11 +04:00
|
|
|
Emails []Email // Embedded structs
|
2013-11-13 20:03:31 +04:00
|
|
|
BillingAddress Address // Embedded struct
|
|
|
|
BillingAddressId sql.NullInt64 // Embedded struct BillingAddress's foreign key
|
|
|
|
ShippingAddress Address // Embedded struct
|
|
|
|
ShippingAddressId int64 // Embedded struct ShippingAddress's foreign key
|
|
|
|
IgnoreMe int64 `sql:"-"` // Ignore this field with tag
|
2013-10-27 17:37:31 +04:00
|
|
|
}
|
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
type Email struct { // TableName: `emails`
|
|
|
|
Id int64
|
|
|
|
UserId int64 // Foreign key for above embedded structs
|
2013-11-13 20:03:31 +04:00
|
|
|
Email string `sql:"type:varchar(100);"` // Set column type directly with tag
|
2013-11-02 17:02:54 +04:00
|
|
|
Subscribed bool
|
|
|
|
}
|
2013-10-28 06:09:44 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
type Address struct { // TableName: `addresses`
|
|
|
|
Id int64
|
2013-11-13 20:03:31 +04:00
|
|
|
Address1 string `sql:"not null;unique"` // Set column as unique with tag
|
|
|
|
Address2 string `sql:"type:varchar(100);unique"`
|
|
|
|
Post sql.NullString `sql:not null`
|
|
|
|
// Be careful: "NOT NULL" will only works for NullXXX scanner, because golang will initalize a default value for most type...
|
2013-11-02 17:02:54 +04:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2013-11-03 07:32:25 +04:00
|
|
|
## Opening a Database
|
|
|
|
|
|
|
|
```go
|
2013-11-03 07:38:53 +04:00
|
|
|
import "github.com/jinzhu/gorm"
|
|
|
|
import _ "github.com/lib/pq"
|
2013-11-04 16:32:46 +04:00
|
|
|
// import _ "github.com/go-sql-driver/mysql"
|
2013-11-04 16:47:45 +04:00
|
|
|
// import _ "github.com/mattn/go-sqlite3"
|
2013-11-03 07:38:53 +04:00
|
|
|
|
|
|
|
db, err := Open("postgres", "user=gorm dbname=gorm sslmode=disable")
|
2013-11-04 16:32:46 +04:00
|
|
|
// db, err = Open("mysql", "gorm:gorm@/gorm?charset=utf8&parseTime=True")
|
2013-11-04 16:47:45 +04:00
|
|
|
// db, err = Open("sqlite3", "/tmp/gorm.db")
|
2013-11-04 16:32:46 +04:00
|
|
|
|
2013-11-03 07:38:53 +04:00
|
|
|
|
|
|
|
// Set the maximum idle database connections
|
|
|
|
db.SetPool(100)
|
|
|
|
|
2013-11-06 18:13:18 +04:00
|
|
|
|
2013-11-06 17:43:41 +04:00
|
|
|
// By default, table name is plural of struct type, if you like singular table name
|
|
|
|
db.SingularTable(true)
|
2013-11-03 07:32:25 +04:00
|
|
|
|
2013-11-06 18:13:18 +04:00
|
|
|
|
2013-11-03 07:32:25 +04:00
|
|
|
// Gorm is goroutines friendly, so you can create a global variable to keep the connection and use it everywhere like this
|
|
|
|
|
|
|
|
var DB gorm.DB
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
DB, err = gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable")
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Sprintf("Got error when connect database, the error is '%v'", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
## Struct & Database Mapping
|
|
|
|
|
|
|
|
```go
|
|
|
|
// Create table from struct
|
|
|
|
db.CreateTable(User{})
|
|
|
|
|
|
|
|
// Drop table
|
|
|
|
db.DropTable(User{})
|
|
|
|
```
|
|
|
|
|
2013-11-07 07:42:36 +04:00
|
|
|
### Automating Migrations
|
|
|
|
|
|
|
|
Feel Free to update your struct, AutoMigrate will keep your database update to date.
|
|
|
|
|
|
|
|
FYI, AutoMigrate will only add new columns, won't change column's type or delete unused columns, to make sure gorm won't harm your data.
|
|
|
|
|
|
|
|
If table doesn't exist when AutoMigrate, it will run create table automatically.
|
|
|
|
|
2013-11-07 08:23:45 +04:00
|
|
|
(only postgres and mysql supported)
|
|
|
|
|
2013-11-07 07:42:36 +04:00
|
|
|
```go
|
|
|
|
db.AutoMigrate(User{})
|
|
|
|
```
|
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
## Create
|
|
|
|
|
|
|
|
```go
|
|
|
|
user := User{Name: "jinzhu", Age: 18, Birthday: time.Now()}
|
2013-10-27 17:37:31 +04:00
|
|
|
db.Save(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
```
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:31:36 +04:00
|
|
|
### Create With SubStruct
|
|
|
|
|
2013-11-05 04:08:42 +04:00
|
|
|
Refer [Query With Related](#query-with-related) to find how to find associations
|
|
|
|
|
2013-11-03 06:31:36 +04:00
|
|
|
```go
|
|
|
|
user := User{
|
|
|
|
Name: "jinzhu",
|
|
|
|
BillingAddress: Address{Address1: "Billing Address - Address 1"},
|
|
|
|
ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
|
2013-11-14 14:59:11 +04:00
|
|
|
Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}},
|
2013-11-03 06:31:36 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
db.Save(&user)
|
|
|
|
//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1");
|
|
|
|
//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1");
|
|
|
|
//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
|
|
|
|
//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com");
|
|
|
|
//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com");
|
|
|
|
```
|
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
## Query
|
|
|
|
|
|
|
|
```go
|
|
|
|
// Get the first record
|
|
|
|
db.First(&user)
|
2013-11-04 13:58:56 +04:00
|
|
|
//// SELECT * FROM users ORDER BY id LIMIT 1;
|
2013-11-03 06:49:09 +04:00
|
|
|
// Search table `users` are guessed from the out struct type.
|
2013-11-03 06:09:56 +04:00
|
|
|
// You are possible to specify the table name with Model() if no out struct for some methods like Pluck()
|
2013-11-03 06:49:09 +04:00
|
|
|
// Or set table name with Table(), if so, it will ignore the out struct even have it. more details following.
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-04 13:58:56 +04:00
|
|
|
// Get the last record
|
|
|
|
db.Last(&user)
|
|
|
|
//// SELECT * FROM users ORDER BY id DESC LIMIT 1;
|
|
|
|
|
|
|
|
// Get a record without order by primary key
|
|
|
|
db.Find(&user)
|
|
|
|
//// SELECT * FROM users LIMIT 1;
|
|
|
|
|
|
|
|
// Get first record as map
|
|
|
|
db.First(&users)
|
|
|
|
//// SELECT * FROM users LIMIT 1;
|
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
// Get All records
|
|
|
|
db.Find(&users)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users;
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
// Using a Primary Key
|
|
|
|
db.First(&user, 10)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users WHERE id = 10;
|
2013-11-02 17:02:54 +04:00
|
|
|
```
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
### Query With Where (SQL like condition)
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
```go
|
|
|
|
// Get the first matched record
|
2013-10-27 17:37:31 +04:00
|
|
|
db.Where("name = ?", "jinzhu").First(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name = 'jinzhu' limit 1;
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
// Get all matched records
|
2013-10-27 17:37:31 +04:00
|
|
|
db.Where("name = ?", "jinzhu").Find(&users)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name = 'jinzhu';
|
2013-10-27 17:37:31 +04:00
|
|
|
|
|
|
|
db.Where("name <> ?", "jinzhu").Find(&users)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name <> 'jinzhu';
|
|
|
|
|
|
|
|
// IN
|
2013-11-03 17:19:38 +04:00
|
|
|
db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name IN ('jinzhu', 'jinzhu 2');
|
|
|
|
|
|
|
|
// LIKE
|
2013-10-28 05:05:44 +04:00
|
|
|
db.Where("name LIKE ?", "%jin%").Find(&users)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name LIKE "%jin%";
|
|
|
|
|
|
|
|
// Multiple Conditions
|
|
|
|
db.Where("name = ? and age >= ?", "jinzhu", "22").Find(&users)
|
|
|
|
//// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
|
|
|
|
```
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
### Query With Where (Struct & Map)
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
```go
|
|
|
|
// Search with struct
|
2013-10-29 13:37:45 +04:00
|
|
|
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;
|
|
|
|
|
|
|
|
// Search with map
|
|
|
|
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
|
|
|
|
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
|
2013-11-03 06:09:56 +04:00
|
|
|
|
|
|
|
// IN For Primary Key
|
|
|
|
db.Where([]int64{20, 21, 22}).Find(&users)
|
|
|
|
//// SELECT * FROM users WHERE id IN (20, 21, 22);
|
2013-11-02 17:02:54 +04:00
|
|
|
```
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
### Query With Not
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
```go
|
2013-11-03 06:09:56 +04:00
|
|
|
// Attribute Not Equal
|
2013-11-02 17:02:54 +04:00
|
|
|
db.Not("name", "jinzhu").First(&user)
|
|
|
|
//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1;
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
// Not In
|
|
|
|
db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users)
|
|
|
|
//// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
|
|
|
|
|
|
|
|
// Not In for Primary Key
|
2013-10-31 14:12:18 +04:00
|
|
|
db.Not([]int64{1,2,3}).First(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE id NOT IN (1,2,3);
|
|
|
|
|
2013-10-31 14:12:18 +04:00
|
|
|
db.Not([]int64{}).First(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users;
|
|
|
|
|
|
|
|
// Normal SQL
|
2013-10-31 18:49:48 +04:00
|
|
|
db.Not("name = ?", "jinzhu").First(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE NOT(name = "jinzhu");
|
|
|
|
|
|
|
|
// Not With Struct
|
2013-10-31 14:12:18 +04:00
|
|
|
db.Not(User{Name: "jinzhu"}).First(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name <> "jinzhu";
|
|
|
|
```
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
### Query With Inline Condition
|
2013-10-31 14:12:18 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
```go
|
|
|
|
// Find with primary key
|
2013-10-27 18:36:43 +04:00
|
|
|
db.First(&user, 23)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE id = 23 LIMIT 1;
|
|
|
|
|
|
|
|
// Normal SQL
|
|
|
|
db.Find(&user, "name = ?", "jinzhu")
|
|
|
|
//// SELECT * FROM users WHERE name = "jinzhu";
|
|
|
|
|
|
|
|
// Multiple Conditions
|
2013-10-27 18:36:43 +04:00
|
|
|
db.Find(&users, "name <> ? and age > ?", "jinzhu", 20)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;
|
|
|
|
|
|
|
|
// Inline Search With Struct
|
|
|
|
db.Find(&users, User{Age: 20})
|
|
|
|
//// SELECT * FROM users WHERE age = 20;
|
|
|
|
|
|
|
|
// Inline Search With Map
|
2013-10-29 13:52:37 +04:00
|
|
|
db.Find(&users, map[string]interface{}{"age": 20})
|
2013-11-02 17:02:54 +04:00
|
|
|
//// SELECT * FROM users WHERE age = 20;
|
|
|
|
```
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
### Query With Or
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
```go
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
|
|
|
|
//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
|
|
|
|
|
|
|
|
// Or With Struct
|
|
|
|
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users)
|
|
|
|
//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
|
|
|
|
|
|
|
|
// Or With Map
|
|
|
|
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users)
|
|
|
|
```
|
|
|
|
|
2013-11-05 03:46:06 +04:00
|
|
|
### Query With Related
|
|
|
|
|
|
|
|
```go
|
2013-11-05 04:08:42 +04:00
|
|
|
// Find emails from user with guessed foreign key
|
2013-11-05 03:46:06 +04:00
|
|
|
db.Model(&user).Related(&emails)
|
|
|
|
//// SELECT * FROM emails WHERE user_id = 111;
|
|
|
|
|
2013-11-05 04:08:42 +04:00
|
|
|
// Find address from user with specified foreign key 'BillingAddressId'
|
2013-11-05 03:46:06 +04:00
|
|
|
db.Model(&user).Related(&address1, "BillingAddressId")
|
|
|
|
//// SELECT * FROM addresses WHERE id = 123; // 123 is the value of user's BillingAddressId
|
|
|
|
|
2013-11-05 04:08:42 +04:00
|
|
|
// Find user from email with guessed primary key value from emails
|
2013-11-05 03:46:06 +04:00
|
|
|
db.Model(&email).Related(&user)
|
|
|
|
//// SELECT * FROM users WHERE id = 111; // 111 is the value of email's UserId
|
|
|
|
```
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
### Query Chains
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
Gorm has a chainable API, so you could query like this
|
2013-11-03 06:09:56 +04:00
|
|
|
|
|
|
|
```go
|
2013-11-03 06:49:09 +04:00
|
|
|
db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(&users)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users WHERE name <> 'jinzhu' AND age >= 20 AND role <> 'admin';
|
|
|
|
|
|
|
|
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users)
|
|
|
|
```
|
|
|
|
|
|
|
|
## Update
|
|
|
|
|
|
|
|
### Update an existing struct
|
|
|
|
|
|
|
|
```go
|
|
|
|
user.Name = "jinzhu 2"
|
|
|
|
user.Age = 100
|
|
|
|
db.Save(&user)
|
|
|
|
//// UPDATE users SET name='jinzhu 2', age=100 WHERE id=111;
|
|
|
|
```
|
|
|
|
|
|
|
|
### Update one attribute with `Update`
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
```go
|
2013-11-03 06:09:56 +04:00
|
|
|
// Update an existing struct's name if name is different
|
|
|
|
db.Model(&user).Update("name", "hello")
|
|
|
|
//// UPDATE users SET name='hello' WHERE id=111;
|
|
|
|
|
|
|
|
// Find out a struct, and update it if name is different
|
|
|
|
db.First(&user, 111).Update("name", "hello")
|
|
|
|
//// SELECT * FROM users LIMIT 1;
|
|
|
|
//// UPDATE users SET name='hello' WHERE id=111;
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
// Specify table name with where search
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Table("users").Where(10).Update("name", "hello")
|
|
|
|
//// UPDATE users SET name='hello' WHERE id = 10;
|
|
|
|
```
|
|
|
|
|
|
|
|
### Update multiple attributes with `Updates`
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
```go
|
|
|
|
// Update an existing record if have any changed values
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Model(&user).Updates(User{Name: "hello", Age: 18})
|
|
|
|
//// UPDATE users SET name='hello', age=18 WHERE id = 111;
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
// Updates with Map
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Table("users").Where(10).Updates(map[string]interface{}{"name": "hello", "age": 18})
|
|
|
|
//// UPDATE users SET name='hello', age=18 WHERE id = 10;
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
// Updates with Struct
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Model(User{}).Updates(User{Name: "hello", Age: 18})
|
|
|
|
//// UPDATE users SET name='hello', age=18;
|
|
|
|
```
|
|
|
|
|
|
|
|
## Delete
|
|
|
|
|
|
|
|
### Delete an existing struct
|
|
|
|
|
|
|
|
```go
|
|
|
|
db.Delete(&email)
|
|
|
|
// DELETE from emails where id=10;
|
|
|
|
```
|
|
|
|
|
|
|
|
### Batch Delete with search
|
|
|
|
|
|
|
|
```go
|
|
|
|
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
|
|
|
|
// DELETE from emails where email LIKE "%jinhu%";
|
|
|
|
```
|
|
|
|
|
|
|
|
### Soft Delete
|
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
If a struct has DeletedAt field, it will get soft delete ability automatically!
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
For those don't have the filed, will be deleted from database permanently
|
|
|
|
|
|
|
|
```go
|
|
|
|
db.Delete(&user)
|
|
|
|
//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
|
|
|
|
|
|
|
|
// Batch delete when search
|
|
|
|
db.Where("age = ?", 20).Delete(&User{})
|
|
|
|
//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
|
|
|
|
|
|
|
|
// For structs have DeletedAt field, when do query, will ignore deleted records by default
|
|
|
|
db.Where("age = 20").Find(&user)
|
|
|
|
//// SELECT * FROM users WHERE age = 100 AND (deleted_at IS NULL AND deleted_at <= '0001-01-02');
|
|
|
|
|
|
|
|
// Find out all records including those deleted with Unscoped
|
|
|
|
db.Unscoped().Where("age = 20").Find(&users)
|
|
|
|
//// SELECT * FROM users WHERE age = 20;
|
|
|
|
|
|
|
|
// Permanently delete a record with Unscoped
|
|
|
|
db.Unscoped().Delete(&order)
|
|
|
|
// DELETE FROM orders WHERE id=10;
|
|
|
|
```
|
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
## FirstOrInit
|
|
|
|
|
|
|
|
Try to load the first record, if fails, initialize struct with search conditions.
|
2013-11-03 06:49:09 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
(only support map or struct conditions, SQL like conditions are not supported)
|
|
|
|
|
|
|
|
```go
|
|
|
|
db.FirstOrInit(&user, User{Name: "non_existing"})
|
|
|
|
//// User{Name: "non_existing"}
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-10-29 16:32:27 +04:00
|
|
|
db.Where(User{Name: "Jinzhu"}).FirstOrInit(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// User{Id: 111, Name: "Jinzhu", Age: 20}
|
2013-10-29 16:32:27 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
|
|
|
|
//// User{Id: 111, Name: "Jinzhu", Age: 20}
|
|
|
|
```
|
|
|
|
|
|
|
|
### FirstOrInit With Attrs
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
Attr's arguments would be used to initialize struct if no record found, but won't be used for search
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
```go
|
|
|
|
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
|
|
|
|
//// SELECT * FROM USERS WHERE name = 'non_existing';
|
|
|
|
//// User{Name: "non_existing", Age: 20}
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// Above code could be simplified if has only one attribute
|
2013-10-31 08:59:04 +04:00
|
|
|
db.Where(User{Name: "noexisting_user"}).Attrs("age", 20).FirstOrInit(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// If a record found, Attrs would be just ignored
|
2013-11-02 17:02:54 +04:00
|
|
|
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user)
|
|
|
|
//// SELECT * FROM USERS WHERE name = jinzhu';
|
2013-11-03 06:09:56 +04:00
|
|
|
//// User{Id: 111, Name: "Jinzhu", Age: 20}
|
2013-11-03 06:18:16 +04:00
|
|
|
```
|
2013-10-30 11:33:34 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
### FirstOrInit With Assign
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
Assign's arguments would be used to set the struct even a record found, but won't be used for search
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
```go
|
|
|
|
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user)
|
|
|
|
//// User{Name: "non_existing", Age: 20}
|
|
|
|
|
|
|
|
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&user)
|
|
|
|
//// User{Id: 111, Name: "Jinzhu", Age: 30}
|
|
|
|
```
|
|
|
|
|
|
|
|
## FirstOrCreate
|
|
|
|
|
|
|
|
Try to load the first record, if fails, initialize struct with search conditions and save it
|
|
|
|
|
|
|
|
```go
|
|
|
|
db.FirstOrCreate(&user, User{Name: "non_existing"})
|
|
|
|
//// User{Id: 112, Name: "non_existing"}
|
|
|
|
|
2013-10-29 16:32:27 +04:00
|
|
|
db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user)
|
2013-11-02 17:02:54 +04:00
|
|
|
//// User{Id: 111, Name: "Jinzhu"}
|
|
|
|
|
|
|
|
db.FirstOrCreate(&user, map[string]interface{}{"name": "jinzhu", "age": 30})
|
2013-11-03 06:09:56 +04:00
|
|
|
//// user -> User{Id: 111, Name: "jinzhu", Age: 20}
|
2013-11-02 17:02:54 +04:00
|
|
|
```
|
|
|
|
|
|
|
|
### FirstOrCreate With Attrs
|
2013-10-29 16:32:27 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
Attr's arguments would be used to initialize struct if no record found, but won't be used for search
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
```go
|
|
|
|
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
|
|
|
|
//// SELECT * FROM users WHERE name = 'non_existing';
|
|
|
|
//// User{Id: 112, Name: "non_existing", Age: 20}
|
2013-10-31 05:34:27 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user)
|
|
|
|
//// User{Id: 111, Name: "jinzhu", Age: 20}
|
2013-11-02 17:02:54 +04:00
|
|
|
```
|
|
|
|
|
|
|
|
### FirstOrCreate With Assign
|
2013-10-31 05:34:27 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
Assign's arguments would be used to initialize the struct if not record found,
|
2013-11-03 06:49:09 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
If any record found, will assign those values to the record, and save it back to database.
|
2013-10-31 05:34:27 +04:00
|
|
|
|
2013-11-02 17:02:54 +04:00
|
|
|
```go
|
|
|
|
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
|
|
|
|
//// user -> User{Id: 112, Name: "non_existing", Age: 20}
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user)
|
|
|
|
//// SELECT * FROM users WHERE name = 'jinzhu';
|
2013-11-02 17:02:54 +04:00
|
|
|
//// UPDATE users SET age=30 WHERE id = 111;
|
2013-11-03 06:09:56 +04:00
|
|
|
//// User{Id: 111, Name: "jinzhu", Age: 30}
|
2013-11-02 17:02:54 +04:00
|
|
|
```
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Select
|
2013-11-02 17:02:54 +04:00
|
|
|
|
|
|
|
```go
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Select("name, age").Find(&users)
|
|
|
|
//// SELECT name, age FROM users;
|
|
|
|
```
|
2013-10-30 11:33:34 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Order
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
```go
|
2013-10-27 17:37:31 +04:00
|
|
|
db.Order("age desc, name").Find(&users)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users ORDER BY age desc, name;
|
|
|
|
|
2013-10-27 18:18:06 +04:00
|
|
|
db.Order("age desc").Order("name").Find(&users)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users ORDER BY age desc, name;
|
|
|
|
|
2013-10-28 05:18:34 +04:00
|
|
|
// ReOrder
|
|
|
|
db.Order("age desc").Find(&users1).Order("age", true).Find(&users2)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users ORDER BY age desc; (users1)
|
|
|
|
//// SELECT * FROM users ORDER BY age; (users2)
|
|
|
|
```
|
2013-10-28 05:18:34 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Limit
|
|
|
|
|
|
|
|
```go
|
2013-10-27 17:37:31 +04:00
|
|
|
db.Limit(3).Find(&users)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users LIMIT 3;
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// Cleanup limit with -1
|
|
|
|
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
|
|
|
|
//// SELECT * FROM users LIMIT 10; (users1)
|
|
|
|
//// SELECT * FROM users; (users2)
|
|
|
|
```
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Offset
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
```go
|
|
|
|
db.Offset(3).Find(&users)
|
|
|
|
//// SELECT * FROM users OFFSET 3;
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// Cleanup offset with -1
|
|
|
|
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
|
|
|
|
//// SELECT * FROM users OFFSET 10; (users1)
|
|
|
|
//// SELECT * FROM users; (users2)
|
|
|
|
```
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Count
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
```go
|
|
|
|
db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count)
|
|
|
|
//// SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users)
|
|
|
|
//// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count)
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// Set table name with Model
|
|
|
|
db.Model(User{}).Where("name = ?", "jinzhu").Count(&count)
|
|
|
|
//// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count)
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// Set table name with Table
|
|
|
|
db.Table("deleted_users").Count(&count)
|
|
|
|
//// SELECT count(*) FROM deleted_users;
|
|
|
|
```
|
|
|
|
|
|
|
|
## Pluck
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
Get struct's attribute as map
|
|
|
|
|
|
|
|
```go
|
2013-10-27 17:37:31 +04:00
|
|
|
var ages []int64
|
2013-10-28 05:05:44 +04:00
|
|
|
db.Find(&users).Pluck("age", &ages)
|
2013-11-03 06:09:56 +04:00
|
|
|
|
|
|
|
// Set Table With Model
|
2013-10-27 18:18:06 +04:00
|
|
|
var names []string
|
|
|
|
db.Model(&User{}).Pluck("name", &names)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT name FROM users;
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// Set Table With Table
|
|
|
|
db.Table("deleted_users").Pluck("name", &names)
|
|
|
|
//// SELECT name FROM deleted_users;
|
2013-11-11 17:55:44 +04:00
|
|
|
|
|
|
|
// Pluck more than one column? Do it like this
|
|
|
|
db.Select("name, age").Find(&users)
|
2013-11-03 06:09:56 +04:00
|
|
|
```
|
|
|
|
|
|
|
|
## Callbacks
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
Callback is a function defined to a struct, the function would be run when reflect a struct to database.
|
2013-11-11 15:06:26 +04:00
|
|
|
If a function return error, gorm will prevent future operations and do rollback
|
2013-11-03 06:09:56 +04:00
|
|
|
|
|
|
|
Those callbacks are defined now:
|
|
|
|
|
|
|
|
`BeforeCreate`, `AfterCreate`
|
|
|
|
`BeforeUpdate`, `AfterUpdate`
|
|
|
|
`BeforeSave`, `AfterSave`
|
|
|
|
`BeforeDelete`, `AfterDelete`
|
|
|
|
|
|
|
|
```go
|
2013-11-11 15:06:26 +04:00
|
|
|
// Won't update readonly user
|
2013-11-03 06:09:56 +04:00
|
|
|
func (u *User) BeforeUpdate() (err error) {
|
|
|
|
if u.readonly() {
|
|
|
|
err = errors.New("Read Only User")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2013-11-11 15:06:26 +04:00
|
|
|
|
|
|
|
// If have more than 1000 users, will rollback the insertion
|
|
|
|
func (u *User) AfterCreate() (err error) {
|
|
|
|
if (u.Id > 1000) { // just an example, don't use Id to count users
|
|
|
|
err = errors.New("Only 1000 users allowed")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2013-11-03 06:09:56 +04:00
|
|
|
```
|
2013-10-27 17:37:31 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Specify Table Name
|
2013-11-01 11:01:39 +04:00
|
|
|
|
2013-11-03 07:32:25 +04:00
|
|
|
```go
|
2013-11-03 06:09:56 +04:00
|
|
|
// When Create Table from struct
|
2013-10-28 16:27:25 +04:00
|
|
|
db.Table("deleted_users").CreateTable(&User{})
|
2013-11-03 06:09:56 +04:00
|
|
|
|
|
|
|
// When Pluck
|
2013-10-28 16:27:25 +04:00
|
|
|
db.Table("users").Pluck("age", &ages)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT age FROM users;
|
|
|
|
|
|
|
|
// When Query
|
2013-10-28 16:27:25 +04:00
|
|
|
var deleted_users []User
|
|
|
|
db.Table("deleted_users").Find(&deleted_users)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM deleted_users;
|
2013-10-28 16:27:25 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
// When Delete
|
|
|
|
db.Table("deleted_users").Where("name = ?", "jinzhu").Delete()
|
|
|
|
//// DELETE FROM deleted_users WHERE name = 'jinzhu';
|
|
|
|
```
|
2013-10-28 17:52:22 +04:00
|
|
|
|
2013-11-06 18:13:18 +04:00
|
|
|
### Specify Table Name for Struct
|
|
|
|
|
|
|
|
You are possible to specify table name for a struct by defining method TableName
|
|
|
|
|
|
|
|
```go
|
|
|
|
type Cart struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Cart) TableName() string {
|
|
|
|
return "shopping_cart"
|
|
|
|
}
|
2013-11-06 18:20:26 +04:00
|
|
|
|
|
|
|
func (u User) TableName() string {
|
|
|
|
if u.Role == "admin" {
|
|
|
|
return "admin_users"
|
|
|
|
} else {
|
|
|
|
return "users"
|
|
|
|
}
|
|
|
|
}
|
2013-11-06 18:13:18 +04:00
|
|
|
```
|
|
|
|
|
2013-11-11 09:16:08 +04:00
|
|
|
## Transaction
|
|
|
|
|
|
|
|
```go
|
|
|
|
tx := db.Begin()
|
|
|
|
|
|
|
|
user := User{Name: "transcation"}
|
|
|
|
|
|
|
|
tx.Save(&u)
|
|
|
|
tx.Update("age": 90)
|
|
|
|
// do whatever
|
|
|
|
|
|
|
|
// rollback
|
|
|
|
tx.Rollback()
|
|
|
|
|
|
|
|
// commit
|
|
|
|
tx.Commit()
|
|
|
|
```
|
|
|
|
|
2013-11-11 13:50:27 +04:00
|
|
|
## Logger
|
|
|
|
|
|
|
|
Grom has builtin logger support, enable it with:
|
|
|
|
|
|
|
|
```go
|
|
|
|
db.LogMode(true)
|
|
|
|
```
|
|
|
|
|
2013-11-11 13:51:39 +04:00
|
|
|
![logger](https://raw.github.com/jinzhu/gorm/master/images/logger.png)
|
2013-11-11 13:50:27 +04:00
|
|
|
|
|
|
|
```go
|
|
|
|
// Use your own logger
|
|
|
|
// Checkout gorm's default logger for how to format messages: https://github.com/jinzhu/gorm/blob/master/logger.go#files
|
|
|
|
db.SetLogger(log.New(os.Stdout, "\r\n", 0))
|
|
|
|
|
|
|
|
// Disable log
|
|
|
|
db.LogMode(false)
|
|
|
|
|
|
|
|
// Enable log for a single operation, make debug easy
|
|
|
|
db.Debug().Where("name = ?", "jinzhu").First(&User{})
|
|
|
|
```
|
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Run Raw SQl
|
2013-10-29 07:01:51 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
```go
|
2013-10-27 17:37:31 +04:00
|
|
|
db.Exec("drop table users;")
|
2013-11-03 06:09:56 +04:00
|
|
|
```
|
2013-10-28 07:24:51 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
## Error Handling
|
|
|
|
|
|
|
|
```go
|
2013-10-28 07:24:51 +04:00
|
|
|
query := db.Where("name = ?", "jinzhu").First(&user)
|
|
|
|
query := db.First(&user).Limit(10).Find(&users)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// query.Error keep the latest error happened
|
|
|
|
//// query.Errors keep all errors happened
|
|
|
|
//// If an error happened, gorm will stop do query, insert, update, delete
|
|
|
|
|
|
|
|
// I often use below code to do error handling in real applicatoins
|
|
|
|
err = db.Where("name = ?", "jinzhu").First(&user).Error
|
2013-10-27 17:37:31 +04:00
|
|
|
```
|
|
|
|
|
2013-10-28 04:08:45 +04:00
|
|
|
## Advanced Usage With Query Chain
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
Already excited about above usage? Let's see some magic!
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
```go
|
2013-10-27 18:18:06 +04:00
|
|
|
db.First(&first_article).Count(&total_count).Limit(10).Find(&first_page_articles).Offset(10).Find(&second_page_articles)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM articles LIMIT 1; (first_article)
|
|
|
|
//// SELECT count(*) FROM articles; (count)
|
|
|
|
//// SELECT * FROM articles LIMIT 10; (first_page_articles)
|
|
|
|
//// SELECT * FROM articles LIMIT 10 OFFSET 10; (second_page_articles)
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Where("created_at > ?", "2013-10-10").Find(&cancelled_orders, "state = ?", "cancelled").Find(&shipped_orders, "state = ?", "shipped")
|
|
|
|
//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'cancelled'; (cancelled_orders)
|
|
|
|
//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'shipped'; (shipped_orders)
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
db.Where("product_name = ?", "fancy_product").Find(&orders).Find(&shopping_carts)
|
|
|
|
//// SELECT * FROM orders WHERE product_name = 'fancy_product'; (orders)
|
|
|
|
//// SELECT * FROM carts WHERE product_name = 'fancy_product'; (shopping_carts)
|
|
|
|
// Do you noticed the table is different?
|
2013-10-27 18:18:06 +04:00
|
|
|
|
2013-10-28 16:27:25 +04:00
|
|
|
db.Where("mail_type = ?", "TEXT").Find(&users1).Table("deleted_users").First(&user2)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users WHERE mail_type = 'TEXT'; (users1)
|
|
|
|
//// SELECT * FROM deleted_users WHERE mail_type = 'TEXT'; (users2)
|
2013-10-28 16:27:25 +04:00
|
|
|
|
2013-11-03 06:49:09 +04:00
|
|
|
db.Where("email = ?", "x@example.org").Attrs(User{FromIp: "111.111.111.111"}).FirstOrCreate(&user)
|
2013-11-03 06:09:56 +04:00
|
|
|
//// SELECT * FROM users WHERE email = 'x@example.org';
|
|
|
|
//// INSERT INTO "users" (email,from_ip) VALUES ("x@example.org", "111.111.111.111") (if no record found)
|
2013-10-30 11:33:34 +04:00
|
|
|
|
2013-10-27 18:18:06 +04:00
|
|
|
// Open your mind, add more cool examples
|
|
|
|
```
|
|
|
|
|
2013-10-26 11:18:44 +04:00
|
|
|
## TODO
|
2013-11-11 17:55:44 +04:00
|
|
|
* Join, Having, Group, Includes
|
2013-11-13 20:03:31 +04:00
|
|
|
* Scopes, Valiations
|
|
|
|
* AlertColumn, DropColumn, AddIndex, RemoveIndex
|
2013-10-25 12:24:29 +04:00
|
|
|
|
2013-10-26 11:18:44 +04:00
|
|
|
# Author
|
2013-10-25 12:24:29 +04:00
|
|
|
|
2013-11-03 06:09:56 +04:00
|
|
|
**jinzhu**
|
2013-10-25 12:24:29 +04:00
|
|
|
|
2013-10-26 11:18:44 +04:00
|
|
|
* <http://github.com/jinzhu>
|
|
|
|
* <wosmvp@gmail.com>
|
|
|
|
* <http://twitter.com/zhangjinzhu>
|
2013-11-12 03:13:58 +04:00
|
|
|
|
|
|
|
## License
|
|
|
|
|
|
|
|
Released under the [MIT License](http://www.opensource.org/licenses/MIT).
|