Learning in the Open: II – first relation and more ActiveRecord

It took a lit­tle longer than I planned but here we go again. In the mean­time ActiveRe­cord 2.1 was released, and soon after that a minor update bring­ing one cool big fea­ture. From now on we’ll be work­ing on ver­sion 2.1.2. Pick­ing up from where we left off last time. We have a user entity. Since we’re build­ing a web­site where users can pub­lish bench­mark results, we’ll cre­ate now a bench­mark entity, and cre­ate a rela­tion between these two.

I want to see results!

Let’s start by adding an appro­pri­ate field to the User class:

private readonly ICollection<BenchmarkResult> benchmarkResults = new HashSet<BenchmarkResult>();

We also cre­ate a property:

public IEnumerable<BenchmarkResult> BenchmarkResults

{

    get

    {

        foreach (var result in benchmarkResults)

        {

            yield return result;

        }

    }

}

So far this is just a reg­u­lar prop­erty. To map it as a one-to-many rela­tion we use the Has­Man­y­At­tribute.

[HasMany(Access = PropertyAccess.FieldCamelcase, 

    Cascade = ManyRelationCascadeEnum.SaveUpdate, 

    RelationType = RelationType.Set,

    Inverse = true)]

public IEnumerable<BenchmarkResult> BenchmarkResults

There’s quite a lot going on here, so let’s go over it piece by piece

  • Access prop­erty Field­Camel­case spec­ify we want ActiveRe­cord (and NHiber­nate under­neath it) to go to the field directly, which makes sense since we’re expos­ing it as mere enumerable.
  • Cas­cade spec­i­fies that when sav­ing or updat­ing our user, all new and changed bench­mark results in the col­lec­tion should also be appro­pri­ately saved or updated.
  • Usu­ally we wouldn’t have to spec­ify type of the rela­tion. Usu­ally it will infer it from the kind of col­lec­tion we expose, and would use set for ISet, map for IDic­tionary, bag for ICol­lec­tion etc. How­ever since we’re expos­ing only IEnu­mer­able it does not have enough infor­ma­tion to decide, that’s why we have to be explicit here.
  • We also spec­ify Inverse prop­erty to be true, which basi­cally means that it’s child’s task to main­tain the rela­tion­ship. That also means that child needs to have a ref­er­ence to the par­ent.

Let’s now build our Bench­markRe­sult class.

[ActiveRecord]

public class BenchmarkResult : ActiveRecordLinqBase<BenchmarkResult>

{

    protected BenchmarkResult()

    {

    }

 

    public BenchmarkResult(User user, string benmchmarkName, string computerModel, double score)

    {

        if (user == null)

        {

            throw new ArgumentNullException("user");

        }

        if (benmchmarkName == null)

        {

            throw new ArgumentNullException("benmchmarkName");

        }

        if (computerModel == null)

        {

            throw new ArgumentNullException("computerModel");

        }

 

        User = user;

        BenmchmarkName = benmchmarkName;

        ComputerModel = computerModel;

        Score = score;

    }

}

So far there’s noth­ing new here. Com­puter con­fig­u­ra­tion and bench­mark will become enti­ties them­selves soon, but let’s not get ahead of ourselves.

The only inter­est­ing prop­erty at this point is the User.

[BelongsTo]

public User User { get; private set; }

It has a BelongsToAt­tribute to denote it points to another entity (on our case the ‘one’ end of our one-to-many).

Let’s now let our users to actu­ally save bench­mark results, and we’re more or less done:

public BenchmarkResult RunBenchmark(string benchmarkName, string computerModel, double score)

{

    var result = new BenchmarkResult(this, benchmarkName, computerModel, score);

    benchmarkResults.Add(result);

    return result;

}

Test

We now have all the logic in place, so let’s build a test:

[Fact]

public void Can_perform_benchmark_runs()

{

    var stefan = new User

    {

        Email = "stefan@gmail.com",

        Name = "Stefan",

        Password = "Super compilcated password!",

        About = "Stefan is a very cool."

    };

    stefan.RunBenchmark("Foo bar!", "AyeMack Pro", 3.2);

    stefan.Save();

 

    var user = User.FindAll().Single();

 

    Assert.NotEmpty(user.BenchmarkResults);

    Assert.Equal(1, user.BenchmarkResults.Count());

 

    var result = user.BenchmarkResults.Single();

 

    Assert.NotNull(result);

    Assert.Equal("Foo bar!", result.BenmchmarkName);

    Assert.Equal("AyeMack Pro", result.ComputerModel);

    Assert.Equal(3.2, result.Score);

}

If we run it now, it will fail. Good news is, that it does not have any­thing to do directly with our logic. Bad news is, that we have a pass­ing test nonethe­less, so let’s have a look at it.

Error

We get “Incor­rect syn­tax near the key­word 'User'.” error mes­sage. Here’s the SQL that was sent to the database:

error_sql

Looks good doesn’t it? Well not – really, User is a SQL Server key­word, as we can’t just use it as iden­ti­fier – we have to escape it. To do it, we have to spec­ify the col­umn name explic­itly, and escape it by enclos­ing it within two ` char­ac­ters (located above tab key on my keyboard).

Yes I’m aware of hbm2ddl.keywords auto-quote. How­ever I had some issues get­ting it to work with ActiveRe­cord. Any help doing this will be appreciated.

[BelongsTo(Column = "`User`")]

public User User { get; private set; }

Now the test will pass, and the fol­low­ing SQL will be generated:

ok_sql

Now that we have the cor­rect SQL, let’s look at what our schema looks like:

schema_inverse_true

  • Thi­lak Nathen

    hbm2dll auto quot­ing would be a much ele­gant solution.

  • http://kozmic.pl/Default.aspx Krzysztof Koźmic

    @Thilak Nathen,

    Be so kind to read the entire post before commenting.

  • Thi­lak Nathen

    Ahh, sorry dude… din see your dis­claimer. Missed that bit.

    I'm still won­der­ing why auto-quoting is not enabled by default for the MsSql200x dialect.

  • Fred Legrain

    Thank you for the series!

    About escap­ing the User col­umn name, do you know the rea­son why this is not han­dled by the Hiber­nate SQL dialect?

    Cheers,
    Frédéric

  • http://kozmic.pl/Default.aspx Krzysztof Koźmic

    Frédéric,

    actu­ally NHiber­nate pro­vides this capa­bil­ity. How­ever I had issues using it, which turned out to be prob­a­bly a bug with NHiber­nate. Once I know more, I'll address this in future post.