1

I need your advice whether RavenDB would be suitable for building a Music Database. I would like to use the embedded version in a C# Windows Application.

Currently the database is based on SQL with normalisation in place, having tables for e.g. Artist, Album, Genre, Share (main folder of music collection), Folder, Song and then a bunch of tables to build the relations like AlbumArtist, GenreSong, ArtistSong, ComposerSong, ConductorSOng, etc. I think you'll get it.

Now with RavenDB I could store every Song as a Document, containing all the information, but then I would multiply ArtistNAme, AlbumName and even the Folder for every song.

Figured out that I could separate Artist, Genre, etc. and use Includes in my Query, but how would i run then a query which gives me all Songs with a Genre of "Rock" or all Albums for a specific Artist?

My understanding is that i would need an Index to be able to use Properties from an included document as part of a query. Otherwise I would get compilation errors. Right? So basically I would need to build one large index containing all the fields that a user might do a query.

Or is there any other way, which I don't see?

Bhavesh Odedra
  • 10,990
  • 12
  • 33
  • 58
Helmut
  • 105
  • 1
  • 9

3 Answers3

0

While you can "include" properties from other documents in an index (using LoadDocument) it's not recommended to use extensively as the index need to be rebuilt more often.

In your case you could model your Song document to include references to Artist, Genre etc by id and query on that and then use a Transformer to transform the result to desired "view model". Use LoadDocument in the transformer to fetch artist name, genre name etc and return the transformed result. The transformation is performed server side on request.

Your song entity (simplified) might look like this:

public class Song
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string ArtistId { get; set; }
}

And an index like this:

public class Song_ByArtist : AbstractIndexCreationTask<Song>
{
    public Song_ByArtist()
    {
        Map = songs => from song in songs
                       select new
                       {
                           song.Name,
                           song.ArtistId
                       };
    }
}

Combined with a transformer:

public class Song_Artist_Transformer : AbstractTransformerCreationTask<Song>
{
    public Song_Artist_Transformer()
    {
        TransformResults = results => from song in results
                                      let artist = LoadDocument<Artist>(song.ArtistId)
                                      select new SongArtistViewModel
                                      {
                                          SongName = song.Name,
                                          ArtistName = artist.Name
                                      };
    }
}

You can query for songs by artists and return a view model including the artist name with:

using (var session = _documentStore.OpenSession())
{
    var results = session.Query<Song, Song_ByArtist>()
        .Where(x => x.ArtistId == "artists/1")
        .TransformWith<Song_Artist_Transformer, SongArtistViewModel>();
}

This would return all songs for artist "artists/1" transformed as a view model with song name and artist name.

So the bottom line is: model your song document to include references to other documents (aggregates if following DDD) where needed and then include the information needed by using transformers. Transformers could be looked at sort of like a "View" in a relational db.

Note: Make one combined index for your song document where you index all properties (both song properties and references) and then use multiple transformers to present the data as needed. It's often better to use one "large" index per document instead of several small for the same document type. In this example I only mapped the name and artist id to keep it simple.

Hope this helps!

Jens Pettersson
  • 1,167
  • 1
  • 9
  • 14
0

Data is cheap.

I would suggest duplicating the data as long as its relatively simple like artist name, album name, and folder name. Especially if you don't think they will change. But if they change you'll have to update them on each song of course.

If you start doing includes for simple things like artist name, then you'll be adding a ridiculous amount of complexity when its not necessary.

For artists/albums/genre/etc you can build map-reduce indexes that group the songs by artist or genre or whatever you're interested in. The result of the map-reduce can be whatever you want, just a list of song ids or you can include a list of all the song data. Then query the index by whatever you're grouping on.

Because artist/album/genre are so tightly coupled to the songs - you might benefit from letting your songs define what artists and albums are in the library, instead of having separate documents for them. This makes it easier to add/edit/delete songs - if you add a song with a new artist - suddenly u have a new artist! If you delete all the songs of a given album - suddenly the album is gone!

If you want to implement something like playlists (which should have their own documents) - the playlist document could just have a list of song ids, and when you load the playlist you can easily do an include for all the songs.

For a more complex scenario - if you wanted to show a list of the users playlists along with some overall data about the songs included (e.g. what genres of songs are in this playlist?) you can build an index that loads all the associated songs for each playlist and spits out a list of genres from the songs. Then just query the index.

Ben Wilde
  • 5,552
  • 2
  • 39
  • 36
0

Good reading about document stores vs relational databases may be found in this blog post. Furterhmore, it shows a bit of how one can store a Movie database in a document store (which I feel it is pretty similar to a Music store in terms of documents relationships).

In RavenDB you can create Map/Reduce indexes that can be used to help you merging information from different documents and it is usually cheaper (as stated by @Jaynard) than loading documents in index time (i.e. using LoadDocument).

public class Song
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string ArtistId { get; set; }
}

public class Artist
{
    public string Id {get;set;}
    public string Name {get;set;}
}

public class SongsByArtist : AbstractMultiMapIndexCreationTask<SongsByArtist.ArtistSongs>
{
    public class ArtistSongs
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public IEnumerable<object> Songs { get; set; }
    }

    public SongsByArtist()
    {
        AddMap<Artist>(artists => from artist in artists
                                  select new ArtistSongs
                                  {
                                      Id = artist.Id,
                                      Name = artist.Name,
                                      Songs = new List<object>()
                                  });

        AddMap<Song>(songs => from song in songs
                              select new ArtistSongs
                              {
                                  Id = song.ArtistId,
                                  Name = null,
                                  Songs = new List<object> { new { song.Id, song.Name } }
                              });

        Reduce = results => from result in results
                            group result by result.Id
                                into g
                                select new ArtistSongs
                                {
                                    Id = g.Key,
                                    Name = g.First(x => x.Name != null).Name,
                                    Songs = g.SelectMany(x => x.Songs)
                                };
    }
}

And a test to prove this:

public class CanGetArtistSongs : RavenTestBase
{
    [Fact]
    public void WillSupportLast()
    {
        using (var store = NewDocumentStore())
        {
            using (var session = store.OpenSession())
            {
                session.Store(new Artist { Id = "artists/1", Name = "Pink Floyd" });
                session.Store(new Song { Name = "Shine On You Crazy Diamond Part I", ArtistId = "artists/1"});
                session.Store(new Artist { Id = "artists/2", Name = "Metallica" });
                session.Store(new Song { Name = "Whiplash", ArtistId = "artists/2"});
                session.Store(new Song { Name = "One", ArtistId = "artists/2"});
                session.SaveChanges();
            }

            new SongsByArtist().Execute(store);

            using (var session = store.OpenSession())
            {
                var results = session.Query<SongsByArtist.ArtistSongs, SongsByArtist>()
                                     .Customize(customization => customization.WaitForNonStaleResults())
                                     .Where(x => x.Name == "Metallica")
                                     .ToList();

                Assert.Empty(store.DatabaseCommands.GetStatistics().Errors);
                Assert.Equal(2, results.First().Songs.Count());
            }
        }
    }
}
Ricardo Brandão
  • 1,050
  • 7
  • 15