diff --git a/README.md b/README.md index 28b8d08b..96cbc91b 100644 --- a/README.md +++ b/README.md @@ -6,167 +6,306 @@ Yet Another ORM library for Go, aims for developer friendly * CURD * Chainable API +* Embedded structs support * Before/After Create/Save/Update/Delete Callbacks -* Order/Select/Limit/Offset Support * Update, Updates Like Rails's update_attribute, update_attributes * FirstOrInit, FirstOrCreate Like Rails's first_or_initialize, first_or_create -* Dynamically set table name when search, update, delete... +* Order/Select/Limit/Offset Support * Automatically CreatedAt, UpdatedAt * Soft Delete -* Create table from struct +* Create/Drop table from struct +* Dynamically set table name when search, create, update, delete... * Prevent SQL Injection * Goroutines friendly * Database Pool +* Convention Over Configuration (CoC) ## Basic Usage -```go -db, _ = Open("postgres", "user=gorm dbname=gorm sslmode=disable") +## Opening a Database -type User struct { - Id int64 - Age int64 - Birthday time.Time - Name string - CreatedAt time.Time - UpdatedAt time.Time +```go +db, err = Open("postgres", "user=gorm dbname=gorm sslmode=disable") + +// Gorm is goroutines friendly, so you can create a global variable to keep the connection and use it everywhere + +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)) + } } -// Set database pool -db.SetPool(10) +// Set the maximum idle database connections +db.SetPool(100) +``` -// Create -user = User{Name: "jinzhu", Age: 18, Birthday: time.Now()} +## Conventions + +```go +type User struct { // TableName: `users`, gorm will pluralize struct name as table name + Id int64 // Id: Database Primary key + Birthday time.Time // Time + Age int64 + Name string + CreatedAt time.Time // CreatedAt: Time of record is created, will be insert automatically + UpdatedAt time.Time // UpdatedAt: Time of record is updated, will be updated automatically + DeletedAt time.Time // DeletedAt: Time of record is deleted, refer Soft Delete for more + Email []Email // Embedded structs + BillingAddress Address // Embedded struct + BillingAddressId int64 // Embedded struct's foreign key + ShippingAddress Address // Embedded struct + ShippingAddressId int64 // Embedded struct's foreign key +} + +type Email struct { // TableName: `emails` + Id int64 + UserId int64 // Foreign key for above embedded structs + Email string + Subscribed bool +} + +type Address struct { // TableName: `addresses` + Id int64 + Address1 string + Address2 string + Post string +} +``` + +## Struct & Database Mapping + +```go +// Create table from struct +db.CreateTable(User{}) + +// Drop table +db.DropTable(User{}) +``` + +## Create + +```go +user := User{Name: "jinzhu", Age: 18, Birthday: time.Now()} db.Save(&user) +``` -// Update +## Update + +```go user.Name = "jinzhu 2" db.Save(&user) +``` -// Delete +## Delete + +```go db.Delete(&user) +``` -// Get First matched record +## Query + +```go +// Get the first record +db.First(&user) +//// SELECT * FROM USERS LIMIT 1; + +// Get All records +db.Find(&users) +//// SELECT * FROM USERS; + +// Using a Primary Key +db.First(&user, 10) +//// SELECT * FROM USERS WHERE id = 10; +``` + +### Where (SQL like condition) + +```go +// Get the first matched record db.Where("name = ?", "jinzhu").First(&user) +//// SELECT * FROM users WHERE name = 'jinzhu' limit 1; -// Get All matched records +// Get all matched records db.Where("name = ?", "jinzhu").Find(&users) +//// SELECT * FROM users WHERE name = 'jinzhu'; -// Advanced Where Usage db.Where("name <> ?", "jinzhu").Find(&users) -//// users -> select * from users name <> 'jinzhu'; -db.Where(20).First(&user) -//// users -> select * from users where id = 20; -db.Where([]int64{20, 21, 22}).Find(&user) -//// users -> select * from users where id in (20, 21, 22); -db.Where("name = ? and age >= ?", "jinzhu", "22").Find(&users) -//// users -> select * from users name = 'jinzhu' and age >= 22; +//// SELECT * FROM users WHERE name <> 'jinzhu'; + +// IN db.Where("name in (?)", []string["jinzhu", "jinzhu 2"]).Find(&users) -//// users -> select * from users name in ('jinzhu', 'jinzhu 2'); +//// SELECT * FROM users WHERE name IN ('jinzhu', 'jinzhu 2'); + +// IN For Primary Key +db.Where([]int64{20, 21, 22}).Find(&users) +//// SELECT * FROM users WHERE id IN (20, 21, 22); + +// LIKE db.Where("name LIKE ?", "%jin%").Find(&users) -//// users -> select * from users name LIKE "%jinzhu%"; +//// 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; +``` + +### Where (Struct & Map) + +```go +// Search with struct db.Where(&User{Name: "jinzhu", Age: 20}).First(&user) -//// user -> select * from users name = "jinzhu" and age = 20 limit 1; -db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).First(&user) -//// user -> select * from users name = "jinzhu" and age = 20 limit 1; -db.Where("birthday < ?", time.Now()).Find(&users) +//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1; -// Not -db.Not([]int64{1,2,3}).First(&user) -//// user -> select * from users where id NOT IN (1,2,3) -db.Not([]int64{}).First(&user) -//// user -> select * from users; +// Search with map +db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users) +//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20; +``` + +### Not + +```go +// Not Equal db.Not("name", "jinzhu").First(&user) -//// user -> select * from users where name <> "jinzhu" +//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1; + +// 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 +db.Not([]int64{1,2,3}).First(&user) +//// SELECT * FROM users WHERE id NOT IN (1,2,3); + +db.Not([]int64{}).First(&user) +//// SELECT * FROM users; + +// Normal SQL db.Not("name = ?", "jinzhu").First(&user) -//// user -> select * from users where NOT(name = "jinzhu") -db.Not("name <> ?", "jinzhu").First(&user) -//// user -> select * from users where NOT(name <> "jinzhu") -db.Not("name", []string{"jinzhu", "jinzhu 2"}).First(&user) -//// user -> select * from users where name NOT IN ("jinzhu", "jinzhu 2") +//// SELECT * FROM users WHERE NOT(name = "jinzhu"); + +// Not With Struct db.Not(User{Name: "jinzhu"}).First(&user) -//// user -> select * from users where name <> "jinzhu"; +//// SELECT * FROM users WHERE name <> "jinzhu"; +``` -// Inline search condition +### Inline Search Condition + +```go +// Find with primary key db.First(&user, 23) -//// user -> select * from users where id = 23 limit 1; -db.First(&user, "name = ?", "jinzhu") -//// user -> select * from users where name = "jinzhu" limit 1; -db.Find(&users, "name = ?", "jinzhu") -//// users -> select * from users where name = "jinzhu"; +//// SELECT * FROM users WHERE id = 23 LIMIT 1; + +// Normal SQL +db.Find(&user, "name = ?", "jinzhu") +//// SELECT * FROM users WHERE name = "jinzhu"; + +// Multiple Conditions db.Find(&users, "name <> ? and age > ?", "jinzhu", 20) -//// users -> select * from users where name <> "jinzhu" and age > 20; -db.Find(&users, &User{Age: 20}) -//// users -> select * from users where age = 20; +//// 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 db.Find(&users, map[string]interface{}{"age": 20}) -//// users -> select * from users where age = 20; +//// SELECT * FROM users WHERE age = 20; +``` + +## FirstOrInit + +Try to load the first record, if fails, initialize struct with search conditions. +(only support map or struct conditions, SQL like conditions are not supported) + +```go +db.FirstOrInit(&user, User{Name: "non_existing"}) +//// User{Name: "non_existing"} -// FirstOrInit -db.FirstOrInit(&user, User{Name: "noexisting_user"}) -//// user -> User{Name: "noexisting_user"} db.Where(User{Name: "Jinzhu"}).FirstOrInit(&user) -//// user -> User{Id: 111, Name: "Jinzhu"} -db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu", "age": 20}) -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +//// User{Id: 111, Name: "Jinzhu", Age: 20} -// FirstOrInit With Attrs -db.Where(User{Name: "noexisting_user"}).Attrs(User{Age: 20}).FirstOrInit(&user) -//// user -> select * from users where name = 'noexisting_user'; -//// If no record found, will assign the attrs to user, so user become: -//// User{Name: "noexisting_user", Age: 20} +db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"}) +//// User{Id: 111, Name: "Jinzhu", Age: 20} +``` + +### FirstOrInit With Attrs + +Attr's arguments would be used to initialize struct if not record found, but won't be used for search + +```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} + +// Above code could be simplified if have only one attribute db.Where(User{Name: "noexisting_user"}).Attrs("age", 20).FirstOrInit(&user) -// Same as above -db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrInit(&user) -//// user -> select * from users where name = 'jinzhu'; -//// If found the user, will ingore the attrs: -//// User{Id: 111, Name: "Jinzhu", Age: 18} -// FirstOrInit With Assign -db.Where(User{Name: "noexisting_user"}).Assign(User{Age: 20}).FirstOrInit(&user) -//// user -> select * from users where name = 'noexisting_user'; -//// If no record found, will assign the value to user, so user become: -//// User{Name: "noexisting_user", Age: 20} (same as FirstOrInit With Attrs) -db.Where(User{Name: "noexisting_user"}).Assign("age", 20).FirstOrInit(&user) -// Same as above -//// user -> User{Name: "noexisting_user", Age: 20} -db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrInit(&user) -//// user -> select * from users where name = 'jinzhu'; -//// If found the user, will assign the value to user, so user become: (different with FirstOrInit With Attrs) +// If record found, Attrs would be ignored +db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = jinzhu'; //// User{Id: 111, Name: "Jinzhu", Age: 20} -// FirstOrCreate -db.FirstOrCreate(&user, User{Name: "noexisting_user"}) -//// user -> User{Id: 112, Name: "noexisting_user"} +### FirstOrInit With Assign + +Assign's arguments would be used to set the struct even record found, but won't be used for search + +```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"} + db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user) -//// user -> User{Id: 111, Name: "Jinzhu"} -db.FirstOrCreate(&user, map[string]interface{}{"name": "jinzhu", "age": 20}) -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +//// User{Id: 111, Name: "Jinzhu"} +db.FirstOrCreate(&user, map[string]interface{}{"name": "jinzhu", "age": 30}) +//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +``` + +### FirstOrCreate With Attrs + +Attr's arguments would be used to initialize struct if not record found, but won't be used for search + +```go // FirstOrCreate With Attrs -db.Where(User{Name: "noexisting_user"}).Attrs(User{Age: 20}).FirstOrCreate(&user) -//// user -> select * from users where name = 'noexisting_user'; -//// If not record found, will assing the attrs to the user first, then create it -//// Same as db.Where(User{Name: "noexisting_user"}).FirstOrCreate(&user).Update("age": 20), but one less sql -db.Where(User{Name: "noexisting_user"}).Attrs("age", 20).FirstOrCreate(&user) -// Save as above -//// user -> User{Id: 112, Name: "noexisting_user", Age: 20} -db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrCreate(&user) -//// user -> select * from users where name = 'jinzhu'; -//// If found any record, will ignore the attrs -//// user -> User{Id: 111, Name: "Jinzhu", Age: 18} +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} -// FirstOrCreate With Assign -db.Where(User{Name: "noexisting_user"}).Assign(User{Age: 20}).FirstOrCreate(&user) -//// user -> select * from users where name = 'noexisting_user'; -//// If not record found, will assing the value to the user first, then create it -//// user -> User{Id: 112, Name: "noexisting_user", Age: 20} (Same as FirstOrCreate With Attrs) -db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrCreate(&user) -//// user -> select * from users where name = 'jinzhu'; -//// If any record found, will assing the value to the user and update it -//// UPDATE users SET age=20 WHERE id = 111; -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user) +//// User{Id: 111, Name: "Jinzhu", Age: 20} +``` +### FirstOrCreate With Assign +Assign's arguments would be used to initialize the struct if not record found, +If any record found, will assign those values to the record, and save it back to database. + +```go +db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user) +//// user -> User{Id: 112, Name: "non_existing", Age: 20} + +db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user) +//// UPDATE users SET age=30 WHERE id = 111; +//// User{Id: 111, Name: "Jinzhu", Age: 30} +``` + +### SELECT + +```go //// user -> User{Id: 111, Name: "Jinzhu", Age: 18} //// You must noticed that the Attrs is similar to FirstOrInit with Attrs, yes? @@ -227,10 +366,10 @@ Below callbacks are defined now: Callbacks is a function defined to a model, if the function return error, will prevent the database operations. func (u *User) BeforeUpdate() (err error) { - if u.readonly() { - err = errors.New("Read Only User") - } - return +if u.readonly() { +err = errors.New("Read Only User") +} +return } // Pluck (get users's age as map) @@ -282,11 +421,11 @@ db.First(&user, 20).Updates(User{Name: "hello", Age: 18}) // Soft Delete // For those struct have DeletedAt field, they will get soft delete ability automatically! type Order struct { - Id int64 - Amount int64 - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time +Id int64 +Amount int64 +CreatedAt time.Time +UpdatedAt time.Time +DeletedAt time.Time } order := order{Id:10} db.Delete(&order) @@ -350,7 +489,6 @@ db.Where("email = ?", "x@example.org"').Attrs(User{FromIp: "111.111.111.111"}).F ``` ## TODO -* SubStruct * Index, Unique, Valiations * Auto Migration * SQL Log diff --git a/gorm_test.go b/gorm_test.go index de9d8dc5..7e76c722 100644 --- a/gorm_test.go +++ b/gorm_test.go @@ -12,12 +12,32 @@ import ( ) type User struct { - Id int64 - Age int64 - Birthday time.Time - Name string - CreatedAt time.Time - UpdatedAt time.Time + Id int64 // Id: Primary key + Birthday time.Time // Time + Age int64 + Name string + CreatedAt time.Time // CreatedAt: Time of record is created, will be insert automatically + UpdatedAt time.Time // UpdatedAt: Time of record is updated, will be updated automatically + DeletedAt time.Time // DeletedAt: Time of record is deleted, refer Soft Delete for more + Email []Email // Embedded structs + BillingAddress Address // Embedded struct + BillingAddressId int64 // Embedded struct's foreign key + ShippingAddress Address // Embedded struct + ShippingAddressId int64 // Embedded struct's foreign key +} + +type Email struct { + Id int64 + UserId int64 // Foreign key for above embedded structs + Email string + Subscribed bool +} + +type Address struct { + Id int64 + Address1 string + Address2 string + Post string } type Product struct { @@ -1002,12 +1022,14 @@ type Category struct { } type Post struct { - Id int64 - CategoryId int64 - Title string - Body string - Comments []Comment - Category Category + Id int64 + CategoryId int64 + MainCategoryId int64 + Title string + Body string + Comments []Comment + Category Category + MainCategory Category } type Comment struct { @@ -1027,10 +1049,11 @@ func TestSubStruct(t *testing.T) { db.CreateTable(Comment{}) post := Post{ - Title: "post 1", - Body: "body 1", - Comments: []Comment{{Content: "Comment 1"}, {Content: "Comment 2"}}, - Category: Category{Name: "Category 1"}, + Title: "post 1", + Body: "body 1", + Comments: []Comment{{Content: "Comment 1"}, {Content: "Comment 2"}}, + Category: Category{Name: "Category 1"}, + MainCategory: Category{Name: "Main Category 1"}, } if err := db.Save(&post).Error; err != nil { @@ -1043,7 +1066,7 @@ func TestSubStruct(t *testing.T) { var p Post db.First(&p, post.Id) - if post.CategoryId == 0 || p.CategoryId == 0 { + if post.CategoryId == 0 || p.CategoryId == 0 || post.MainCategoryId == 0 || p.MainCategoryId == 0 { t.Errorf("Category Id should exist") } @@ -1066,3 +1089,16 @@ func TestSubStruct(t *testing.T) { comment3 := Comment{Content: "Comment 3", Post: Post{Title: "Title 3", Body: "Body 3"}} db.Save(&comment3) } + +func TestT(t *testing.T) { + user := User{Name: "jinzhu", Age: 18, Birthday: time.Now()} + db.Save(&user) + + var user2 User + db.Where("name in (?)", []string{"1,2"}).First(&user2).Update("name", "hhh") + debug(user2) + + var users []User + db.Find(&users, User{Age: 20}) + debug(users) +}