Most of general tips provided by Radim Köhler are true, but regarding the original problem of formulating a formula - NH (at least >=5, I don't know how's that with older ones) should be able to cover that case.
First of all, your attempt would fail, because in formulas any column names referring to anything other than the current main entity must be prefixed with aliases. Otherwise, NH will assume that the column name refers to the current main entity.
For example, your original attempt was:
Map(x => x.CityName).Formula("select Name from City c where c.street = STREET");
^ fault ^CEE ^CEE ^fault
Note how you added c
prefix to street
to disambiguate which street
is from City and which one isn't. NHibernate will detect such prefixes and will assume all columns with prefixes are bound to tables specified in the sql text. So, c.street
will be assumed to be not important. However, STREET
and Name
don't have prefixes. NH will assume that they come from the main entity, the Person that you are defining mapping for. That may yield funny results sometimes, but more likely, there's no Name
nor Street
columns on Person table and you'll get an error from rdbms.
To fix that, you should have something like:
Map(x => x.CityName).Formula("select c.Name from City c where c.street = a.STREET");
^CEE ^AYE
Of course, now another error will say that prefix a
is not known. Sure, we don't have it defined anywhere, since we have PERSON and CITY and the intermediate ADDRESS is totally ommitted in the sql query.
Now, take a look at this:
Map(x => x.CityName)
.Formula(@"
select c.Name
from City c
where c.street =
( select a.Street
from Address a
where a.address_id = address_id
)"); ^
^ no prefix!
Since inner address_id
has no prefix, NH will assume it is from Person. Great. There are no other unprefixed columns so all other columns are ignored and assumed to be bound to scopes defined in the SQL text itself.
Thus, the inner subquery will select the Address basing on Person.AddressID, and pick the street from that. Hopefully it's just one street since we match addresses by ID. After finding the street the outer subquery will use it to match a City.
We also could write the formula as
select c.Name
from Address a
inner join City c no c.street = a.street
where a.address_id = address_id
with the same effect and probably different performance.
One thing to note, when you write a subquery Formula or when you write where foo = (select x from...)
in subquery, you MUST ensure that the subquery always returns zero or one result. Not two or more. Many RDBMSes treat such thing as an error. In case of this query, the same street
name may occur in multiple Cities so you have high chance of failure just because you match address to City by Street.