Last time I went over going from separate web applications per tenant to a shared web application for all tenants, but each tenant still had its own database. Now we’re going to take the next step and let multiple tenants share the same database. After we add tenant_id to most of the tables in our database we’ll need the application to take care of a few things. First, we need to apply a where clause to all queries to ensure that each tenant sees only their data. This is pretty painless with NHibernate, we just have to define a parameterized filter:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<filter-def name="tenant">
<filter-param name="id" type="System.Int32" />
</filter-def>
</hibernate-mapping>
And then apply it to each entity:
<class name="User" table="[user]">
<id name="Id" column="user_id">
<generator class="identity" />
</id>
<property name="Username" />
<property name="Email" />
<filter name="tenant" condition="tenant_id = :id" />
</class>
The last step is to set the value of the filter at runtime. This is done on the ISession like this:
Container
.RegisterType<ISession>(
new PerRequestLifetimeManager(),
new InjectionFactory(c =>
{
var session = c.Resolve<ISessionFactory>().OpenSession();
session.EnableFilter("tenant").SetParameter("id", c.Resolve<Tenant>().Id);
return session;
})
);
The current tenant comes from c.Resolve<Tenant>(). In order for that to work, you have to tell Unity how to find the current tenant. In ASP.NET MVC, we can look at the host header on the request and find our tenant that way. We could just as easily use another strategy though. Maybe if this were a WCF service, we could use an authentication header to establish the current tenant context. You could build out some interfaces and strategies around establishing the current tenant context, however for this article I’ll just bang it out.
Container
.RegisterType<Tenant>(new InjectionFactory(c =>
{
var repository = c.Resolve<ITenantRepository>();
var context = c.Resolve<HttpContextBase>();
var host = context.Request.Headers["Host"] ?? context.Request.Url.Host;
return repository.FindByHost(host);
}));
Second, we have to set the tenant_id when new entities are saved. This is a bit more complicated with NHibernate and requires a bit of a concession in that we have to add a field to the entity in order for NHibernate to know how to persist the value. I’m using a private nullable int for this.
public class User
{
private int? tenantId;
public virtual int Id { get; set; }
public virtual string Username { get; set; }
public virtual string Email { get; set; }
}
It’s private because I don’t want the business logic to deal with it and it’s nullable because my tenant table is in a separate database which means I can’t lean on the data model to enforce referential integrity. That’s a problem because the default value for an integer is zero which could be happily saved by the database. By making it nullable I can be sure the database will blow up if the tenant_id is not set.
So, back to the issue at hand. The tenant_id needs to be set when the entity is saved. For this, I’m using an interceptor and setting the value in the OnSave method:
public class MultiTenantInterceptor : EmptyInterceptor
{
private readonly Func<Tenant> tenant;
public MultiTenantInterceptor(Func<Tenant> tenant)
{
this.tenant = tenant;
}
public override bool OnSave(object entity... object[] state, string[] propertyNames...)
{
var index = Array.IndexOf(propertyNames, "tenantId");
if (index == -1)
return false;
var tenantId = tenant().Id;
state[index] = tenantId;
entity
.GetType()
.GetField("tenantId", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(entity, tenantId);
return false;
}
}
This IInterceptor mechanism is a little wonky. If you change any data, you have to do it in both the entity instance and the state array that NHibernate uses to hydrate entities. It’s not a big deal, it’s just one of those things you have to accept like the fact that Apple and Google are tracking your every move via your smart phone. Oh, and the interceptor gets wired up like this:
Container
.RegisterType<ISessionFactory>(
new ContainerControlledLifetimeManager(),
new InjectionFactory(c =>
{
return new NHibernate.Cfg.Configuration()
.Configure()
.SetInterceptor(new MultiTenantInterceptor(() => c.Resolve<Tenant>()))
.BuildSessionFactory();
})
);
We’re almost done. There is one more case that needs to be handled. When NHibernate loads an entity by its primary key, it doesn’t run through the query engine which means the tenant filter isn’t applied. Fortunately, we can take care of this in the interceptor:
public class MultiTenantInterceptor : EmptyInterceptor
{
...
public override bool OnLoad(object entity... object[] state, string[] propertyNames...)
{
var index = Array.IndexOf(propertyNames, "tenantId");
if (index == -1)
return false;
var entityTenantId = Convert.ToInt32(state[index]);
var currentTenantId = tenant().Id;
if (entityTenantId != currentTenantId)
{
throw new AuthorizationException("Permission denied to {0}", entity);
}
return false;
}
}
That’s it. Have fun and happy commingling.
This is pretty cool from an academic standpoint. I don’t really like the onsave/onload mechanisms to make NHibernate handle commingled entities correctly. I’ve done some work with NHibernates event handlers and interceptors, and I haven’t been impressed with their consistency.
So what’s the verdict? Commingled or separate database?
I chose an interceptor over an event listener because the interceptor is more predictable in that it gets called every time on save. The downside is you only get one interceptor, whereas with listeners you can have several of them. The downside though is that you have to listen to Save, SaveOrUpdate, SaveOrUpdateCopy and maybe one or two more that I can’t remember off the top of my head.
As for which multi-tenancy architecture is better (co-mingled or not) is one of those “it depends” answers. It’s certainly simpler to go with one database but there might be a business reason or scalability/performance/QoS/compliance/Ivory tower reason to go with multiple databases. You can use both techniques together to achieve sharded co-mingled databases meaning you have a farm of databases each with a group of co-mingled tenants on them.
Good way of telling, and pleasant post to get information on the topic of my presentation subject matter, which i am going to convey in college.
Był owo szampański artykuł na nowy maszt naczelny.
Wieczorami, kiedy zasuwało się chłodno (jak owo się czasami zdarzało po ulewnych deszczach), był wygodą gwoli
pewnych, bo jest dozwolone stało sobie usiąść z plecami opartymi.
zesuwa się po
balustradzie, mózg rejestruje śmiałe ślady od momentu kliknij po szczegółowe informacje pocisków, wygięte, poszarpane
szaroniebieskie płaskowniki.
Warkot cichnie, oddala się. Nikt się nie odwraca, nie widzi znikających
w
ciemności, mętnie świecących stanowisk pozycyjnych.
Kołysząca się na drucie goła, słabowita żarówka, w chybotliwym połysku kawałek
biało-czerwonego szlabanu. Ciemne sylwetki w długich.
Magnificent goods from you, man. I have take into accout your stuff previous to and you’re simply too fantastic. I actually like what you have obtained right here, really like what you are saying and the way during which you are saying it. You make it entertaining and you still take care of to keep it smart. I can’t wait to learn much more from you.
That is really a terrific site.
Why users still make use of to read news papers when in this technological globe everything is presented on web?
It is the ideal time to generate a very few strategies for the future and it’s time for you to be at liberty. I’ve truly learn this text if I might simply just I must inform you few fascinating things or information. You could produce up coming articles making reference to this informative article. My partner and i want to find out more challenges about that!