diff --git a/domain/album.go b/domain/album.go index 7e32e2ab3..d1137cc82 100644 --- a/domain/album.go +++ b/domain/album.go @@ -6,13 +6,13 @@ type Album struct { Id string Name string ArtistId string `parent:"artist"` - CoverArtPath string // TODO http://stackoverflow.com/questions/13795842/linking-itunes-itc2-files-and-ituneslibrary-xml + CoverArtPath string CoverArtId string Artist string AlbumArtist string - Year int + Year int `idx:"Year"` Compilation bool - Starred bool + Starred bool `idx:"Starred"` PlayCount int PlayDate time.Time Rating int diff --git a/persistence/ledis_repository.go b/persistence/ledis_repository.go index d94207455..64dbf9c05 100644 --- a/persistence/ledis_repository.go +++ b/persistence/ledis_repository.go @@ -7,6 +7,8 @@ import ( "reflect" "strings" + "time" + "github.com/deluan/gosonic/domain" "github.com/deluan/gosonic/utils" "github.com/siddontang/ledisdb/ledis" @@ -18,6 +20,7 @@ type ledisRepository struct { fieldNames []string parentTable string parentIdField string + indexes map[string]string } func (r *ledisRepository) init(table string, entity interface{}) { @@ -31,7 +34,24 @@ func (r *ledisRepository) init(table string, entity interface{}) { r.fieldNames[i] = k i++ } - r.parentTable, r.parentIdField, _ = r.getParent(entity) + r.parseAnnotations(entity) +} + +func (r *ledisRepository) parseAnnotations(entity interface{}) { + r.indexes = make(map[string]string) + dt := reflect.TypeOf(entity).Elem() + for i := 0; i < dt.NumField(); i++ { + f := dt.Field(i) + table := f.Tag.Get("parent") + if table != "" { + r.parentTable = table + r.parentIdField = f.Name + } + idx := f.Tag.Get("idx") + if idx != "" { + r.indexes[idx] = f.Name + } + } } // TODO Use annotations to specify fields to be used @@ -123,6 +143,18 @@ func (r *ledisRepository) saveOrUpdate(id string, entity interface{}) error { } + for idx, fn := range r.indexes { + idxName := fmt.Sprintf("%s:idx:%s", r.table, idx) + if _, err := Db().ZRem([]byte(idxName), []byte(id)); err != nil { + return err + } + score := calcScore(entity, fn) + sidx := ledis.ScorePair{score, []byte(id)} + if _, err = Db().ZAdd([]byte(idxName), sidx); err != nil { + return err + } + } + sid := ledis.ScorePair{0, []byte(id)} if _, err = Db().ZAdd([]byte(allKey), sid); err != nil { return err @@ -134,6 +166,26 @@ func (r *ledisRepository) saveOrUpdate(id string, entity interface{}) error { return nil } +func calcScore(entity interface{}, fieldName string) int64 { + var score int64 + + dv := reflect.ValueOf(entity).Elem() + v := dv.FieldByName(fieldName) + + switch v.Interface().(type) { + case int: + score = v.Int() + case bool: + if v.Bool() { + score = 1 + } + case time.Time: + score = utils.ToMillis(v.Interface().(time.Time)) + } + + return score +} + func (r *ledisRepository) getParentRelationKey(entity interface{}) string { parentId := r.getParentId(entity) if parentId != "" { @@ -142,22 +194,6 @@ func (r *ledisRepository) getParentRelationKey(entity interface{}) string { return "" } -// TODO Optimize -func (r *ledisRepository) getParent(entity interface{}) (table string, idField string, id string) { - dt := reflect.TypeOf(entity).Elem() - for i := 0; i < dt.NumField(); i++ { - f := dt.Field(i) - table = f.Tag.Get("parent") - if table != "" { - idField = f.Name - dv := reflect.ValueOf(entity).Elem() - id = dv.FieldByName(f.Name).String() - return - } - } - return -} - func (r *ledisRepository) getParentId(entity interface{}) string { if r.parentTable != "" { dv := reflect.ValueOf(entity).Elem() diff --git a/persistence/ledis_repository_test.go b/persistence/ledis_repository_test.go index a510e698f..91a2e21a7 100644 --- a/persistence/ledis_repository_test.go +++ b/persistence/ledis_repository_test.go @@ -5,14 +5,20 @@ import ( "strconv" "testing" + "time" + "github.com/deluan/gosonic/tests" + "github.com/deluan/gosonic/utils" . "github.com/smartystreets/goconvey/convey" ) type TestEntity struct { Id string Name string - ParentId string `parent:"parent"` + ParentId string `parent:"parent"` + Year time.Time `idx:"ByYear"` + Count int `idx:"ByCount"` + Flag bool `idx:"ByFlag"` } func shouldBeEqual(actualStruct interface{}, expectedStruct ...interface{}) string { @@ -30,6 +36,55 @@ func createRepo() *ledisRepository { func TestBaseRepository(t *testing.T) { tests.Init(t, false) + Convey("Subject: Annotations", t, func() { + repo := createRepo() + Convey("It should parse the parent table definition", func() { + So(repo.parentTable, ShouldEqual, "parent") + So(repo.parentIdField, ShouldEqual, "ParentId") + }) + Convey("It should parse the definded indexes", func() { + So(repo.indexes, ShouldHaveLength, 3) + So(repo.indexes["ByYear"], ShouldEqual, "Year") + So(repo.indexes["ByFlag"], ShouldEqual, "Flag") + So(repo.indexes["ByCount"], ShouldEqual, "Count") + }) + }) + + Convey("Subject: calcScore", t, func() { + repo := createRepo() + + Convey("It should create an int score", func() { + def := repo.indexes["ByCount"] + entity := &TestEntity{Count: 10} + score := calcScore(entity, def) + + So(score, ShouldEqual, 10) + }) + Convey("It should create a boolean score", func() { + def := repo.indexes["ByFlag"] + Convey("Value false", func() { + entity := &TestEntity{Flag: false} + score := calcScore(entity, def) + + So(score, ShouldEqual, 0) + }) + Convey("Value true", func() { + entity := &TestEntity{Flag: true} + score := calcScore(entity, def) + + So(score, ShouldEqual, 1) + }) + }) + Convey("It should create a time score", func() { + def := repo.indexes["ByYear"] + now := time.Now() + entity := &TestEntity{Year: now} + score := calcScore(entity, def) + + So(score, ShouldEqual, utils.ToMillis(now)) + }) + }) + Convey("Subject: NewId", t, func() { repo := createRepo() @@ -68,7 +123,7 @@ func TestBaseRepository(t *testing.T) { repo := createRepo() Convey("When I save a new entity and a parent", func() { - entity := &TestEntity{"123", "My Name", "ABC"} + entity := &TestEntity{Id: "123", Name: "My Name", ParentId: "ABC", Year: time.Now()} err := repo.saveOrUpdate("123", entity) Convey("Then saving the entity shouldn't return any errors", func() { So(err, ShouldBeNil) @@ -97,11 +152,11 @@ func TestBaseRepository(t *testing.T) { Convey("Given a table with one entity", func() { repo := createRepo() - entity := &TestEntity{"111", "One Name", "AAA"} + entity := &TestEntity{Id: "111", Name: "One Name", ParentId: "AAA"} repo.saveOrUpdate(entity.Id, entity) Convey("When I save an entity with a different Id", func() { - newEntity := &TestEntity{"222", "Another Name", "AAA"} + newEntity := &TestEntity{Id: "222", Name: "Another Name", ParentId: "AAA"} repo.saveOrUpdate(newEntity.Id, newEntity) Convey("Then the number of entities should be 2", func() { @@ -112,7 +167,7 @@ func TestBaseRepository(t *testing.T) { }) Convey("When I save an entity with the same Id", func() { - newEntity := &TestEntity{"111", "New Name", "AAA"} + newEntity := &TestEntity{Id: "111", Name: "New Name", ParentId: "AAA"} repo.saveOrUpdate(newEntity.Id, newEntity) Convey("Then the number of entities should be 1", func() { @@ -133,7 +188,7 @@ func TestBaseRepository(t *testing.T) { Convey("Given a table with 3 entities", func() { repo := createRepo() for i := 1; i <= 3; i++ { - e := &TestEntity{strconv.Itoa(i), fmt.Sprintf("Name %d", i), "AAA"} + e := &TestEntity{Id: strconv.Itoa(i), Name: fmt.Sprintf("Name %d", i), ParentId: "AAA"} repo.saveOrUpdate(e.Id, e) }