3

i'm trying to upgrade an old CMS to use NHibernate and can't deter from the original database structure much. Here is the bit which is causing an issue. Say i have the following 2 tables:

Articles: 
- Id (PK, Identity)
- Title 
- Content 

Meta: 
- ArticleId (PK, FK to Articles)
- Description 
- Keywords 

I have created the following classes:

public class Article { 
  public virtual int Id { get; set; } 
  public virtual string Title { get; set; } 
  public virtual string Content { get; set; } 
} 

public class Meta : IComponent { 
  public virtual string Description { get; set; } 
  public virtual string Keywords { get; set; } 
}

public interface IComponent {
}

Usually the Meta would normally be mapped as a component (or a one to one relationship) property on the Article class. However in the application i'm building an admin can enable/disable the components that apply to articles. Also i'd like them to extend the application to add their own components without touching the Article class.

For them reasons i can't add a property against the Article class. Now ideally in my code i'd like to be able to say:

var articles = session.Query<Article>()
    .Fetch(a = a.Component<Meta>())
    .Where(a => a.Component<Meta>().Keywords.Contains("Some Word"))
    .ToList();

// This wouldn't generate an extra SQL statement
var keywords = articles[0].Component<Meta>().Keywords;

Which would generate the following SQL (or similar):

SELECT * FROM Articles INNER JOIN Meta ON Articles.Id = Meta.ArticleId WHERE Meta.Keywords LIKE '%Some Word%'

Is it possible to map the Component method so that it does an inner join to get the Meta. The concept seems pretty simple but i don't have a clue where begin. I'd really appreciate the help.

Thanks

nfplee
  • 7,643
  • 12
  • 63
  • 124

3 Answers3

0

In your NHibernate mapping file, you can specify the fetch type. The spec for the one-to-one xml is located in the NHibernate Documentation. Notice that number 5 has an option of Join and Select and it defaults to select.

Zoidberg
  • 10,137
  • 2
  • 31
  • 53
  • How does that help me map Component()? – nfplee Jul 05 '11 at 12:48
  • The xml determines the details of the mapping between tables. Component is just a type in your objects. You will need to put a property into your Article class called Meta of type Compontent, then define the relationship between that property and meta (as a one-to-one) inside of your configuration XML. – Zoidberg Jul 05 '11 at 13:26
  • Still abit confused. Could you provide a more detailed answer with an example please. – nfplee Jul 06 '11 at 15:17
0

Given this:

public class Article
{
    public virtual int ArticleId { get; set; }
    public virtual string Title { get; set; }
    public virtual string Content { get; set; }
}


public class Meta : IComponent
{
    public virtual Article Article { get; set; }

    public virtual int MetaId { get; set; }
    public virtual string Description { get; set; }
    public virtual string Keywords { get; set; }
}

AFAIK, you cannot Fetch something that isn't part of an entity. So from your example, it's not possible to fetch Meta from the Article entity.

So if you want to fetch the other info of an Article, you just have to join Article to them, then project the complete data in your Linq, example:

var articles =
        from a in s.Query<Article>()
        join m in s.Query<Meta>() on a equals m.Article
        where m.Keywords.Contains("Some Word")
        select new { a, m };

foreach(var x in articles)
    Console.WriteLine("{0} {1}", x.a.Title, x.m.Description);

Resulting query:

select *
from [Article] article0_, [Meta] meta1_ 
where meta1_.ArticleId = article0_.ArticleId 
    and meta1_.Keywords like '%Some Word%'

Another approach, start from Meta, then fetch the Article; on query, this will join the Article immediately, i.e. no lazy loading:

var artB =
        from m in s.Query<Meta>().Fetch(x => x.Article)
        where m.Keywords.Contains("Some Word")
        select m;

foreach (var x in artB)
    Console.WriteLine("{0} {1}", x.Article.Title, x.Description);

Resulting query:

select *
from [Meta] meta0_ 
left outer join [Article] article1_ on meta0_.ArticleId = article1_.ArticleId 
where meta0_.Keywords like '%Some Word%'

To keep an Article having only one Meta, put a Unique on Meta's reference:

create table Article
(
ArticleId int identity(1,1) not null primary key,
Title varchar(100) not null,
Content varchar(100) not null
);

create table Meta
(
    -- this prevents an Article having two Meta
ArticleId int not null references Article(ArticleId) unique, 

MetaId int identity(1,1) not null primary key,

Description varchar(100) not null,
Keywords varchar(100) not null
);

insert into Article(Title,Content) values('Great','Yeah')

insert into Meta(ArticleId, Description, Keywords) values(1,'Oh','Some Word');
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • Hi thanks for your answer. The second option gives me strong typing but if i had another component say Meta2 then it falls down. I like the idea in the first approach (the linq query is simple and allows me to add joins to additional components). However it does require a little more work than i'd ideally like as i'd have to create an additional model for the view to make it strongly typed. If there's a way that i could say article.Component().Description (note the Article class/mapping can be modified accordingly) it would be great. If not then cheers for your help anyway. – nfplee Jul 18 '11 at 15:29
  • Nice idea there :-) though if someone will make that feasible in Linq-to-NH, this won't suffice: `Fetch(a => a.Component())` as projections need an alias(evaluated at design-time by the compiler), so to nitpick, this would be the API `Fetch(a => new { art = a, meta = a.Component() })`. Try to e-mail someone from NHibernate if they could accommodate that idea. – Michael Buen Jul 18 '11 at 15:48
  • Ok will do. Cheers for your help. – nfplee Jul 19 '11 at 12:43
  • My guess is that either you make it part of the entity Article in its mapping or you make it a one-to-many. Problem in your query is that you start at the article class while it is obvious that you should start with your component. This way you can eager load both the component and the article with a join. Just as Micheal mentions. This approach works with multiple IComponent types unless you want to query multiple components at once as then you need to join then which is obvious. – Ramon Smits Jul 19 '11 at 13:08
0

Would the following solution be of interest?

You can make a protected mapping to your components and access these from this public generic method.

This way you can choose to eager/lazy load your components.

public class Article
{
    protected virtual ICollection<IComponent> Components { get; set; }

    public virtual T Component<T>() where T : IComponent
    {
        return Components.FirstOrDefault(c=>c.Type==typeof(T));
    }
}
Ramon Smits
  • 2,482
  • 1
  • 18
  • 20
  • Hi, cheers this looks promising. I'm not quite sure how i would map the Components though. My guess is i'd need a table in my database to say which components apply to the article. Please let me know your thoughts on this. Thanks – nfplee Jul 20 '11 at 08:52