Consider the following variables are generated by the player analyzer service:
level = 6;
errors = 4;
score = 12;
...
And we have some rules and messages:
1. errors == 0 AND level > 5 : Senior player
2. score == 10 OR errors == 3: Border line player
3. score > 10 AND score < 13: Not good, just passed
4. ...
Another example: Consider the following variables are generated by the food analyzer service:
fruit = 2;
coca = 6;
...
And we have some rules and messages:
1. fruit == 0 : Consider buying some fruits
2. coca == 0: That's healthy
3. ...
How should I save rules and messages in a RDBMS like MySQL so it becomes easy to query and find the messages.
The worst method is to save rules in one column and messages in another column and loading every record to test in host programming language.
Can you suggest a better method for this situation? This isn't a good method when we have a few thousand messages, we need a method to filter messages on DB side.
I've created a quick ERD to demonstrate how I'd initially design it:
What do all these columns and tables mean?
property_name
This contains a list of everything that can have a value checked against it.
operator
Contains a list of the different operators that are used for each property.
rule_message
Stores the actual message being displayed.
operator_property
This is the joining table between all three other tables, and contains your rules and logic.
How to use this design: * You can add in all your properties and operators into the tables. * To find the message to display for a scenario, such as checking what to show for a player:
SELECT rn.rule_message_id, rm.message
FROM rule_message rm
INNER JOIN operator_property op ON rm.rule_message_id = op.rule_message_id
INNER JOIN property_Name pn ON op.property_id = pn.property_id
INNER JOIN operator o ON op.operator_id = o.operator_id
WHERE 1=1
AND (
pn.property_name = "errors"
AND pn.operator_symbol = "=="
AND op.check_value = 0
)
AND (
pn.property_name = "level"
AND pn.operator_symbol = "5"
AND op.check_value = 5
)
This query would ideally return 1 row. If it returns 0, then no messages apply. If it returns 2 or more, it means that it didn't neatly fit into one of your criteria, so none of the messages apply.
Hope this helps! I've written articles on desigining databases before and the best tip I can give you is to work out the purpose of the data, which it seems like you already have.
Also, if you can think of better names for tables, then go for it - this was just a quick design to illustrate the point.
Generally, this kind of rule interpretation is not done directly in the database, and it will eventually be done in an interpreter like your check_rules_against_data
, and that is absolutely fine.
It is quite common to just write all the rules directly in one or more php files (surrounded of course by some code like if ($rule) { echo $message; }
). It is usually faster than to dynamically evaluate every rule every time (and keep in mind, the database will have to do just that too). How you encode the filters depends on your needs; you can stick to your rule format, you could just show the full php code and let the user edit it, you could split them up and use the databasedesign to e.g. verify that a variable exists (see e.g. my extended rule_term
-table below or completeitpro's answer). All of that would work just fine.
If you want, or if you want to test it, you can however do some preselection in your database. There are a lot of ways to do it, and a lot of ways to optimize it for special situations, that will massively depend on what you actually want to do, so I will just describe one way, to give you an idea.
Your variables look like you will have a ton of them, but all of them integer (so owning a coke doesn't mean: Items[x]='COCA'
, but coca=1
), so you can put them and the rules in tables like this:
variable
variableid | variablename | variabletype
----------------------------------------
1 | errors | 1
2 | level | 1
3 | score | 1
user_variable
userid | variableid | valueint
-------------------------------------
1 | 1 | 0
1 | 2 | 6
1 | 3 | 10
2 | 1 | 3
2 | 3 | 10
3 | 1 | 0
3 | 2 | 6
3 | 3 | 10
4 | 1 | 0
4 | 2 | 5
rule
ruleid | mincount | message
---------------------------
1 | 2 | Senior player -> AND (2 terms have to fit)
2 | 1 | Border line player -> OR (any 1 term can fit)
rule_term
ruleid | variableid | minvalueint | maxvalueint
-----------------------------------------------
1 | 1 | 0 | 0 -> error == 0
1 | 2 | 6 | 9999 -> level > 5
2 | 1 | 3 | 3 -> error == 3
2 | 3 | 10 | 10 -> score == 10
With these rules, you can now preselect rules that hit:
select user_variable.userid, rule.ruleid, count(*) as cntfulfilled,
max(rule.mincount) as mincnt, max(rule.message) as message
from rule_term
join rule
on rule_term.ruleid = rule.ruleid
join user_variable
on rule_term.variableid = user_variable.variableid
and rule_term.minvalueint <= user_variable.valueint
and rule_term.maxvalueint >= user_variable.valueint
group by user_variable.userid, rule.ruleid
having count(*) >= max(rule.mincount);
This should count for each user and each rule, how many subterms of this rule are fulfilled. This should be, if I'm not mistaken:
userid | ruleid | cntfulfilled | mincnt | message
--------------------------------------------------
1 | 1 | 2 | 2 | Senior player
1 | 2 | 1 | 1 | Border line player
2 | 2 | 2 | 1 | Border line player
3 | 1 | 2 | 2 | Senior player
To express AND
, mincnt
should be the number of all subterms, for OR
, it will be 1. To build rules with either plain AND
or OR
, this will already be the complete test.
For more complicated rules, you have to be able to recreate the rule in php to put it in your check-function. You can e.g. encode it in a table like:
extended rule_term
-table:
ruleid | pos | cond | var.id | min | max
--------------------------------------------
3 | 1 | 1 | 0 | 0 | 0 -> (
3 | 2 | 0 | 1 | 1 | 1 -> error == 1
3 | 3 | 4 | 2 | 5 | 5 -> AND level == 5
3 | 4 | 2 | 0 | 0 | 0 -> )
3 | 5 | 5 | 3 | 10 | 10 -> OR score == 10
where I used cond=1: (, cond=2: ), cond=3: NOT, cond=4: AND, cond=5: OR. (There are better ways to encode it, e.g. express just the logic and group it in nested AND
-subgroups, but it will not improve anything here).
This will allow you to still preselect rules that might fit, to get the rules you have to analyze afterwards in php (you cannot use mincnt anymore, since mincnt will be 1 even if just error == 1
, not just when score == 10
).
You can add more things to it: you can add string variable types (add a column valuestr
to user_variable
and rule_term
and adjust the joins) or a flag for 'NOT', and you can add more complicated copnditions to your join if you are able to express them in rows in the rule_term-table (e.g. combine 2 variables and check for 2 variables in a double join).
It's a little bit harder, but you might want to use left joins and some additional logic to compare variables that are not there (e.g. if you don't want to set the variable coca
for everyone, just for users that have (or had) coke.
If you want to use horizontal variables (a fixed number of variables, each in a column), you should do the same for the rule-terms (a column min/max for each variable) and adjust the joins to check every column.
This is just a general idea, and you obviously have tons of alternatives to do that, and the best option and optimization will largely depend on your actual needs, and spending more time thinking about your databasedesign (or on how to generate dynamic php files) will later reduce frustration (a lot) or increase speed (a lot). And I will remind you again, test the option to generate dynamic php files - this will usually be a lot faster.
This is the classic case for a rule system, and should likely not be implemented in the database. I put together a java library (Rulette) which does pretty much this.
Essentially you would set it up by creating a rule_system table and inserting an entry to it, and creating a rule input table with your entries (level, error, score). By your samples, level and error seems to be 'VALUE' types while 'score' seems to be a 'RANGE' type.
Now you can create a rule table ('player_rules {id, level, error, score}') to configure all your rules and map them to entries an output table ('player_message {id, message}').
Good to go!!
RuleSystem rs = new RuleSystem("player-rule-system");
Rule r = rs.getRule(new HasMap<>(){"level":level, "error: : error, "score" : score});