Pregunta

A user can be in groups. And an item/product is assigned groups that can see the item. Users can see that item if they are in one of the assigned groups.

I want neither public (anonymous users in no groups) nor groupless users (logged in users not in any groups) to see the item. But I want the interface to allow assigning the item an 'all/any groups' attribute so that users that are in any group at all can see the item.

Where/How should I store this assignment?

p.s. I expect the technique to also be extended to other entities, for example I'd assign a file to a category, and groups are linked to categories. so when a file is marked as visible by the 'all/any category' then if the user (thru groups and group-categories) is linked to at least one category then the file is visible to them.

Decision:

It seemed the choice was whether to implement as a row in a entity-groups table or as fields in the entity table. The chosen answer used the former.

And either managing the group membership in a table or adding JOIN conditions. The chosen answer used the former, but I'm going to use the latter. I'm putting an indirection between the query and usage so if (when) performance is a problem I should be able to change to a managed table underneath (as suggested) without changing usage.

I've other special groups like 'admin', 'users', etc. which can also fit into the same concept (the basis simply being a list of groups) more easily than special and variable field handling for each entity.

thanks all.

¿Fue útil?

Solución

As mentioned by both Martin Smith and Mikael Eriksson, making this a property of the entity is a very tidy and straight forward approach. Purely in terms of data representation, this has a very nice feel to it.

I would, however, also consider the queries that you are likely to make against the data. For example, based on your description, you seem most likely to have queries that start with a single user, find the groups they are a member of, and then find the entities they are associated to. Possibly something lke this...

SELECT DISTINCT -- If both user and entity relate to multiple groups, de-dupe them
  entity.*
FROM
  user
INNER JOIN
  user_link_group
    ON user.id = user_link_group.user_id
INNER JOIN
  group_link_entity
    ON group_link_entity.group_id = user_link_group.group_id
INNER JOIN
  entity
    ON entity.id = group_link_entity.entity_id
WHERE
  user.id = @user_id

If you were to use this format, and the idea of a property in the entity table, you would need something much less elegant, and I think the following UNION approach is possibly the most efficient...

<ORIGINAL QUERY>

UNION       -- Not UNION ALL, as the next query may duplicate results from above

SELECT
  entity.*
FROM
  entity
WHERE
  EXISTS (SELECT * FROM user_link_group WHERE user_id = @user_id)
  AND isVisibleToAllGroups != 0

-- NOTE: This also implies the need for an additional index on [isVisibleToAllGroups]

Rather than create the corner case in the "what entity can I see" query, it is instead an option to create the corner case in the maintenance of the link tables...

  1. Create a GLOBAL group
  2. If an enitity is visible to all groups, map them to the GLOBAL group
  3. If a user is added to a group, ensure they are also linked to the GLOBAL group
  4. If a user is removed from all groups, ensure they are also removed from the GLOBAL group

In this way, the original simple query works without modification. This means that no UNION is needed, with it's overhead of sorting and de-duplication, and neither is the INDEX on isVisibleToAllGroups needed. Instead, the overhead is moved to maintaining which groups a user is linked to; a one time overhead instead.

This assumes that the question "what entities can I see" is more common than changing groups. It also adds a behaviour that is defined by the DATA and not by the SCHEMA, which necessitates good documentation and understanding. As such, I do see this as a powerful type of optimisation, but I also see it as a trades-and-balances type of compromise that needs accounting for in the database design.

Otros consejos

I'd put it in the items table as a boolean/bit column IsVisibleToAllGroups.

It does make queries to get all items for a user a bit less straightforward but the other alternative would be to expand out "all groups" so you add a permission row for each individual group but this can potentially lead to a huge expansion in the number of rows and you still have to keep this up-to-date if an additional group is added later and somehow distinguish between a permission that was granted explicitly to all (current and future) groups and one that just happened to be granted to all groups currently in existence.

Edit You don't mention the RDBMS you are using. One other approach you could take would be to have a hierarchy of groups.

GroupId     ParentGroupId Name
----------- ------------- ----------
0           NULL          Base Group
1           0             Group 1
2           0             Group 2

You could then assign your "all" permissions to GroupId=0 and use (SQL Server approach below)

WITH GroupsForUser
     AS (SELECT G.GroupId,
                G.ParentGroupId
         FROM   UserGroups UG
                JOIN Groups G
                  ON G.GroupId = UG.GroupId
         WHERE  UserId = @UserId
         UNION ALL
         SELECT G.GroupId,
                G.ParentGroupId
         FROM   Groups G
                JOIN GroupsForUser GU
                  ON G.GroupId = GU.ParentGroupId)
SELECT IG.ItemId
FROM   GroupsForUser GU
       JOIN ItemGroups IG
         ON IG.GroupId = GU.GroupId  

Instead of a boolean, which needs additional logic in every query, I'd add a column 'needs_group' which contains the name (or number) of the group that is required for the item. Whether a NULL field means 'nobody' or 'everybody' is only a (allow/deny) design-decision. Creating one 'public' group and putting everybody in it is also a design decision. YMMV

This concept should get you going:

enter image description here

The user can see the product if:

  • the corresponding row exists in USER_GROUP_PRODUCT
  • or PRODUCT.PUBLIC is TRUE (and user is in at least one group, if I understand your question correctly).

There are 2 key points to consider about this model:

  1. Liberal usage of identifying relationships - primary keys of parents are "migrated" within primary keys of children, which enables "merging" of GROUP_ID at the bottom USER_GROUP_PRODUCT. This is what allows the DBMS to enforce the constraint that both user and product have to belong to the same group to be mutually visible. Usage of non-identifying relationships and surrogate keys would prevent the DBMS from being able to enforce that directly (you'd have to write custom triggers).
  2. Usage of PRODUCT.PUBLIC - you'll have to treat this field as "magic" in your client code. The alternative is to simply fill the USER_GROUP_PRODUCT with all the possible combinations, but this approach is fragile in case a new user is added - it would not automatically see the product unless you update the USER_GROUP_PRODUCT as well, but how would you know you need to update it unless you have a field such as PRODUCT.PUBLIC? So if you can't avoid PRODUCT.PUBLIC anyway, why not treat it specially and save some storage space in the database?
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top