ORM (Object Relational Mapper)

This is a comparison betweengorp andsqlalchemy.

Using pq and psycopg2 it creates a bunch of ORM instance objects,then edits them all one by one and then deletes them all. This exampleassumes PostgreSQL and that the table already exists.

It creates X number of "talks" which has the following column types:

  • id serial integer
  • topic varchar(200)
  • when timestamp
  • tags array of text
  • duration real
    Then lastly it measures how long it takes to do all the inserts, all theupdates and all the deletes.

When running these for 10,000 iterations on my computer I get thefollowing outputs:

  1. $ python orm.py
  2. insert 3.09894585609
  3. edit 30.3197979927
  4. delete 18.6974749565
  5. TOTAL 52.1162188053
  6. $ go run orm.go
  7. insert 2.542336905s
  8. edit 10.28062312s
  9. delete 6.851942699s
  10. TOTAL 19.674902724s

Python

  1. # *- coding: utf-8 -*
  2. import time
  3. import random
  4. import datetime
  5.  
  6. from sqlalchemy import create_engine
  7. from sqlalchemy.orm import sessionmaker
  8. from sqlalchemy.ext.declarative import declarative_base
  9. from sqlalchemy import Column, Integer, String, Float, DateTime, Sequence
  10. from sqlalchemy.dialects import postgresql
  11.  
  12. HOW_MANY = 1000
  13.  
  14. # import logging
  15. # logging.basicConfig()
  16. # logger = logging.getLogger('sqlalchemy.engine')
  17. # logger.setLevel(logging.INFO)
  18.  
  19. Base = declarative_base()
  20.  
  21.  
  22. class Talk(Base):
  23. __tablename__ = 'talks'
  24.  
  25. id = Column(Integer, Sequence('talks_id_seq'), primary_key=True)
  26. topic = Column(String)
  27. when = Column(DateTime)
  28. tags = Column(postgresql.ARRAY(String))
  29. duration = Column(Float)
  30.  
  31.  
  32. def _random_topic():
  33. return random.choice((
  34. u'No talks added yet',
  35. u"I'm working on a branch of django-mongokit that I "
  36. "thought you'd like to know about.",
  37. u'I want to learn Gaelic.',
  38. u"I'm well, thank you.",
  39. u' (Kaw uhn KEU-ra shin KAW-la root uh CHOO-nik mee uhn-royer?)',
  40. u'Chah beh shin KEU-ra, sheh shin moe CHYEH-luh uh vah EEN-tchuh!',
  41. u'STUH LUH-oom BRISS-kaht-chun goo MAWR',
  42. u"Suas Leis a' Ghàidhlig! Up with Gaelic!",
  43. u"Tha mi ag iarraidh briosgaid!",
  44. ))
  45.  
  46.  
  47. def _random_when():
  48. return datetime.datetime(random.randint(2000, 2010),
  49. random.randint(1, 12),
  50. random.randint(1, 28),
  51. 0, 0, 0)
  52.  
  53.  
  54. def _random_tags():
  55. tags = [u'one', u'two', u'three', u'four', u'five', u'six',
  56. u'seven', u'eight', u'nine', u'ten']
  57. random.shuffle(tags)
  58. return tags[:random.randint(0, 3)]
  59.  
  60.  
  61. def _random_duration():
  62. return round(random.random() * 10, 1)
  63.  
  64.  
  65. def run():
  66. engine = create_engine(
  67. 'postgresql://peterbe:test123@localhost/fastestdb',
  68. echo=False
  69. )
  70.  
  71. Session = sessionmaker(bind=engine)
  72. session = Session()
  73.  
  74. session.query(Talk).delete()
  75.  
  76. t0 = time.time()
  77. # CREATE ALL
  78. talks = []
  79. for i in range(HOW_MANY):
  80. talk = Talk(
  81. topic=_random_topic(),
  82. when=_random_when(),
  83. duration=_random_duration(),
  84. tags=_random_tags()
  85. )
  86. session.add(talk)
  87. talks.append(talk)
  88.  
  89. session.commit()
  90.  
  91. t1 = time.time()
  92. # EDIT ALL
  93.  
  94. for talk in talks:
  95. talk.topic += "extra"
  96. talk.duration += 1.0
  97. talk.when += datetime.timedelta(days=1)
  98. talk.tags.append("extra")
  99. session.merge(talk)
  100.  
  101. session.commit()
  102. t2 = time.time()
  103.  
  104. # DELETE EACH
  105. for talk in talks:
  106. session.delete(talk)
  107. session.commit()
  108. t3 = time.time()
  109.  
  110. print "insert", t1 - t0
  111. print "edit", t2 - t1
  112. print "delete", t3 - t2
  113. print "TOTAL", t3 - t0
  114.  
  115. if __name__ == '__main__':
  116. run()

Go

  1. package main
  2.  
  3. import (
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "github.com/coopernurse/gorp"
  8. _ "github.com/lib/pq"
  9. "log"
  10. "math/rand"
  11. // "os"
  12. "regexp"
  13. "strings"
  14. "time"
  15. )
  16.  
  17. type StringSlice []string
  18.  
  19. // Implements sql.Scanner for the String slice type
  20. // Scanners take the database value (in this case as a byte slice)
  21. // and sets the value of the type. Here we cast to a string and
  22. // do a regexp based parse
  23. func (s *StringSlice) Scan(src interface{}) error {
  24. asBytes, ok := src.([]byte)
  25. if !ok {
  26. return error(errors.New("Scan source was not []bytes"))
  27. }
  28.  
  29. asString := string(asBytes)
  30. parsed := parseArray(asString)
  31. (*s) = StringSlice(parsed)
  32.  
  33. return nil
  34. }
  35.  
  36. func ToArray(str []string) string {
  37. L := len(str)
  38. out := "{"
  39. for i, s := range str {
  40. out += "\"" + s + "\""
  41. if i+1 < L {
  42. out += ","
  43. }
  44. }
  45. out += "}"
  46.  
  47. return out
  48. }
  49.  
  50. // construct a regexp to extract values:
  51. var (
  52. // unquoted array values must not contain: (" , \ { } whitespace NULL)
  53. // and must be at least one char
  54. unquotedChar = `[^",\\{}\s(NULL)]`
  55. unquotedValue = fmt.Sprintf("(%s)+", unquotedChar)
  56.  
  57. // quoted array values are surrounded by double quotes, can be any
  58. // character except " or \, which must be backslash escaped:
  59. quotedChar = `[^"\\]|\\"|\\\\`
  60. quotedValue = fmt.Sprintf("\"(%s)*\"", quotedChar)
  61.  
  62. // an array value may be either quoted or unquoted:
  63. arrayValue = fmt.Sprintf("(?P<value>(%s|%s))", unquotedValue, quotedValue)
  64.  
  65. // Array values are separated with a comma IF there is more than one value:
  66. arrayExp = regexp.MustCompile(fmt.Sprintf("((%s)(,)?)", arrayValue))
  67.  
  68. valueIndex int
  69. )
  70.  
  71. // Find the index of the 'value' named expression
  72. func init() {
  73. for i, subexp := range arrayExp.SubexpNames() {
  74. if subexp == "value" {
  75. valueIndex = i
  76. break
  77. }
  78. }
  79. }
  80.  
  81. // Parse the output string from the array type.
  82. // Regex used: (((?P<value>(([^",\\{}\s(NULL)])+|"([^"\\]|\\"|\\\\)*")))(,)?)
  83. func parseArray(array string) []string {
  84. results := make([]string, 0)
  85. matches := arrayExp.FindAllStringSubmatch(array, -1)
  86. for _, match := range matches {
  87. s := match[valueIndex]
  88. // the string _might_ be wrapped in quotes, so trim them:
  89. s = strings.Trim(s, "\"")
  90. results = append(results, s)
  91. }
  92. return results
  93. }
  94.  
  95. const HOW_MANY = 1000
  96.  
  97. func random_topic() string {
  98. topics := []string{
  99. "No talks added yet",
  100. "I'm working on a branch of django-mongokit that I thought you'd like to know about.",
  101. "I want to learn Gaelic.",
  102. "I'm well, thank you.",
  103. "(Kaw uhn KEU-ra shin KAW-la root uh CHOO-nik mee uhn-royer?)",
  104. "Chah beh shin KEU-ra, sheh shin moe CHYEH-luh uh vah EEN-tchuh!",
  105. "STUH LUH-oom BRISS-kaht-chun goo MAWR",
  106. "Suas Leis a' Ghàidhlig! Up with Gaelic!",
  107. "Tha mi ag iarraidh briosgaid!",
  108. }
  109.  
  110. return topics[rand.Intn(len(topics))]
  111. }
  112.  
  113. func random_when() time.Time {
  114. return time.Date(
  115. 2000+rand.Intn(10),
  116. time.November,
  117. rand.Intn(12),
  118. rand.Intn(28),
  119. 0, 0, 0, time.UTC)
  120. }
  121.  
  122. func random_tags() []string {
  123. tags := []string{
  124. "one",
  125. "two",
  126. "three",
  127. "four",
  128. "five",
  129. "six",
  130. "seven",
  131. "eight",
  132. "nine",
  133. "ten",
  134. }
  135. return tags[:rand.Intn(4)]
  136. }
  137.  
  138. func random_duration() float64 {
  139. return rand.Float64() * 10
  140. }
  141.  
  142. func main() {
  143. dbmap := initDb()
  144. defer dbmap.Db.Close()
  145.  
  146. // alter sequence talks_id_seq restart with 1;
  147.  
  148. err := dbmap.TruncateTables()
  149. checkErr(err, "TruncateTables failed")
  150.  
  151. // dbmap.TraceOn("[gorp]", log.New(os.Stdout, "myapp:", log.Lmicroseconds))
  152.  
  153. t0 := time.Now()
  154. var talks [HOW_MANY]Talk
  155.  
  156. trans, err := dbmap.Begin()
  157. if err != nil {
  158. panic(err)
  159. }
  160. // CREATE
  161. for i := 0; i < HOW_MANY; i++ {
  162. topic := random_topic()
  163. when := random_when()
  164. tags := random_tags()
  165. duration := random_duration()
  166.  
  167. talk := Talk{
  168. Topic: topic,
  169. When: when,
  170. Tags: ToArray(tags),
  171. Duration: duration,
  172. }
  173.  
  174. err = dbmap.Insert(&talk)
  175. checkErr(err, "Insert failed")
  176. talks[i] = talk
  177.  
  178. }
  179.  
  180. trans.Commit()
  181. t1 := time.Since(t0)
  182. t0 = time.Now()
  183.  
  184. trans, err = dbmap.Begin()
  185. if err != nil {
  186. panic(err)
  187. }
  188.  
  189. // EDIT ALL
  190. for _, talk := range talks {
  191.  
  192. talk.Topic += "extra"
  193. talk.Duration += 1.0
  194. talk.When = talk.When.Add(time.Hour * 24)
  195. tags := parseArray(talk.Tags)
  196. talk.Tags = ToArray(append(tags, "extra"))
  197.  
  198. _, err := dbmap.Update(&talk)
  199. checkErr(err, "Update failed")
  200. }
  201.  
  202. trans.Commit()
  203. t2 := time.Since(t0)
  204. t0 = time.Now()
  205.  
  206. trans, err = dbmap.Begin()
  207. if err != nil {
  208. panic(err)
  209. }
  210.  
  211. // DELETE ALL
  212. for _, talk := range talks {
  213. _, err = dbmap.Exec("delete from talks where id=$1", talk.Id)
  214. checkErr(err, "Delete failed")
  215. }
  216.  
  217. trans.Commit()
  218. t3 := time.Since(t0)
  219.  
  220. fmt.Println("insert", t1)
  221. fmt.Println("edit", t2)
  222. fmt.Println("delete", t3)
  223. fmt.Println("TOTAL", t1+t2+t3)
  224.  
  225. }
  226.  
  227. type Talk struct {
  228. // db tag lets you specify the column name
  229. // if it differs from the struct field
  230. Id int64 `db:"id"`
  231. Topic string `db:"topic"`
  232. When time.Time `db:"when"`
  233. // Tags StringSlice
  234. Tags string `db:"tags"`
  235. Duration float64 `db:"duration"`
  236. }
  237.  
  238. func initDb() *gorp.DbMap {
  239. // connect to db using standard Go database/sql API
  240. // use whatever database/sql driver you wish
  241. db, err := sql.Open("postgres", `
  242. user=peterbe dbname=fastestdb
  243. password=test123 sslmode=disable`)
  244. checkErr(err, "sql.Open failed")
  245.  
  246. // construct a gorp DbMap
  247. dbmap := &gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}}
  248.  
  249. // add a table, setting the table name to 'talks' and
  250. // specifying that the Id property is an auto incrementing PK
  251. dbmap.AddTableWithName(Talk{}, "talks").SetKeys(true, "Id")
  252.  
  253. return dbmap
  254. }
  255.  
  256. func checkErr(err error, msg string) {
  257. if err != nil {
  258. log.Fatalln(msg, err)
  259. }
  260. }