Episode Transcript
Available transcripts are automatically generated. Complete accuracy is not guaranteed.
Speaker 1 (00:00):
Imagine spending months, I mean literally months building this pristine
software application.
Speaker 2 (00:05):
Oh yeah, the dream scenario.
Speaker 1 (00:07):
Right. You've obsessively tuned the user interface, your API N
points are like lightning fast, and the deployment is just
totally flawless.
Speaker 2 (00:16):
Sounds perfect so far, it is.
Speaker 1 (00:18):
Until launch day when a malicious, bought or honestly, maybe
just a profoundly confused user inputs a massive string of
emojis and a raw seagal command right into your crucial
checkout form.
Speaker 3 (00:30):
Oh no, that is the ultimate architectural stress test right there.
Speaker 1 (00:33):
Yeah, because if your system accepts that input, it doesn't
just look bad, it potentially corrupts your entire database, bringing
the application to a grinding halt.
Speaker 3 (00:43):
And that's exactly why you can build the most elegant
front end in the world. But if your underlying data
layer just blindly accepts garbage, I mean, your application is
essentially a house of cards exactly. Ensuring data integrity is well,
it's arguably the most critical foundational step in software architecture.
Speaker 1 (00:59):
So today our deep dive, we are focusing entirely on
defending your data. We're talking about mastering business rules to
build a truly bulletproof data model.
Speaker 2 (01:09):
Yes, this is such a vital topic.
Speaker 1 (01:11):
Right, So, whether you're a programming student, maybe a young
professional scaling your first production app, or a self taught
developer listening to this and refining your back end architecture,
we're going to trace the journey of a single piece
of data today.
Speaker 3 (01:25):
We're going to track it from the very moment it
hits your application layer all the way down into the
rigid constraints of your database vault.
Speaker 1 (01:33):
And doing this correctly it requires understanding that securing data
isn't just about throwing up random roadblocks, No.
Speaker 2 (01:40):
Not at all.
Speaker 3 (01:41):
It requires a deliberate mapping of the functional requirements of
your system. And those requirements that's what we call business rules.
Speaker 1 (01:48):
Okay, so let's define the boundaries of what we're actually
talking about today. Because business rules can encompass like almost anything.
Speaker 3 (01:54):
Right, it really can. It's a broad term.
Speaker 1 (01:56):
So how do we categorize them when we sit down
to our detect a system.
Speaker 3 (02:01):
Well, we generally divide them into two camps, high level
and low level rules. So high level rules those governed
processes and relationships across multiple models, like what exactly say
in a financial application, a high level rule might dictate
that a company model interacts with millions of stock record models, right,
and those records must be aggregated and scrubbed before generating
(02:24):
a quarterly record.
Speaker 1 (02:25):
Got it? So that requires like complex service objects and
background jobs exactly.
Speaker 3 (02:31):
But today we are focusing on the bedrock. We're talking
about low level business rules. These dictate the strict, non
negotiable attributes of a single data model.
Speaker 1 (02:41):
Okay, let me try to unpack this with an analogy.
If high level rules dictate how the rooms in a
house connect to each other, low level rules dictate that
every single brick used must be solid and uniform.
Speaker 2 (02:52):
That is a perfect way to look at it.
Speaker 1 (02:53):
Yeah, so if I'm building this house, why not just
put all that logic in the controller, Like if a
user submits a form to create a company, couldn't I
just write a quick script in the controller that checks
if the company name exist before saving it.
Speaker 3 (03:05):
I mean you could, sure, but you'd be violating a
core architectural principle, especially in NBC. You know model view
controller frameworks.
Speaker 1 (03:15):
Right, the whole skinny controllers and fat models.
Speaker 3 (03:18):
Idea Precisely, we aim for skinny controllers. The controller's only
job is really just to direct traffic. It takes the
HTDP request, hands the parameters to the model, and then
returns a view or ad Jason response.
Speaker 2 (03:30):
That's it.
Speaker 1 (03:31):
So the model itself has to own the data.
Speaker 3 (03:33):
Rules exactly because think about it. If you put validation
logic in the controller, what happens when you need to
create a company from a background worker or like a
command line Rake task.
Speaker 1 (03:44):
Oh I see, your controller logic is just bypassed completely.
Speaker 3 (03:47):
Yep. The model is the central authority for the data,
regardless of where the request actually originated.
Speaker 1 (03:52):
That makes perfect sense. So taking a company model, a
low level business rule would dictate that a company must
have a name and that name must be unique across
the entire system.
Speaker 3 (04:03):
Right Or for a stock price model, the rules would
say the amount must be a numerical value and it
must have a valid timestam.
Speaker 1 (04:09):
Okay, So how do we translate those human readable rules
into actual Ruby code.
Speaker 3 (04:14):
Well, frameworks like rubyond rails give us an object relational mapper,
an RM active record, and active record provides this really
robust API for slapping validation rules directly onto our data.
Speaker 1 (04:27):
Models using a specific method.
Speaker 2 (04:29):
Right yeah, using a method called validates.
Speaker 1 (04:30):
So inside the company model class. I just declare, validates,
point into the dot name attribute, and pass in a
hash of conditions. You got it, like presence true and
uniqueness true.
Speaker 3 (04:42):
Precisely, and Rails parses that single line of code to
enforce those two critical rules. But to really grasp the
power here, you have to understand the mechanism of how Rails.
Speaker 1 (04:52):
Enforces them the validation life cycle.
Speaker 3 (04:54):
Exactly, when you instantiate a new company object and call
dot save, Rails intercepts that data transaction. It's pauses it right,
It pauses looks as your validates declarations, and evaluates the
current state of the object against those exact roles.
Speaker 1 (05:07):
Okay, so if it passes, it generates the seql insert
statement and commits it to the database. But what mechanically
happens if it fails? Say I tried to save a
company with a blank name.
Speaker 3 (05:19):
In that case, the save method halts the transaction completely
and returns false. But more importantly, it populates a specific
object attached.
Speaker 1 (05:27):
To that instance, the errors collection.
Speaker 3 (05:29):
Right, yes, the errors collection, which is a really robust
API in itself. If the name is blank, Rails injects
an error object pointing to the not name attribute with
the message can't be blank.
Speaker 1 (05:40):
Which is just incredibly useful for front integration.
Speaker 2 (05:42):
Oh massively useful.
Speaker 1 (05:43):
Because if I'm using like a RAILS form builder in
my view, it can automatically read that errors collection. So
if it season error attached to dot name, it wraps
that specific HTML input field in a CSS class that
turns it red.
Speaker 3 (05:56):
Yep, and it can display that can't be blank message
directly below the field.
Speaker 1 (06:00):
It creates this seamless feedback loop for the user, and
you didn't even have to write any custom HTMIL error
handling exactly.
Speaker 3 (06:07):
The back end model talks directly to the front end
view via that erro's object.
Speaker 2 (06:11):
It's brilliant.
Speaker 1 (06:12):
But here's where it gets really interesting. What happens when
the built in validators just aren't enough. Ah. Yeah, the
edge cases right like presence and uniqueness are standard, but
business rules are often highly domain specific. Let's say a
stakeholder hands you a rule a stock taker symbol must
be between two and four characters long. Rails doesn't have
(06:36):
a simple true false switch for that exact logic.
Speaker 3 (06:39):
It doesn't, so when the basic tools fall short, we
shift to custom validators.
Speaker 1 (06:44):
Okay, how does that work?
Speaker 2 (06:45):
Well?
Speaker 3 (06:46):
Instead of using the validates method with an as at
the end, we use the singular.
Speaker 1 (06:49):
Validate method validate no as.
Speaker 3 (06:51):
Right, and this doesn't call a built in RAILS helper. Instead,
it points directly to a custom private method that you
define within the model itself.
Speaker 1 (07:00):
Okay, so I'd write validate dot check ticker length, and
then inside that method I evaluate the string exactly. But wait,
since I'm not using a RAILS helper, how do I
actually stop the dot save method from executing if the
ticker is say, five characters long.
Speaker 3 (07:14):
This is where you have to manually interact with that
errors collection.
Speaker 1 (07:16):
We just talk about that. I see.
Speaker 3 (07:17):
Yeah, So inside your custom method you evaluate your condition.
If the string length is invalid, you literally call errors
dot ad specify the attribute, in this case, ticker symbol,
and pass in your custom message.
Speaker 1 (07:31):
So by manually injecting an item into the errors collection,
you're telling Rails, hey, this object is now in an
invalid state.
Speaker 3 (07:38):
Bingo. When the validation cycle finishes checking everything, it sees
that the errors collection is not empty and it halts
the database safe.
Speaker 1 (07:45):
You essentially build a custom circuit breaker. I love that.
Speaker 3 (07:48):
So we've established total granular control over our application. Layer,
we validate presence, uniqueness, custom logic for string lengths. The
application seems completely secure.
Speaker 1 (07:57):
The application is secure, but relying only on this layer
provides a very dangerous illusion of total data security.
Speaker 3 (08:04):
Wait, really, so if I bypass RAILS entirely, say I
open a direct database management tool like table plus, or
I connect via a command line interface like PSLM, and
I run a raw SQL insert statement.
Speaker 2 (08:16):
What happens?
Speaker 3 (08:17):
Well, what do you think happens to those customer? Ruby
validations just vaporize like the database literally doesn't know they exist.
The database is completely one hundred percent blind to them.
Speaker 2 (08:27):
Oh wow, think of it this way.
Speaker 3 (08:28):
The RAILS validations act like a highly sophisticated spell checker
in a word processor. They catch mistakes in real time
while you are typing within the application environment.
Speaker 1 (08:38):
Right.
Speaker 3 (08:39):
But if a malicious actor, or even just a buggy
background script opens the underlying text file with a hex
editor and writes directly to it, the spell checker is
totally powerless.
Speaker 1 (08:48):
Because raw SQL queries bypass the Ruby memory space entirely.
Speaker 3 (08:53):
Exactly, the database receives a command to insert a blank name,
assumes you know what you're doing, and stores the corrupted data.
Speaker 1 (09:00):
So to achieve true data integrity, we can't just rely
on the spell checker. We have to essentially lock the
pdf itself.
Speaker 2 (09:07):
I like that.
Speaker 1 (09:07):
Yes, we have to push these rules down into the
actual database schema, like putting a guard at the front
desk of an office building, which is great, but if
someone breaks in through a basement window, that guard is useless, and.
Speaker 3 (09:19):
That basement window is direct database access, so we secure
it using migration file migrations. Okay, Yeah, migrations are a
domain specific language in Rails that allows you to manage
your database schema using Ruby code. You generate a new file,
write your schema changes, and run Rails dB dot migrate
to translate that Ruby into database specific SQL commands like
(09:42):
alter table.
Speaker 1 (09:43):
So here's a common question that trips up a lot
of junior developers. Yeah, if you realize your database schema
is missing a constraint, why run a command to generate
an entirely new migration file.
Speaker 2 (09:53):
That's a good question, right, If the table was.
Speaker 1 (09:55):
Created by a migration file two months ago, why not
just open that old file, add the constraint, and save it.
Speaker 3 (10:01):
Because modifying old migrations destroys the architectural history of your application.
Also well, Rails tracks which migrations have been executed using
a hidden ledger a database table actually called SNEA migrations,
and every time you run a migration, Rails records the
unique time stamp of that file in the ledger.
Speaker 1 (10:18):
Oh, I get it. So if I edit a file
from two months ago, my local machine and the production
server will look at their internal ledgers, see that a
file with that timestamp was already executed, and just completely
ignore my new edits exactly.
Speaker 3 (10:30):
Your schema will be completely out of sync across different
development environments. Migrations must be treated as an immutable chronological log.
You only ever move forward to fix an old mistake.
You always generate a new migration file.
Speaker 1 (10:44):
Okay, so let's lock down the vault. Then we want
to enforce the rule that a company name cannot be blank,
but we want the database itself to enforce it in
our new migration file. What's the actual mechanism we add.
Speaker 3 (10:57):
A structural constraint in the migration file We use is
the change column command on the company's table, targeting the
name column, and we pass in a specific option null
false null false yep. When you run that migration rails
alters the actual database table to physically reject any record
where the name is missing.
Speaker 1 (11:15):
So even if a raw sequel injection somehow bypasses the
application layer, the database engine itself evaluates the transaction, sees
the null value, and throws a fatal constrained violation error.
Speaker 2 (11:25):
Zoom. The data is protected at the bedrock level.
Speaker 1 (11:28):
Okay, let's escalate the complexity a bit. Consider a risk
factor attribute on a financial model. The business rule states
it can only be one of three values low, medium,
or high. Okay. At the application layer, we could just
use an inclusion validator. But how do we enforce that
strict list at the database layer, because I'm assuming we
(11:49):
can't just use null false any string, even the word
non existent isn't null. It's a valid string.
Speaker 3 (11:56):
Right. This is where database specific features come into play
on your engine. You have a few really powerful options.
If you are using postrescool, for example, you could create
a custom ENOM data type at the database level and
enom Yeah, and Enom explicitly defines the allowed strings for
a column. So if a raw SEQL query tries to
insert the word critical, Postgress rejects it right there, because
(12:18):
critical is not part of the registered enom type.
Speaker 1 (12:21):
Okay, but what if you are using postgress or you
just want a more universal SQL solution.
Speaker 3 (12:25):
In that case, you would write a check shit constraint.
A check constraint physically evaluates a boolean expression before writing
the row.
Speaker 1 (12:32):
How does that look in the migration.
Speaker 3 (12:33):
Well, in your migration, you execute a raw SQL command
that tells the database only except this insert. If the
risk factor column is in the array of low, medium,
or high, the database evaluates the logic itself.
Speaker 1 (12:48):
Wow. So between null false enoms and cheek it constraints,
we have effectively sealed the vault. We have. But I
mean this brings up a massive friction point. If the
database is this rigid, it's going to throw hard, fable
errors whenever input isn't totally perfect.
Speaker 3 (13:04):
Oh.
Speaker 1 (13:04):
Absolutely, Like if a user types of valid stock ticker
but types it in lowercase, a strict database constraint might
just reject it. Throwing a database error screen for a
simple capitalization mistake is a terrible user experience.
Speaker 2 (13:16):
It's awful.
Speaker 3 (13:17):
It creates an adversarial relationship between the user and the system.
We want strict data integrity, obviously, but we want the
application to behave like a helpful assistant, silently correcting formatting
before the data ever reaches the strict constraints of the vault.
Speaker 1 (13:31):
So how do we achieve that balance.
Speaker 3 (13:33):
To achieve this, we introduce model life cycle hooks.
Speaker 1 (13:36):
Okay, so we've talked about the validation cycle during a
save transaction. Where do hooks fit into this timeline?
Speaker 3 (13:42):
Hooks allow you to inject logic at exact, almost microscopic
moments during the object's life cycle. We have hooks like
before validation, after validation, before save, and after commit.
Speaker 1 (13:53):
Okay, so if we want to automatically capitalize a stock ticker,
we'd probably use before validation, right, because you want to
format the data before the application runs its custom length checks.
Speaker 2 (14:04):
That is a perfect use case.
Speaker 3 (14:05):
Yes, you declare before validation dot formatic or symbol.
Speaker 1 (14:09):
And inside that private method.
Speaker 3 (14:11):
Inside that method, you take the user's input strip any
white space using ruby strip method and format it using
dot upcase.
Speaker 1 (14:18):
Okay, let's trace the mechanics of that transaction. A user
lazy types a lowercase ticker with some trailing spaces. They
hit submit. The controller passes the messi string to the
model ye, but before the model even runs, its validates checks,
it pauses, it runs our hook, strips the spaces and
capitalizes the text. Then it runs the validations, and.
Speaker 3 (14:37):
Since it's formatted, the pristine string passes exactly.
Speaker 1 (14:41):
Yeah. Then the save continues generating the sequel, and the
database accepts the perfectly formatted data without triggering any of
those strip constraints.
Speaker 3 (14:50):
The hook silently repairs the data midflight. It's beautiful. But,
and this is important, there is a critical architectural danger here.
Speaker 1 (15:00):
Well, what is it?
Speaker 3 (15:00):
What happens if the logic inside your hook throws an
exception or explicitly fails.
Speaker 1 (15:05):
Oh, if the hook crashes, does the database transaction still happen? No?
Speaker 3 (15:11):
In modern rails, if a before save or before validation
hook intentionally throws an abort signal, specifically using the throw
abort syntax, it halts the entire transaction chain immediately. Wow, okay,
the model will not be saved.
Speaker 1 (15:23):
Now.
Speaker 2 (15:23):
This is powerful.
Speaker 3 (15:24):
If you are, say, checking a third party API to
verify a ticker symbol before saving, and the API is down,
you can imbort the save. But on the flip side, right,
it also means a poorly written hook can silently swallow data,
preventing records from saving without generating standard validation errors.
Speaker 1 (15:40):
So hooks are kind of a double edged sword. They
provide incredibly elegant formatting and background logic, but they introduce
this hidden complexity that can make debugging a halted save very, very.
Speaker 3 (15:52):
Difficult exactly, which is why hooks should be kept really minimal.
They should primarily be used for internal data normalization like
upcas strings or calculating derivative values, not for complex external
business logic.
Speaker 1 (16:05):
Makes sense, Well, we've engineered an incredibly robust flow today.
We defined our business rules. We built the application layer
validations to act as a real time spell checker, integrating
with our UI via the errors api we did. We
recognize the vulnerability of direct database access, so we lock
down the vault using immutaal migrations, enforcing null false and
(16:26):
chi check constraints. And finally, we use life cycle hooks
to ensure a smooth user experience by normalizing data before
it hits those rigid walls.
Speaker 3 (16:35):
It's a complete architectural journey.
Speaker 1 (16:37):
So to formalize this as a sort of architectural checklist
for you listening, if you have a non negotiable business rule,
say an employees's age must be over eighteen. You must
implement it in two places. You need the application layer
validation to provide immediate, friendly feedback to the user interface,
and you need a database level check constraint to act
(16:57):
as the ultimate unbreakable security vault against rogue scripts or
back end bypasses.
Speaker 3 (17:02):
That du layer approach that is the industry standard for
a truly bulletproof data model.
Speaker 1 (17:08):
It really is.
Speaker 3 (17:08):
But you know, the strictness we've advocated for today introduces
a bit of a philosophical architectural question. Oh yeah, we've
spent this entire deep dive obsessing over strictly enforcing rules
to ensure absolute data purity, but we operate in a
world of messy, unpredictable human input.
Speaker 1 (17:27):
Is very true, people are messy.
Speaker 3 (17:28):
So the question becomes, at what point do overly strict,
rigid database constraints stop your application from capturing valuable, if
imperfect reality. Wow, when exactly does data protection become data prevention?
Speaker 1 (17:42):
That is the balance every architect has to negotiate, isn't it. It
is in need definitely something to carefully weigh the next
time you sit down and define your schema. Until next time,