github.com/rjgonzale/pop/v5@v5.1.3-dev/connection.go (about) 1 package pop 2 3 import ( 4 "context" 5 "sync/atomic" 6 "time" 7 8 "github.com/gobuffalo/pop/v5/internal/defaults" 9 "github.com/gobuffalo/pop/v5/internal/randx" 10 "github.com/jmoiron/sqlx" 11 "github.com/pkg/errors" 12 ) 13 14 // Connections contains all available connections 15 var Connections = map[string]*Connection{} 16 17 // Connection represents all necessary details to talk with a datastore 18 type Connection struct { 19 ID string 20 Store store 21 Dialect dialect 22 Elapsed int64 23 TX *Tx 24 eager bool 25 eagerFields []string 26 } 27 28 func (c *Connection) String() string { 29 return c.URL() 30 } 31 32 // URL returns the datasource connection string 33 func (c *Connection) URL() string { 34 return c.Dialect.URL() 35 } 36 37 // MigrationURL returns the datasource connection string used for running the migrations 38 func (c *Connection) MigrationURL() string { 39 return c.Dialect.MigrationURL() 40 } 41 42 // MigrationTableName returns the name of the table to track migrations 43 func (c *Connection) MigrationTableName() string { 44 return c.Dialect.Details().MigrationTableName() 45 } 46 47 // NewConnection creates a new connection, and sets it's `Dialect` 48 // appropriately based on the `ConnectionDetails` passed into it. 49 func NewConnection(deets *ConnectionDetails) (*Connection, error) { 50 err := deets.Finalize() 51 if err != nil { 52 return nil, err 53 } 54 c := &Connection{ 55 ID: randx.String(30), 56 } 57 58 if nc, ok := newConnection[deets.Dialect]; ok { 59 c.Dialect, err = nc(deets) 60 if err != nil { 61 return c, errors.Wrap(err, "could not create new connection") 62 } 63 return c, nil 64 } 65 return nil, errors.Errorf("could not found connection creator for %v", deets.Dialect) 66 } 67 68 // Connect takes the name of a connection, default is "development", and will 69 // return that connection from the available `Connections`. If a connection with 70 // that name can not be found an error will be returned. If a connection is 71 // found, and it has yet to open a connection with its underlying datastore, 72 // a connection to that store will be opened. 73 func Connect(e string) (*Connection, error) { 74 if len(Connections) == 0 { 75 err := LoadConfigFile() 76 if err != nil { 77 return nil, err 78 } 79 } 80 e = defaults.String(e, "development") 81 c := Connections[e] 82 if c == nil { 83 return c, errors.Errorf("could not find connection named %s", e) 84 } 85 err := c.Open() 86 return c, errors.Wrapf(err, "couldn't open connection for %s", e) 87 } 88 89 // Open creates a new datasource connection 90 func (c *Connection) Open() error { 91 if c.Store != nil { 92 return nil 93 } 94 if c.Dialect == nil { 95 return errors.New("invalid connection instance") 96 } 97 details := c.Dialect.Details() 98 driver := c.Dialect.DefaultDriver() 99 if details.Driver != "" { 100 driver = details.Driver 101 } 102 103 db, err := sqlx.Open(driver, c.Dialect.URL()) 104 if err != nil { 105 return errors.Wrap(err, "could not open database connection") 106 } 107 db.SetMaxOpenConns(details.Pool) 108 if details.IdlePool != 0 { 109 db.SetMaxIdleConns(details.IdlePool) 110 } 111 if details.ConnMaxLifetime > 0 { 112 db.SetConnMaxLifetime(details.ConnMaxLifetime) 113 } 114 if details.Unsafe { 115 db = db.Unsafe() 116 } 117 c.Store = &dB{db} 118 119 if d, ok := c.Dialect.(afterOpenable); ok { 120 err = d.AfterOpen(c) 121 if err != nil { 122 c.Store = nil 123 } 124 } 125 return errors.Wrap(err, "could not open database connection") 126 } 127 128 // Close destroys an active datasource connection 129 func (c *Connection) Close() error { 130 return errors.Wrap(c.Store.Close(), "couldn't close connection") 131 } 132 133 // Transaction will start a new transaction on the connection. If the inner function 134 // returns an error then the transaction will be rolled back, otherwise the transaction 135 // will automatically commit at the end. 136 func (c *Connection) Transaction(fn func(tx *Connection) error) error { 137 return c.Dialect.Lock(func() error { 138 var dberr error 139 cn, err := c.NewTransaction() 140 if err != nil { 141 return err 142 } 143 err = fn(cn) 144 if err != nil { 145 dberr = cn.TX.Rollback() 146 } else { 147 dberr = cn.TX.Commit() 148 } 149 if err != nil { 150 return err 151 } 152 return errors.Wrap(dberr, "error committing or rolling back transaction") 153 }) 154 155 } 156 157 // Rollback will open a new transaction and automatically rollback that transaction 158 // when the inner function returns, regardless. This can be useful for tests, etc... 159 func (c *Connection) Rollback(fn func(tx *Connection)) error { 160 cn, err := c.NewTransaction() 161 if err != nil { 162 return err 163 } 164 fn(cn) 165 return cn.TX.Rollback() 166 } 167 168 // NewTransaction starts a new transaction on the connection 169 func (c *Connection) NewTransaction() (*Connection, error) { 170 var cn *Connection 171 if c.TX == nil { 172 tx, err := c.Store.Transaction() 173 if err != nil { 174 return cn, errors.Wrap(err, "couldn't start a new transaction") 175 } 176 var store store = tx 177 178 // Rewrap the store if it was a context store 179 if cs, ok := c.Store.(contextStore); ok { 180 store = contextStore{store: store, ctx: cs.ctx} 181 } 182 cn = &Connection{ 183 ID: randx.String(30), 184 Store: store, 185 Dialect: c.Dialect, 186 TX: tx, 187 } 188 } else { 189 cn = c 190 } 191 return cn, nil 192 } 193 194 // WithContext returns a copy of the connection, wrapped with a context. 195 func (c *Connection) WithContext(ctx context.Context) *Connection { 196 cn := c.copy() 197 cn.Store = contextStore{ 198 store: cn.Store, 199 ctx: ctx, 200 } 201 return cn 202 } 203 204 func (c *Connection) copy() *Connection { 205 return &Connection{ 206 ID: randx.String(30), 207 Store: c.Store, 208 Dialect: c.Dialect, 209 TX: c.TX, 210 } 211 } 212 213 // Q creates a new "empty" query for the current connection. 214 func (c *Connection) Q() *Query { 215 return Q(c) 216 } 217 218 // disableEager disables eager mode for current connection. 219 func (c *Connection) disableEager() { 220 c.eager = false 221 c.eagerFields = []string{} 222 } 223 224 // TruncateAll truncates all data from the datasource 225 func (c *Connection) TruncateAll() error { 226 return c.Dialect.TruncateAll(c) 227 } 228 229 func (c *Connection) timeFunc(name string, fn func() error) error { 230 start := time.Now() 231 err := fn() 232 atomic.AddInt64(&c.Elapsed, int64(time.Since(start))) 233 if err != nil { 234 return err 235 } 236 return nil 237 }