How to access Gorm in Revel Controller?
Asked Answered
N

4

5

let me start by saying these are my first couple days of toying around in Go.

I'm trying to use the Revel framework with Gorm like this:

app/controllers/gorm.go

package controllers

import (
    "fmt"
    "go-testapp/app/models"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "github.com/revel/revel"
)

var DB gorm.DB

func InitDB() {
    var err error
    DB, err = gorm.Open("mysql", "root:@/go-testapp?charset=utf8&parseTime=True")
    if err != nil {
        panic(err)
    }
    DB.LogMode(true)
    DB.AutoMigrate(models.User{})
}

type GormController struct {
    *revel.Controller
    DB *gorm.DB
}

app/controller/app.go

package controllers

import (
    "fmt"
    "go-bingo/app/models"

    _ "github.com/go-sql-driver/mysql"
    "github.com/revel/revel"
)

type App struct {
    GormController
}

func (c App) Index() revel.Result {
    user := models.User{Name: "Jinzhu", Age: 18}

    fmt.Println(c.DB)
    c.DB.NewRecord(user)

    c.DB.Create(&user)

    return c.RenderJson(user)
}

After running it results in:

runtime error: invalid memory address or nil pointer dereference on line 19 c.DB.NewRecord(user)

It successfully creates the datatables with automigrate, but I have no idea how I should use Gorm in my controller.

Any hints in the right direction?

Norean answered 5/8, 2014 at 15:14 Comment(1)
you can setup GORM in the app init and then use app.DB, that way your database connection can be shared across requests? revel.github.io/manual/database.htmlEnrique
R
11

Important note

it's just replacement for GORP of original example of the Revel. And it comes with some pitfalls of the origin. This answer can be used as drop-in replacement for the original one. But it doesn't solve the pitfalls.

Please, take a look comments of this unswer and @MaxGabriel's answer that solves the pitfalls.

I'd recommend to use @MaxGabriel's solution to protect you application against some kinds of slow-* DDoS attacks. And to reduce (in some cases) DB pressure.

Original answer

@rauyran rights, you have to invoke InitDB inside init function (into controllers package).

Full example here (too much):

Tree

/app
    /controllers
      app.go
      gorm.go
      init.go
    /models
      user.go
[...]

user.go

// models/user.go
package models

import  "time" // if you need/want

type User struct {          // example user fields
    Id                    int64
    Name                  string
    EncryptedPassword     []byte
    Password              string      `sql:"-"`
    CreatedAt             time.Time
    UpdatedAt             time.Time
    DeletedAt             time.Time     // for soft delete
}

gorm.go

//controllers/gorm.go
package controllers

import (
    "github.com/jinzhu/gorm"
    _ "github.com/lib/pq" // my example for postgres
    // short name for revel
    r "github.com/revel/revel"
    // YOUR APP NAME
    "yourappname/app/models"
    "database/sql"
)

// type: revel controller with `*gorm.DB`
// c.Txn will keep `Gdb *gorm.DB`
type GormController struct {
    *r.Controller
    Txn *gorm.DB
}

// it can be used for jobs
var Gdb *gorm.DB

// init db
func InitDB() {
    var err error
    // open db
    Gdb, err = gorm.Open("postgres", "user=uname dbname=udbname sslmode=disable password=supersecret")
    if err != nil {
        r.ERROR.Println("FATAL", err)
        panic( err )
    }
    Gdb.AutoMigrate(&models.User{})
    // unique index if need
    //Gdb.Model(&models.User{}).AddUniqueIndex("idx_user_name", "name")
}


// transactions

// This method fills the c.Txn before each transaction
func (c *GormController) Begin() r.Result {
    txn := Gdb.Begin()
    if txn.Error != nil {
        panic(txn.Error)
    }
    c.Txn = txn
    return nil
}

// This method clears the c.Txn after each transaction
func (c *GormController) Commit() r.Result {
    if c.Txn == nil {
        return nil
    }
    c.Txn.Commit()
    if err := c.Txn.Error; err != nil && err != sql.ErrTxDone {
        panic(err)
    }
    c.Txn = nil
    return nil
}

// This method clears the c.Txn after each transaction, too
func (c *GormController) Rollback() r.Result {
    if c.Txn == nil {
        return nil
    }
    c.Txn.Rollback()
    if err := c.Txn.Error; err != nil && err != sql.ErrTxDone {
        panic(err)
    }
    c.Txn = nil
    return nil
}

app.go

package controllers

import(
    "github.com/revel/revel"
    "yourappname/app/models"
)

type App struct {
    GormController
}

func (c App) Index() revel.Result {
    user := models.User{Name: "Jinzhup"}
    c.Txn.NewRecord(user)
    c.Txn.Create(&user)
    return c.RenderJSON(user)
}

init.go

package controllers
import "github.com/revel/revel"

func init() {
    revel.OnAppStart(InitDB) // invoke InitDB function before
    revel.InterceptMethod((*GormController).Begin, revel.BEFORE)
    revel.InterceptMethod((*GormController).Commit, revel.AFTER)
    revel.InterceptMethod((*GormController).Rollback, revel.FINALLY)
}

As you can see, it's like Revel's Booking modified for GORM.

Works fine for me. Result:

{
  "Id": 5,
  "Name": "Jinzhup",
  "EncryptedPassword": null,
  "Password": "",
  "CreatedAt": "2014-09-22T17:55:14.828661062+04:00",
  "UpdatedAt": "2014-09-22T17:55:14.828661062+04:00",
  "DeletedAt": "0001-01-01T00:00:00Z"
}
Rogation answered 22/9, 2014 at 14:19 Comment(5)
If I'm reading this right, this code creates a transaction at the beginning of every HTTP request and closes it at the end. This would create potentially long-running transactions if your HTTP server is slow (say it's blocked on an external HTTP request or another database, or any number of things). This uses up more resources on your database, and those transactions can hold locks on rows and cause deadlocks. Why not just use var Gdb *gorm.DB directly?Ruby
@MaxGabriel, it's just replacement for GORP of original example of the Revel. And yes, a long writing HTTP-request holds DB resources unreleased. E.g. this approach is good for slow-* DDoS attacks. Because, slow HTTP request turns to be slow DB request.Rogation
@MaxGabriel, feel free to add another answer, and I will add note to point to your answer. This is drop-in replacement for the original with all original pitfalls. Nothing more.Rogation
Ah ok, it makes sense that you were just translating from the example code. I took your advice and made another answer with the changes I suggested below: https://mcmap.net/q/1885994/-how-to-access-gorm-in-revel-controllerRuby
@MaxGabriel, yep, I've added the note.Rogation
R
3

This answer is derived from @IvanBlack's answer, at his suggestion. His version is a direct translation of the Revel example code, but we identified some problems with the original code that this answer fixes. The main changes it makes are:

  1. The entire HTTP request is no longer wrapped by a database transaction. Wrapping the entire HTTP request keeps open the transaction far longer than necessary, which could cause a number of problems:

    • Your transactions will hold locks on the database, and many transactions holding locks could lead to deadlocks
    • More database resources are used
    • These problems are magnified if your HTTP requests aren't mostly limited by your SQL database access. E.g. if your HTTP request blocks for 30 seconds on making an external HTTP request or accessing another database, the slowdown on those services could affect your SQL database.
  2. Because the transaction is no longer automatically being checked for errors at the end of the HTTP request, errors are checked as soon as the database insert is made. This is more correct as well: Imagine that after inserting the User struct into a database, you then stored the User.Id in another database like Redis. This would be fine if the database insert worked, but if it failed and you didn't immediately check the error, you would insert the default int64 value of 0 into Redis (before later rolling back only the SQL transaction).

Tree

/app
    /controllers
      app.go
      gorm.go
      init.go
    /models
      user.go
[...]

user.go

// models/user.go
package models

import  "time" // if you need/want

type User struct {          // example user fields
    Id                    int64
    Name                  string
    EncryptedPassword     []byte
    Password              string      `sql:"-"`
    CreatedAt             time.Time
    UpdatedAt             time.Time
    DeletedAt             time.Time     // for soft delete
}

gorm.go

//controllers/gorm.go
package controllers

import (
    "github.com/jinzhu/gorm"
    _ "github.com/lib/pq" // my example for postgres
    // short name for revel
    r "github.com/revel/revel"
)

// type: revel controller with `*gorm.DB`
type GormController struct {
    *r.Controller
    DB *gorm.DB
}

// it can be used for jobs
var Gdb *gorm.DB

// init db
func InitDB() {
    var err error
    // open db
    Gdb, err = gorm.Open("postgres", "user=USERNAME dbname=DBNAME sslmode=disable")
    Gdb.LogMode(true) // Print SQL statements
    if err != nil {
        r.ERROR.Println("FATAL", err)
        panic(err)
    }
}

func (c *GormController) SetDB() r.Result {
    c.DB = Gdb
    return nil
}

init.go

package controllers
import "github.com/revel/revel"

func init() {
    revel.OnAppStart(InitDB) // invoke InitDB function before
    revel.InterceptMethod((*GormController).SetDB, revel.BEFORE)
}

app.go

package controllers

import(
    "github.com/revel/revel"
    "yourappname/app/models"
)

type App struct {
    GormController
}

func (c App) Index() revel.Result {
    user := models.User{Name: "Jinzhup"} // Note: In practice you should initialize all struct fields
    if err := c.DB.Create(&user).Error; err != nil {
        panic(err)
    }
    return c.RenderJSON(user)
}

Result:

{
  "Id": 5,
  "Name": "Jinzhup",
  "EncryptedPassword": null,
  "Password": "",
  "CreatedAt": "2014-09-22T17:55:14.828661062+04:00",
  "UpdatedAt": "2014-09-22T17:55:14.828661062+04:00",
  "DeletedAt": "0001-01-01T00:00:00Z"
}
Ruby answered 30/10, 2017 at 18:34 Comment(0)
C
2

Your error is caused because you haven't initialised your c.DB database variable, it's still nil.

In your controllers/init.go file make sure you are calling revel.OnAppStart(InitDB). It should look something like this:

package controllers

import "github.com/revel/revel"

func init() {
    revel.OnAppStart(InitDB)
    // maybe some other init things
}
Cutcheon answered 5/8, 2014 at 15:25 Comment(5)
My controllers/init.go looks exactly like that and my InitDB inside gorm.go does get called correctly. But unfortunately I have no idea how to pass Gorm onto my app controller. You are right, c.DB is <nil> in my console.logNorean
Are you also setting the intercept methods as shown here? github.com/revel/revel/blob/master/samples/booking/app/…Cutcheon
No, because I don't have (*GorpController).Begin etc, that's for Gorp I guess. I'm trying to use Gorm.Norean
You need to implement the equivalent Begin methods on your GormController, just like the example GorpController doesCutcheon
Do you also happen to know why I must do that? Why can't I just access the gorm create method inside another controller?Norean
D
-1

OR you need to pass in a pointer to AutoMigrate?

 DB.AutoMigrate(&models.User{})
Dilettantism answered 18/10, 2017 at 20:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.