3

I've found very similar questions here but none that match exactly what I'm looking for. The two closest threads I've found are (yes, they are different threads):

NHibernate many-to-many criteria (1)

NHibernate many-to-many criteria (2)

However, I think both of those are using direct Many-to-Many relationships. I am actually simulating the Many-to-Many relationship by having two One-to-Many relationships with a junction table, which is pretty standard practice. Here are my NHibernate mappings:

Files:

<class name="Files" table="files">
  <id name="id">
    <generator class="identity" />
  </id>
  <property name="name" />

  <bag name="files_attrs" table="files_attrs" lazy="true">
    <key column="file_id" />
    <one-to-many class="Files_Attrs" />
  </bag>
</class>

Attributes:

<class name="Attrs" table="attrs">
  <id name="id">
    <generator class="identity" />
  </id>
  <property name="name" />
  <property name="value" />

  <bag name="files_attrs" table="files_attrs" lazy="true">
    <key column="attr_id" />
    <one-to-many class="Files_Attrs" />
  </bag>
</class>

Joiner:

<class name="Files_Attrs" table="files_attrs">
  <id name ="id">
    <generator class="identity" />
  </id>
  <many-to-one name="file" cascade="all" column="file_id" />
  <many-to-one name="attr" cascade="all" column="attr_id" />
</class>

So my problem is exactly like the second link above, but done with a Junction Table. So:

Given a set of Attribute IDs, I'm hoping to run a query that gives me the files that have ALL of those matching Attributes. I can easily run "n" queries for each Attribute ID in the set and compare each list for File IDs that appear in every list, but I feel like there should be an easier way to do this all at once with one query.

Example:

File      | Attributes
----------+-----------------------------------------------------
foo.txt   | (mode = read-only,                    view = visible)
bar.txt   | (mode = read-write, security = all,   view = visible)
duck.txt  | (mode = read-only,                    view = hidden)
goose.txt | (more = read-only,  security = owner, view = visible)

Given these attributes: mode = read-only and view = visible, I want to be returned only foo.txt and goose.txt.

Can anyone help me with this? Thanks.

jchensor
  • 107
  • 1
  • 7
  • Have you tried the accepted answer from #2? It seems it should work even though your relationships are implemented differently. – FuzzyTree May 29 '14 at 16:28

2 Answers2

1

One way how to achieve this, could be to create as many subqueries joined by AND, as many attributes must be found/related to searched files

I searching for name / value

The first solution works with the name/value pairs, from upper layer. I.e user selected mode to be read-only... (the second will be a bit easier, expecting that we already have ID's of the searched Atttributes)

// Below I am using C# properties, which I guess are correct
// based on the mapping. Naming convention is more Java (camel)
// but this should work with above mapping 
// (also - class name Contact, not File)

Files file = null; // this is an alias used below

// here the attributes collection represents search filter
// ... settings for which is user looking for
var attributes = new List<Attrs>
{
    new Attrs{ name = "mode", value = "read-only" },
    new Attrs{ name = "view", value = "visible" }
};

// Let's start with definition of the outer/top query
// which will return all files, which do meet all filter requirements
var query = session.QueryOver<Files>(() => file);

In the next step, we will iterate through attributes, i.e. filters collection

// here we will take each attribute and create a subquery
// all these subqueries, will be joined with AND
// so only these files, which do have all attributes, will be selected
foreach (var attr in attributes)
{
    // create the subquery, returning the FileId
    Attrs attribute = null;
    var subQueryForAttribute = QueryOver.Of<Files_Attrs>()
            .JoinQueryOver(fa => fa.attr, () => attribute)
            .Select(x => x.file.id)
            ;

    // now, take name and value
    var name = attr.name;
    var value = attr.value;

    // and convert them into where condition
    subQueryForAttribute.Where(() => attribute.name == name);
    subQueryForAttribute.Where(() => attribute.value == value);

    // finally, add this subquery as a restriction to the top level query
    query.WithSubquery
        .WhereProperty(() => file.id)
        .In(subQueryForAttribute);
}

Now we have a query, which is ready to support paging - because we are working on a flat structure of files. So we can use Take and skip if needed and then get the list of searched files

// query.Take(25);
// query.Skip(100);

var list = query.List<Files>();

This is a query which will result in a SELECT like this

SELECT ...
FROM files
WHERE id IN (SELECT file_Id FROM files_attrs 
                              INNER JOIN attrs ON attrs.id = file_attrs.attr_id
                            WHERE name = 'mode' AND value = 'read-only' )
  AND id IN (SELECT file_Id FROM files_attrs 
                              INNER JOIN attrs ON attrs.id = file_attrs.attr_id
                            WHERE name = 'view' AND value = 'visible' )

II searching by attributes ID

The second solution, has easier starting conditions, instead of attributes (name and value) we already have their Ids (cite from a question:)

Given a set of Attribute IDs, I'm hoping to run a query that gives me the files that have ALL of those matching Attributes.

// Below I am using C# properties, which I guess are correct
// based on the mapping. Naming convention is more Java (camel)
// but this should work with above mapping 
// (also - class name Files, not File)

Files file = null; // this is an alias used below

// here the attributeIds collection represents attributes to be found
var attributeIds = new List<int> { 1, 4, 5 };

// Let's again start with definition of the outer/top query
// which will return all files, which do meet all filter requirements
var query = session.QueryOver<Files>(() => file);

Next is the iteration through the set of known IDs which must exist as relation (all of them)

// here we will take each attribute and create a subquery
// all these subqueries, will be joined with AND
// so only these files, which do have all attributes, will be selected
foreach (var attrId in attributeIds)
{
    // create the subquery, returning the Files.id
    var subQueryForAttribute = QueryOver.Of<Files_Attrs>()
            // no need to join, all the stuff is in the pairing table
            .Select(x => x.file.id)
            ;
    var id = attrId; // local variable
    // and convert them into where condition
    subQueryForAttribute.Where(pair => pair.attr.id == id);

    // finally, add this subquery as a restriction to the top level query
    query.WithSubquery
        .WhereProperty(() => file.id)
        .In(subQueryForAttribute);
}

var list = query.List<Files>();

The solution with known IDS is a bit easier (less tables are needed in SQL statemenets)

NOTE: have to say: it is great to see, that you've introduced the many-to-one and one-to-many instead of many-to-many. I would, personally, say that exactly this example shows, how big profit it could bring... ability to search even with complex filters

Some links, to show the power of the QueryOver: Query on HasMany reference , and some good reason why not to use many-to-many mapping: many-to-many with extra columns nhibernate

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Thanks, I don't want it to seem like I haven't seen your response, but I've been trying to solve another problem that's taken precedence over this one at the moment. When I get back to this issue, I'll look this over and try it out and write up a new comment! So thank you in advance for you help so far! ^_^ – jchensor May 30 '14 at 01:47
  • Clear :) would be nice if you later let me know;) This technique - adding all restrictions in a queue (`WHER` and `AND`) - is working even with subqueries *(as shown above)*. We are using this to search for people by their City in Addresses, Articles by their Tag names ... good luck with NHibernate ;) – Radim Köhler May 30 '14 at 04:25
  • Hi Radim, I actually chose the path of the many-to-one and one-to-many option from reading a lot on the subject here on Stackoverflow, and I believe most of the arguments for it came from your posts here. ^_^ In any case, I'm trying the code you have above but I'm getting a compilation error on this line: `var query = session.QueryOver(() => file);` The error is: `"Cannot convert lambda expression to type 'string' because it is not a delegate type"` and I also get the error `"The name 'file' does not exist in the current context"` Should I be defining that somewhere first? – jchensor May 30 '14 at 22:30
  • I re-tested all the parts *(in my example with a bit different names, but the same structure)* and fixed all the little issues. The code, if it does match 1:1 to the xml mapping above, should be compile-able. Please, let me know. I am sure you will see what you want ;) ... without `many-to-many` ;) – Radim Köhler May 31 '14 at 03:47
  • 1
    Radim, your code absolutely worked. Thanks SO MUCH. I appreciate it, man! I understand parts of it, but I don't understand it 100% yet. I'll definitely try to make sure I understand all of it (I'll be honest, I'm not good at SQL), but for now at least this will help me meet my deadline. ^_^ After that, I'll definitely go back and re-process it all. Again, thanks so much, Radim. REALLY appreciate it! – jchensor Jun 04 '14 at 23:53
0

Im not sure if this is what you need:

<bag name="files_attrs" table="files_attrs" lazy="true" where="something like '%mode = read-only%' and something like '%view = visible%'">
    <key column="attr_id" />
    <one-to-many class="Files_Attrs" />
</bag>

Where something is the attribute or is the column where is the data to filter.

Try this query:

Files fAlias = null;
Attrs aAlias = null;

var disjunction = new Disjunction();

disjunction.Add(Restrictions.On(() => aAlias.value)
    .IsLike("mode = read-only", MatchMode.Anywhere));
disjunction.Add(Restrictions.On(() => aAlias.value)
    .IsLike("view = visible", MatchMode.Anywhere));

var subquery = QueryOver.Of<Files_Attrs>
    .Inner.JoinAlias(x => x.file, () => fAlias)
    .Inner.JoinAlias(x => x.attr, () => aAlias)
    .Where(disjunction)
    .Select(() => fAlias);

var files = session.QueryOver<Files>
    .WithSubquery.WhereExists(subquery)
    .List();
Najera
  • 2,869
  • 3
  • 28
  • 52
  • Hi, Najera. Thanks for the response. Actually, I'm looking mostly for how to write the NHibernate query to achieve what I'm looking for. I have my mappings and tables and objects defined, I'm just not sure if there is a simple query that will return me the response I'm looking for in my example at the end of my question. – jchensor May 27 '14 at 22:55
  • I added one query in my question. – Najera May 28 '14 at 06:40
  • Thanks, I don't want it to seem like I haven't seen your response, but I've been trying to solve another problem that's taken precedence over this one at the moment. When I get back to this issue, I'll look this over and try it out and write up a new comment! So thank you in advance for you help so far! ^_^ – jchensor May 30 '14 at 01:41