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  }