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