Designing a Great Ruby API
This is a modal window.
The media could not be loaded, either because the server or network failed or because the format is not supported.
Formal Metadata
Title |
| |
Subtitle |
| |
Title of Series | ||
Part Number | 70 | |
Number of Parts | 94 | |
Author | ||
License | CC Attribution - ShareAlike 3.0 Unported: You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this | |
Identifiers | 10.5446/30910 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
RailsConf 201570 / 94
1
4
7
8
9
10
11
13
14
16
17
19
21
24
25
29
30
33
34
35
36
37
39
40
42
47
48
49
50
51
53
54
55
58
59
61
62
64
65
66
67
68
70
71
77
79
81
82
85
86
88
92
94
00:00
Data structurePole (complex analysis)State of matterSocial classEndliche ModelltheorieMathematicsWater vaporRight angleStudent's t-testPhysical systemForcing (mathematics)TheoryGroup actionNatural numberCellular automatonType theoryRepresentation (politics)Game controllerString (computer science)Object (grammar)DatabaseDampingCodeQuery languageCasting (performing arts)Row (database)Attribute grammarModule (mathematics)Table (information)Product (business)WordStrategy gameDifferent (Kate Ryan album)Software testingValidity (statistics)MultiplicationDomain nameControl flowComputer animation
04:13
Vapor barrierLatent heatVariable (mathematics)Multiplication signDefault (computer science)Time zoneRow (database)Computer animation
04:59
Physical lawSpecial unitary groupRule of inferenceAreaFrequencyCategory of beingSheaf (mathematics)Bit rateSymbol tableGroup actionVarianceVelocitySign (mathematics)Point (geometry)Connectivity (graph theory)WordEndliche ModelltheorieMusical ensembleMultiplication signMedical imagingData structureDependent and independent variablesPhysical systemMereologyGraph (mathematics)Food energyGame controllerArithmetic progressionKernel (computing)BitElectronic mailing listTerm (mathematics)Disk read-and-write headCASE <Informatik>DistanceMathematicsInteractive televisionRow (database)Software testingExecution unitRight angleWeightView (database)1 (number)Observational studyLine (geometry)Lattice (order)Series (mathematics)QuicksortSystem callNormal (geometry)Direction (geometry)Social classUniverse (mathematics)NumberShift operatorSummierbarkeitForcing (mathematics)Context awarenessPattern languageAtomic numberFreezingInsertion lossAdditionTouchscreenRootStudent's t-testInformationReading (process)Object (grammar)Spring (hydrology)ChainNichtlineares GleichungssystemIntegrated development environmentType theoryCodeString (computer science)Attribute grammarCode refactoringTime zoneGoodness of fitStatement (computer science)Factory (trading post)Default (computer science)Standard deviationAxiom of choiceImplementationRevision controlSoftware bugOrder (biology)VibrationSet (mathematics)Module (mathematics)CognitionEntire functionSoftware developerComputer fileMacro (computer science)Overhead (computing)Single-precision floating-point formatTraffic reportingNumbering schemeBranch (computer science)Slide ruleResource allocationHash functionQuery languageLatent heatMultilaterationDatabaseSubject indexingDifferenz <Mathematik>Block (periodic table)Product (business)Uniqueness quantificationError messageValidity (statistics)Design by contractDirectory serviceLogical constantMessage passingBinary codeLevel (video gaming)Software maintenanceGravitationTrailTelephone number mappingFlow separationSource codeStructural loadIntegerConnected spaceForm (programming)Web 2.0WebsiteLimit (category theory)Serial portHookingComplete metric spaceAssociative propertyElectronic signatureMultiplicationParameter (computer programming)Cycle (graph theory)Different (Kate Ryan album)Adaptive behaviorPointer (computer programming)InferenceShape (magazine)Loop (music)Constructor (object-oriented programming)Data managementState of matterMappingTable (information)Human migrationData conversionCasting (performing arts)outputDot productBuildingInstance (computer science)Computer animation
Transcript: English(auto-generated)
00:12
So once we've identified what it is that we want to build, we need to roughly, and the key word here is roughly decide what we want it to look like. One of the worst things you can do is pin yourself down to a very exact API too early on.
00:26
When you're refactoring towards a new API in an existing system, it's very important that you have good tests. And then the final steps of actually implementing it, we build those objects, we use them internally, we can post them together manually where we need to, and then the DSLs will come from where we find duplication or pain.
00:44
So before you can design a great API, the first step is to identify the API that you're missing. In any large or legacy code base, and make no mistake, Rails is just a large legacy code base. All of the strategies that you can use everywhere else in your code still apply here. You can find plenty of concepts that are duplicated across the domain.
01:03
Some of the smells to look for are methods with the same prefix, code that has similar structure, or the big one that you find in Rails a lot are multiple modules or classes that are overriding the same method over and over and over again and calling super. So one of these concepts that we found as I backed the record was the need for modifying the type of an attribute.
01:26
Say for example, you have a price column on a product table and you'd like to represent that as a money object instead of a float. So it might look something like this. We're overriding the reader and writer, checking to see if anything's nil.
01:47
Even with really experienced developers, everybody's always wondering, if I do this, am I going to break Rails?
02:38
And that might work on its end. When you override the writer, you're actually doing some
02:56
casting and then giving it that record and it thinks this is before performing the casting.
03:03
Certain values being a certain structure, some validation to expect things to be a certain way, and this might just work a little bit differently.
03:26
Even if you don't break things, there might be other misbehavior. So you might want control over the SQL representation of your money object. Maybe you add in currency and are storing it as a string instead of a float and you need to parse that out.
03:44
And combine them back together when you're going to the database. But the really hard one right now is you might also want to be able to use this object as a value and a query. Being able to pass this away is incredibly useful. So Rails overrides the types of attributes internally.
04:02
And you might be wondering, if this is so hard, how does Rails do it? And if you guessed with a giant pile of hacks, you would be right. It's a giant pile. If not for the faint of heart, you don't give me the chance to close the doors.
04:24
So don't worry too much about the specifics of the actual variable. The specifics of what it's doing aren't too important here. This is a feature that's on by default where we convert time values that you pass in an act record to the current time zone.
04:41
It's one of these things that we just sort of do and most people don't know that it's there. But it is on by default in most applications. So we're just overriding the writer here. The first thing we're doing is converting it to the current time zone. Then we're basically completely re-implementing dirty checking.
05:03
The second and third lines in this method are one-to-one copy pasted from the inside of the dirty module in act record. And then once we've done that, we're then jumping through even more hoops so that we can maintain the before type pass version of the attribute. So in this case, we're looking for the common concepts and some common smells.
05:25
So we're overriding the reader and writer. We're duplicating a lot of code. And this is a relatively small behavior change, but it has to jump through a lot of really complicated hoops in order to do it correctly. It's also important to note that the code when it's written this way introduces a lot of subtle bugs.
05:43
A lot of other modules may be trying to modify the type of this attribute in very unexpected ways. And the bugs are hard to detect once behavior is scattered all over the place. Another place that we modify the behavior of the type passing system is with the serialized macro. So this is ultimately, we're overriding the method that gets called internally
06:05
to perform the type passing instead of overriding the reader and writer explicitly. Really this module wasn't this simple when we got started. Here's some more code from the file and more.
06:24
Really this module literally overrode every single method in active record containing the word attribute. And there are five or more slides with this code that I left out. So in this case we are not explicitly overriding a reader and writer, but we are duplicating code from other parts of active record.
06:44
We're jumping through a lot of hoops and we're overriding literally everything. And this file was the con of so many bugs in 4.2 and earlier. This macro actually ends up directly modifying the value of the columns hash, which is problematic for reasons that we'll get into a lot later.
07:06
Another example is enum, where instead of strings, here we're overriding the writer method, we're also overriding the reader method, we're also overriding the before type cast, and there's several others.
07:24
And once again, enum was a large source of a significant number of bugs, disproportional to the size of the feature. So we found our missing concept. Typed attributes are overwritten everywhere, and one of the things that you might be thinking is, well,
07:43
if we want to do this so much, maybe other people want to be able to do this. So let's talk about what typecasting is. Typecasting is when you go through and explicitly convert a value from one type to another. Here's a very simple example where we have a value which is a string and we want to convert it to an integer, so we call toI.
08:02
In ActiveRecord, what we do is in actual typecasting, it's type coercion, which is the same thing when done implicitly. So here's an example when using ActiveRecord. You have a user model, age is presumably an integer column in the database. We go look at that and decide whenever you assign a value to the age attribute, we're going to convert it to an integer.
08:24
Now the reason that we do this is because ActiveRecord was originally designed to work with web forms. You're going to assign brands to attributes, and having to cast these manually would be a pain. Not just for integer types, but something like date can be significantly harder.
08:41
We didn't want to have to do have this code littered all over our controllers, so ActiveRecord site system was born. The cases we handle today are much more complicated than that, but if you go through the history of how this evolved, everything can be traced back to that original limitation. Now in Rails 4 and earlier, the only way that you can have a coerced attribute is if it's backed by a database column.
09:03
We want to be able to hook into this behavior and be able to modify it. So this is what we get in step 2 now. We're going to roughly identify what we want it to look like. And this is a simpler case. So we're going to have a product model, and we know a few things about our API at this point.
09:21
We're going to need to call some method, in this case we'll go with attribute. We are going to need to say the name of the attribute and have some marker for what the type we want it to be is. Now this is very similar to what you might find in Datamap or Mongo which have simpler APIs. We're going to avoid over specifying the API at this point, and the really nebulous part is going to be how we pass in the type directly.
10:04
So at this point, all we know for sure about our implementation is that we're going to need to introduce a type object into our system. Presumably that's not going to be enough for reasonable implementation. We want something that's not just a little bit less nappy, we want to have something that we can really be proud of and know that we will be able to maintain in the future.
10:23
So we're going to start by composing the objects in our system manually. The only one we know of is our type object, but we're going to be looking for places to extract collaborators and compose them to make our lives easier. Before we start introducing the API, we need to say a few words about the factory. There are some rules that you need to follow.
10:41
Rule number one of the factory is have good test coverage. Rule number two is have good test coverage. Rule number three is see rules mindset. So on the next couple of slides, the code can be very small. The specific details of it here are important.
11:00
What is important here is there is a giant case statement. Like many parts of the factory record, it's a giant case statement just going over a set of symbols. And this is the entire type system in 4.1. We call a bunch of class methods based on a symbol that we had earlier derived from the SQL type. And you'll see at the top of this there's a very small comment there.
11:25
Casts value, which is a string to the appropriate instance. You can think of a lot of ways to try and pass in any value that isn't a string. That was one of the most misleading comments I've seen.
11:40
So we know that we're going to introduce a type object, and we know that typecasting currently lives on the column. So first step, let's give the column a type object. So we add a constructor argument, we pass in nil everywhere, and we just run the tests. And that was the very first commit that went in going towards this.
12:01
Tiny, tiny steps extracting out more and more from what we know, which is that we need a type object and where it's going to live. By injecting it into the constructor of the columns and finding where the column objects are being constructed, this also is going to point us at the other portions of behavior that we're going to need to modify.
12:23
Where are we looking at the SQL types for the columns? Where are we constructing these? If we're injecting the type object into this so that we can modify it later, these surrounding bits of code are all going to have to change well. So we go through in our system and replace all of these case statements, they're all over the place in column,
12:43
and we just slowly move these methods to these type objects. At this point we have introduced a mapping system into our connection adapter, which we're not going to look at in detail because it's very boring and tedious, but it replaces the responsibility of looking at the SQL type string and building the simple integer,
13:02
simple string, simple timestamp, and replace it with a different object based on it. So we have a place that we can start moving all of these case statements to. So we go through our system one by one and we just remove each case statement. In each of these diffs we're just removing a giant case statement and adding another method to our delegate block at the top of the funnel.
13:28
So this is at this point in refactoring what a simple type object looks like. This is the string type which has almost no behavior attached to it.
13:41
Now we've refactored our system into something that's a little bit easier to override. Now we can start looking at actually implementing the API that will let us look into this. So the simplest case we can start with is changing the type attribute from string to integer. So let's write a test. This is what the test might look like.
14:01
We create a model with a schema. We create two attributes with the same type and we say that we want to change the type of one of them and then test the other version. We've actually written the first invocation of our API and let's take a bit of a closer look at it. So we're starting with the simplest thing that can possibly work.
14:22
We know we're going to have a type object so we just pass the type object to our method directory. We could use constants or a symbol or some other marker for the type but for now we're going to keep it very simple and very explicit. This actually turns out to be a design choice that sticks with us through the rest of the refactoring.
14:43
And there's a lot of benefits to giving a manual object. You can understand what's happening here much more easily. The API becomes much simpler. And I'm not just talking about from an implementation point of view. When you give me an object, you presumably have an inkling of what behavior can be modified by this API.
15:05
The object you gave me has a known set of methods on it. I presumably cannot possibly change behavior of anything that won't be calling one of those methods. And every DSL that you add has a cost.
15:20
When you do add a DSL, you want to try to avoid adding DSLs on top of your DSLs on top of your DSLs. There's a lot of cognitive overhead for what the behavior gets modified and where. You basically have to memorize every DSL that you introduce into your system. Understanding plain Ruby stops being enough.
15:41
And the line between being helpful and less painful and being too magic is very, very thin. So we can come up with a very serviceable implementation early on by overriding the column sets. The same thing serialized does internally. But it feels wrong. We're not changing the scheme of the model.
16:04
We're changing the structure of the model. However, we want to take the small steps we possibly can. And we want to get to a working implementation of our API as quickly as possible. But if we just try to modify the column directly, we're going to run into another problem. So this is how we look up the columns and columns inside of ActiveRecord.
16:24
Inside of ActiveRecord base specifically. And when you call either of these methods, they're going to go execute the query immediately. And that means that we can't actually use this inside of any class macros. It's very important that you be able to load your class into memory, load the definition,
16:43
and not need a database connection to do that. For example, on Heroku, when you deploy, when your assets are precompiled, that loads up the environment, which will load up all of your ActiveRecord models into memory, but you won't have a database connection. So we need our implementation to be lazy.
17:02
And when you find that you need laziness in your system, I find that very important to separate the lazy form from the strict form and have both of those available. So here's roughly what the code looks like at this point. So on the top here we have our attribute method, which is the lazy version.
17:22
Below that we have a fine attribute, which is the strict version. And then we're overriding, after the scheme has loaded, we're going in and overriding all of the columns that we want to modify. Now, unfortunately, for most of our cases, we're not just modifying the type of an attribute.
17:45
We're not just replacing the type of an attribute completely. We want to modify the existing type. Serialize might be backed by text. It might be backed by binary. So we really need our decorators. This, again, needs to be lazy. We can't go get the current type when you call it
18:00
because we don't know the current type yet because we haven't gone to the database. Now, decorators are not an API that's going to be public in Rails 5. However, when you are building these lower levels on top of each other, make your internal APIs just as nice to use. You as a maintainer want to be able to understand your system
18:20
and have the same simple composable APIs available to you that your users do in your public app-facing APIs. So there's a lot of code that I'm leaving out for gravity in the implementation of this, but the attribute type decorations is going to be an actual object in our system, not a hash, even though we're calling merge on it and other hash-like methods.
18:44
And it keeps track of the order that they were defined and other complicated things. And one thing to note here in this design, when you're designing a class macro, one of the important things is that it be idempotent. So if you call it at the same time with the same argument,
19:01
it should not modify the behavior multiple times. So we're passing in a name of a decorator into this argument instead of just the name of the thing we want to decorate and the block to decorate it with. So that way we can differentiate one decorator from another. So if we're going to use this for serialize internally, if you call serialize twice, you don't want to convert a thing to JSON
19:25
and then convert that to JSON again. You want to replace the original decoration. So this is what using this API starts to look like as we consume it internally. We give it a block, we give it a name,
19:42
and we look for any attribute that we previously had defined as a thing that we would convert the time zones on. We then create a new type object that tracks the original and in its cast and deserialize methods, it goes in and does the time zone conversion.
20:04
Now we can do the same thing for serialization. However, in this case, we're not basing it off of whether it's a time column. In this case, we're basing it off of purely the name. When you call serialize, it's serialize foo and you might say JSON instead of YAML.
20:21
So we can pull this out again. This seems like a common pattern, wanting to decorate purely based on the name instead of based on additional arguments, so we can pull this out into another API internally. So this is the same thing, but it just takes the name of the attribute instead of the block to be paraphrased.
20:45
And this is what serialize looks like in 4.2. The entire file has basically been deleted. I wanted to put the diff of this file, 4.1, 4.2, but it was so huge with all of the red things
21:00
that the dots were one pixel tall and it filled up the entire slot. And this is what the type object that we extracted from it looks like. There's code there, right? It's not zero code, but it's significantly smaller than what was there before. It turns out most of why intervals like that were implemented
21:20
in the way that they were was just because you have to know about every possible method that can affect typecasting. So we're building our APIs on a simple implementation of taking a type and an attribute and replacing the original type with the new one.
21:43
On top of that we were able to build a thing that we used to decorate an older type. On top of that we were able to build an API to represent a common pattern for that. Once we've introduced the API into our system, it should be universal. So we're modifying this columns hash internally,
22:02
which implies that the columns hash has a lot of additional information that is useful to typecasting. And at this point it really doesn't. We've separated the idea of a type attribute from the database schema. So what we have to do in, what we had to do in ActiveRecord is go through and introduce internal APIs
22:20
that may or may not go through the columns hash, that have steered that information away, so that eventually we can separate it out and make it so that there was a single canonical way to access the type for an attribute. Now we're not going to look at all of the diffs for this because it took about a year and required rewriting a lot of ActiveRecord
22:41
and a lot of A-record. But this is what the schema definition, the schema inference code looks like in Rails today, in OnMaster. So we're no longer defining all the behavior of ActiveRecord based on the columns hash. We have a single method where we go load that up
23:01
and we loop over it and then we just call public API. So when ActiveRecord builds the, determines the shape of the attributes and what types there from the schema automatically, that's just doing something that we're giving you the ability to do as well. And we also started to introduce, we started to find several other objects
23:22
that we've introduced into our system that made management of state in ActiveRecord much easier. This is one of them, it's called attribute and it handles the memoization and state transitions between the various states that an attribute can live in. It manages the types. We found that these objects started to be known about everywhere
23:42
so we introduced a collection object to handle the transitions between those and this is the thing that actually gets mutated. And most methods inside of ActiveRecord now very quickly change to these small one-off things that just delegate to this other object. In a lot of ways, it feels like ActiveRecord internally
24:01
has become a really bad implementation of the data pattern hidden behind a layer of indirection which I think qualifies it for worst non-native gem of all time. And one of the things in our API that we're looking for is we're trying to remove all these modules upon modules upon modules
24:20
that are just overriding behavior over and over again. We found common behavior that needed to be modified frequently. So we pulled out a new object in our system. When we need to add additional behavior on top of that, we can just use a decorator. We can use object-oriented principles that we all know and love. And when you have an object, again, it has an interface.
24:43
You can figure out what it can possibly change. So an API looking simple or having simple invocations is not the same thing as it being easy to understand. Here's the pathological example. If I have a product and that product belongs to user, if I change the user's name, I save the product.
25:03
Did the user's name change the database? Raise your hand if you think you know the answer. Trick question. It's based on whether the product is a new record. But that's sort of my point. Like, belongs to, I wouldn't even think that modifies save if I didn't just know it. There's absolutely nothing here that would indicate what could or couldn't change.
25:22
Certainly if I see that I'm calling the user method and there was a belongs to user, that's fine. But if I want to see what the possibly modifies save, where do I go? Do the docs for save say every possible class macro that can modify it? Do I have to go look at the docs for every class macro to see if that might modify save?
25:44
That's also important when developing these APIs to have a contract. So these are a couple of things that I think should be universally true for attributes in that record that are not true behind 4.1 and are true in 4.2 with me refactoring this API.
26:00
I should also mention this API exists mostly finished internally in 4.2. It's not going to be public until 5.0 unless this work went into 4.2. One of the things that we wanted to have be universally true, when you assign a value to an attribute and then read that value back out, it should never change based on saving and then reloading from the database.
26:23
If you assign the same value to a model from what's already there, the model should never be marked as changed. If you just call a new on a model and don't give it any attributes, it should never be marked as changed. And for any possible value on an attribute, when you pass that to where or findby or any of the finders,
26:42
you should get that model back. So this is the point where we're supposed to have a big conclusion and the aha and the end of point. I don't really know how to end this talk, so... Conclusions? Vibration systems? Please ask me questions now. Thank you very much.
27:13
Do we have any questions? Yes. I've used the 4.1 API and I've used the 4.2 API
27:21
and you just walked us through a little bit in order to make a JVC adapter driver, and I much prefer your 4.2 API. Thank you very much. Thank you. The question was, the internals are better now.
27:40
Yes, Ian. I was curious what the performance impact is now. It seems like they're reusing a lot, reusing more new objects and stuff in terms of typecasting and all of that. Yes and no. So the question was, what is the performance impact? It looks like we added a lot of new objects. And this was actually a very common concern
28:01
that came up a lot during the development of it. If you guys saw me at RubyConf this year, you might have seen that I was in the hall the entire time because right before RubyConf we had gotten the report that 4.2 was twice as slow as 4.1 and I was fixing that. So I have another branch where I removed the objects and replaced them with singletons
28:20
and I saw no improvement in performance. We did introduce the new allocation of the attribute objects but we removed several hashes which are string keyed and we can't actually guarantee that we're getting frozen strings coming in so by replacing the multiple hashes with this attribute object instead we were able to reduce the number of string allocations
28:41
and it comes out to be about the same. There's another low-hanging performance group that are becoming more possible because the internals have changed to this new structure. Dirty tracking can be moved to this attribute object which knows much more whether or not things would have possibly changed so we can do fewer checks and stuff like that.
29:27
So the question was from the maintainer of the or-enhance adapter and it was how is the contract of this published to the connection adapter? So there's a couple of different connection adapters.
29:42
The first one is we have a method that we need to be able to call to look up the type for a given column object. The only method that I believe we're calling for this is from active record base and then on the SQLite and MySQL Postgres adapters
30:00
we introduce the type map object and have a consistent internal structure for how that gets populated and how the lookups occur internal to the adapter object itself. The object is better than the signature
30:37
Yes. and here, how is it that there are other things
30:42
where it stays the same while you're doing other things? Yes, but I don't want to rewrite all the associations to do that. So, no. Oh, I'm sorry, the question was
31:00
I felt that passing the type object was a much cleaner API than passing a symbol in this DSL. Are there other APIs inside of Rails where I think the same thing is true? I think associations definitely because that modifies so much behavior in really unexpected ways I think we could gain a lot by describing that more in terms of objects
31:21
especially when you get into the ways gems can want to add new behavior to that but that's never going to happen. So you showed us an example of modifying the attribute of a record that belongs to another record.
31:42
Can you change it to another slide? So are we going to forbid changing the nested model attribute in this case in future versions of Rails? No. The question was are we going to change this behavior that I think is really confusing the answer that no we are not so that's breaking change and it's not painful enough to warrant that going for a deprecation cycle.
32:09
Any other questions? You showed an example where you could pass a custom money type as a type for an attribute.
32:21
If I need to create a custom type or something and I want to enable something like that do I need to create a type object? Is that an API that I have access to? Yes, and well the API you have access to the question is if I want to create a money type in my system is that an API that I have access to? Do I need to do that and is that an API that I have access to? Yes, the API that you have access to is creating a normal Ruby class.
32:45
The API in this object is three methods. There is a convenience class that you can inherit from if you want to. It's called type value and it gives you things like a template method where if you don't need separate behavior
33:02
for form input versus database input of which a lot of the simpler types like integer you're just always converting into an integer. It just calls a single method by default and then also has a method that you can override where nil is filtered out by default. It's really easy just to make this inherited from nothing.
33:22
It has three methods which are cast, serialize, and deserialize which is form input to the database and then from the database. The contract is the edge API that you can find the documentation for the attribute method and also looking at the class type value
33:42
where all those contracts are pretty poorly documented. We have a string and an integer. You mentioned how the similar thing happened to Mongo.
34:04
Is that going to be recommended? Is that going to do standard or unique upon it? Or is that going to be a normal model? Or is it going to be a special model? Is it going to be a model that's full of strings and integers? Or is it going to be separate from that? Right. The question is, I showed an example where we had a model
34:24
and I can go back to that example really fast. Looking at this example, the question was, after defining a simple string and integer on this model,
34:42
is that the new standard? Is this where we're expected to define all the attributes and often does the introspection still happen? Yes, the introspection does still happen. No, this is not the new standard. This will override the introspection. For example, one of the things that we can deprecate is this behavior that we have where a decimal column
35:03
with zero precision is treated as an integer for performance reasons in Ruby. We can just deprecate that happening automatically because if you want it to be an integer for performance reasons in Ruby, you can just do that. I'm personally going to exhaustively define everything
35:21
on my models because I hate having to go to schema-rv to see what methods I can call on the object. And I am scared mentally. And then I'm experimenting with a workflow where you turn on this automagic schema list thing in development when you first start creating a model.
35:41
And then you test drive, right? And you're like, okay, now I need a title, now I need a body. And you add them to the model, but you never create a migration. And then it just magically saves to one table where it can build everything and have it mostly still work. And then when you're done, you do Rails gdiff-migration and it quotes the model into the schema, and the diffs then comes up with the migration required
36:02
to bring them in line with each other. That's probably not going to be done in time for Rails 5. I hate being like, do I remodel and rerun this migration or do I have eight migration? Or do I just freeze and think of every attribute I'm ever going to add?
36:21
I'm trying to look at ways that we can use this API to eliminate that. We really like screens in that way. Instead of alleys doing this, it's like, okay, we're going to just put a unique index.
36:41
In the future, you might open up scenarios where we could maybe better support what we're doing. Yes. Okay, so the question was, at Roper, they really like databases. I heard Roper Postgres is pretty cool and you should check that out.
37:01
And specifically, if they like database constraints, then is there any chance that this work will lead to better support for validating things at the database layer? Hopefully, I would love to see us actually treat a unique index on the database as a canonical way to do that,
37:23
but still be able to prevent the user-facing error that you get from the uniqueness validation in Rails. For those not familiar, the uniqueness validation in Rails cannot actually validate the uniqueness of anything because it does not have a lock on the database and the database can change between when it goes to check to see if the value exists and when it tries to save the value.
37:41
The database is really good at validating this sort of stuff. I would love seeing more stuff put into the database and yeah, hopefully one day we'll get to the point where that's more the standard way to do it.