go-micro.dev/v5@v5.12.0/store/mysql/mysql.go (about) 1 package mysql 2 3 import ( 4 "database/sql" 5 "fmt" 6 "time" 7 "unicode" 8 9 "github.com/pkg/errors" 10 log "go-micro.dev/v5/logger" 11 "go-micro.dev/v5/store" 12 ) 13 14 var ( 15 // DefaultDatabase is the database that the sql store will use if no database is provided. 16 DefaultDatabase = "micro" 17 // DefaultTable is the table that the sql store will use if no table is provided. 18 DefaultTable = "micro" 19 ) 20 21 type sqlStore struct { 22 db *sql.DB 23 24 database string 25 table string 26 27 options store.Options 28 29 readPrepare, writePrepare, deletePrepare *sql.Stmt 30 } 31 32 func (s *sqlStore) Init(opts ...store.Option) error { 33 for _, o := range opts { 34 o(&s.options) 35 } 36 // reconfigure 37 return s.configure() 38 } 39 40 func (s *sqlStore) Options() store.Options { 41 return s.options 42 } 43 44 func (s *sqlStore) Close() error { 45 return s.db.Close() 46 } 47 48 // List all the known records. 49 func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) { 50 rows, err := s.db.Query(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s;", s.database, s.table)) 51 if err != nil { 52 if err == sql.ErrNoRows { 53 return nil, nil 54 } 55 return nil, err 56 } 57 defer rows.Close() 58 59 var records []string 60 var cachedTime time.Time 61 62 for rows.Next() { 63 record := &store.Record{} 64 if err := rows.Scan(&record.Key, &record.Value, &cachedTime); err != nil { 65 return nil, err 66 } 67 68 if cachedTime.Before(time.Now()) { 69 // record has expired 70 go s.Delete(record.Key) 71 } else { 72 records = append(records, record.Key) 73 } 74 } 75 rowErr := rows.Close() 76 if rowErr != nil { 77 // transaction rollback or something 78 return records, rowErr 79 } 80 if err := rows.Err(); err != nil { 81 return nil, err 82 } 83 return records, nil 84 } 85 86 // Read all records with keys. 87 func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) { 88 var options store.ReadOptions 89 for _, o := range opts { 90 o(&options) 91 } 92 93 // TODO: make use of options.Prefix using WHERE key LIKE = ? 94 95 var records []*store.Record 96 row := s.readPrepare.QueryRow(key) 97 record := &store.Record{} 98 var cachedTime time.Time 99 100 if err := row.Scan(&record.Key, &record.Value, &cachedTime); err != nil { 101 if err == sql.ErrNoRows { 102 return records, store.ErrNotFound 103 } 104 return records, err 105 } 106 if cachedTime.Before(time.Now()) { 107 // record has expired 108 go s.Delete(key) 109 return records, store.ErrNotFound 110 } 111 record.Expiry = time.Until(cachedTime) 112 records = append(records, record) 113 114 return records, nil 115 } 116 117 // Write records. 118 func (s *sqlStore) Write(r *store.Record, opts ...store.WriteOption) error { 119 timeCached := time.Now().Add(r.Expiry) 120 _, err := s.writePrepare.Exec(r.Key, r.Value, timeCached, r.Value, timeCached) 121 if err != nil { 122 return errors.Wrap(err, "Couldn't insert record "+r.Key) 123 } 124 125 return nil 126 } 127 128 // Delete records with keys. 129 func (s *sqlStore) Delete(key string, opts ...store.DeleteOption) error { 130 result, err := s.deletePrepare.Exec(key) 131 if err != nil { 132 return err 133 } 134 _, err = result.RowsAffected() 135 if err != nil { 136 return err 137 } 138 139 return nil 140 } 141 142 func (s *sqlStore) initDB() error { 143 // Create the namespace's database 144 _, err := s.db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s ;", s.database)) 145 if err != nil { 146 return err 147 } 148 149 _, err = s.db.Exec(fmt.Sprintf("USE %s ;", s.database)) 150 if err != nil { 151 return errors.Wrap(err, "Couldn't use database") 152 } 153 154 // Create a table for the namespace's prefix 155 createSQL := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (`key` varchar(255) primary key, value blob null, expiry timestamp not null);", s.table) 156 _, err = s.db.Exec(createSQL) 157 if err != nil { 158 return errors.Wrap(err, "Couldn't create table") 159 } 160 161 // prepare 162 s.readPrepare, _ = s.db.Prepare(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s WHERE `key` = ?;", s.database, s.table)) 163 s.writePrepare, _ = s.db.Prepare(fmt.Sprintf("INSERT INTO %s.%s (`key`, value, expiry) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE `value`= ?, `expiry` = ?", s.database, s.table)) 164 s.deletePrepare, _ = s.db.Prepare(fmt.Sprintf("DELETE FROM %s.%s WHERE `key` = ?;", s.database, s.table)) 165 166 return nil 167 } 168 169 func (s *sqlStore) configure() error { 170 nodes := s.options.Nodes 171 if len(nodes) == 0 { 172 nodes = []string{"localhost:3306"} 173 } 174 175 database := s.options.Database 176 if len(database) == 0 { 177 database = DefaultDatabase 178 } 179 180 table := s.options.Table 181 if len(table) == 0 { 182 table = DefaultTable 183 } 184 185 for _, r := range database { 186 if !unicode.IsLetter(r) { 187 return errors.New("store.namespace must only contain letters") 188 } 189 } 190 191 source := nodes[0] 192 // create source from first node 193 db, err := sql.Open("mysql", source) 194 if err != nil { 195 return err 196 } 197 198 if err := db.Ping(); err != nil { 199 return err 200 } 201 202 if s.db != nil { 203 s.db.Close() 204 } 205 206 // save the values 207 s.db = db 208 s.database = database 209 s.table = table 210 211 // initialize the database 212 return s.initDB() 213 } 214 215 func (s *sqlStore) String() string { 216 return "mysql" 217 } 218 219 // New returns a new micro Store backed by sql. 220 func NewMysqlStore(opts ...store.Option) store.Store { 221 var options store.Options 222 for _, o := range opts { 223 o(&options) 224 } 225 226 // new store 227 s := new(sqlStore) 228 // set the options 229 s.options = options 230 231 // configure the store 232 if err := s.configure(); err != nil { 233 log.Fatal(err) 234 } 235 236 // return store 237 return s 238 }