5

I have a simple model, consisting of a document that references one or more article using a reference object (this is because in the domain, we do not own the articles so we can only reference them).

I'm trying to write a query that lists the documents, printing the ID and a string consisting of a comma separated list of article numbers. For example:

ID ARTICLES
------------------
1  ACC, PE2,
2  ER0, AQ3, FEE
3  PE2

My problem is with selecting the comma separated list.

Here are the domain classes:

// The Entity class has an Id property.
public class Document : Entity
{
    public virtual IEnumerable<ArticleReference> ArticleReferences { get; set; }
    public virtual DateTime ReceiveDate { get; set; }
}

// The ValueObject does not have an Id property ofcourse.
public class ArticleReference : ValueObject
{
    public virtual string ArticleNumber { get; set; }
    public virtual string ArticleName { get; set; }
}

The article reference is a value object so it does not have an ID of its own.

This is the view model that represents an item in the result list:

public class DocumentListItemModel
{
    public int Id { get; set; }
    public string ArticleNumbers { get; set; }
    public string ReceiveDate { get; set; }
}

And here's the query class I have come up with so far:

public class DocumentQuery
{
    public IList<DocumentListItemModel> ExecuteQuery()
    {
        IntermediateModel model = null;
        ArticleReference articleReferenceAlias = null;

        return Session
            .QueryOver<Document>()
            .JoinAlias(n => n.ArticleReferences, () => articleReferenceAlias);
            .SelectSubQuery(
                QueryOver.Of<ArticleReference>(() => articleReferenceAlias)
                    // There is no way of matching references to documents from a domain
                    // point of view since the references are value objects and
                    // therefore don't have an ID.
                    .Where(n => ...)
                    .Select(q => articleReferenceAlias.Number))
                .WithAlias(() => model.ArticleNumbers)
            .TransformUsing(Transformers.AliasToBean<IntermediateModel>());
            .Future<IntermediateModel>()
            .ToList()
            .Select(n =>
                new DocumentListItemModel()
                {
                    Id = n.Id,
                    ArticleNumbers = string.Join(", ", n.ArticleNumbers.OrderBy(p => p)),
                    ReceiveDate = n.ReceiveDate.ToString("d", CultureInfo.CurrentCulture)
                })
            .ToList();
    }

    private class IntermediateModel
    {
        public int Id { get; set; }
        public IEnumerable<string> ArticleNumbers { get; set; }
        public DateTime ReceiveDate { get; set; }
    }
}

As you can see, I can't express the .Where statement because there is no way of matching references to documents from a domain point of view. The references are value objects and therefore don't have an ID.

The question is: how do I fix the query to properly select the list of article numbers so I can use it in my string.Join statement to make the comma separated string?

Sandor Drieënhuizen
  • 6,310
  • 5
  • 37
  • 80
  • have you tried adding public int? DocumentId {get;set;} to ArticleReference? ps aren't the properties supposed to be virtuals in nhibernate – Shaun Wilde Aug 25 '11 at 05:19
  • You're right about the virtuals, I updated my question. I had forgotten about them when writing the conceptual code (the real code is too distracting due to its size). Regarding adding DocumentId to ArticleReference: that would defy the whole idea of value objects, effectively turning it into an entity. – Sandor Drieënhuizen Aug 25 '11 at 08:04
  • true - but because of issues like these I tend to have a separate domain model to my application model so I add funnies like adding a parentid property to a child entity. – Shaun Wilde Aug 25 '11 at 09:55
  • @Shaun Wilde Properties only need to be virtual if you are lazy-loading. Of course, most of the time you'll be using lazy-loading but they aren't a necessity in NHibernate. – docmanhattan Aug 25 '11 at 16:07
  • @Sandor: why do you have this ArticleReference class?? I can't see the point of that. Why not just referencing articles? It would make life much easier ... – Stefan Steinegger Aug 26 '11 at 11:34
  • @Stefan, I have this class because I will be storing more than just an ID (an article name, for example). I'm working on an archiving application and all archived data must still make sense when the referenced data (which lives in external systems) is gone. – Sandor Drieënhuizen Aug 26 '11 at 20:09

2 Answers2

1

I think you are taking the definition of value object too literally. Assigning a surrogate identifier (identity column, Guid, etc.) to a value object does not make it any less of a value object. It's a value object because its equality is based its values, not its identity. This does not require that a value object cannot have an identity, and in practice it almost always has to.

Your application obviously has to be able to link a Document to a set of ArticleReferences and the best way to accomplish that is by adding an ID to ArticleReference.

Jamie Ide
  • 48,427
  • 16
  • 81
  • 117
  • 1
    As for giving a value object an identity, I can only think of valid reasons that concern compensating for technical limitations elsewhere, such as in the persistence layer. From a domain point of view, a value object identity is superfluous (unless the domain itself is flawed) because the value object IS the identity. I understand where you're coming from but since I have managed to work out a solution (which I'll post here soon) for this problem, again there seems to be no need to add an identity so I'll stick with my point of view until further notice :) – Sandor Drieënhuizen Sep 07 '11 at 15:28
0

I managed to solve the problem. This is what I ended up with:

public IList<DocumentListItemModel> ExecuteQuery()
{
    ArticleReference articleReferenceAlias = null;

    return Session
        .QueryOver<Document>()
        .JoinAlias(n => n.ArticleReferences, () => articleReferenceAlias,
            JoinType.LeftOuterJoin)
        .SelectList(list => list
            .Select(n => n.Id)
            .Select(n => articleReferenceAlias.Number))
        .List<object[]>()
        .Select(x => new
        {
            Id = (int)x[0],
            ArticleNumber = (string)x[1]
        })
        .GroupBy(n => n.Id).Select(n =>
        {
            return new DocumentListItemModel
            {
                Id = n.First().Id,
                ArticleNumbers = string.Join(", ", n.Select(p => p.ArticleNumber))
            };
        }).ToList();
    }
}

I couldn't use the alias-to-bean transformer anymore because it cannot handle collection properties. That's why the solution has the GroupBy, it consolidates the rows, aggregating the article numbers into a string.

Sandor Drieënhuizen
  • 6,310
  • 5
  • 37
  • 80