How (and why!) to build a Django based project with SQLAlchemy Core for data analysis
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 |
| |
Title of Series | ||
Number of Parts | 32 | |
Author | ||
License | CC Attribution 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 purpose as long as the work is attributed to the author in the manner specified by the author or licensor. | |
Identifiers | 10.5446/45425 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
DjangoCon Europe 201926 / 32
5
8
14
22
24
27
29
30
31
00:00
Core dumpData analysisSoftwareSoftware developerSlide ruleLink (knot theory)Software developerInformationMultiplication signQueue (abstract data type)Lecture/ConferenceComputer animation
00:24
Core dumpQuery languageComplex (psychology)BuildingDatabaseCASE <Informatik>Pattern languageRational numberEvent horizonVideo game consoleElectric generatorScaling (geometry)Source codeWeb pageGroup actionINTEGRALTable (information)Core dumpData analysisSeries (mathematics)View (database)Single-precision floating-point formatCalculationCartesian coordinate systemQuery languageLine (geometry)Mixed realityRow (database)Computer animation
01:54
Query languageFunction (mathematics)WindowRegulärer Ausdruck <Textverarbeitung>Constraint (mathematics)MathematicsCartesian coordinate systemStaff (military)InformationComputer animation
02:37
Interior (topology)Object-relational mappingLatent heatLimit (category theory)IcosahedronComplex (psychology)Query languageCategory of beingObject (grammar)Digital filterLattice (order)Loop (music)Price indexBitOrder (biology)Query languageFocus (optics)Operator (mathematics)Field (computer science)Data analysisLine (geometry)Row (database)Category of beingLimit (category theory)Lattice (order)Multiplication signMusical ensembleDialectKey (cryptography)Cross-correlationForcing (mathematics)Computer animation
03:46
Limit (category theory)Interior (topology)Hash functionDigital filterQuery languageClique-widthLattice (order)Right angleCore dumpField (computer science)Query languageLimit (category theory)Cross-correlationScaling (geometry)Core dumpEndliche ModelltheorieBuildingGoogolLine (geometry)Square numberComputer animation
04:30
Object-relational mappingQuery languageTable (information)Category of beingObject (grammar)Digital filterSimilarity (geometry)Query languageCASE <Informatik>Similarity (geometry)Category of beingDifferent (Kate Ryan album)Computer animationXML
05:08
Object-relational mappingCore dumpDifferent (Kate Ryan album)Scaling (geometry)Set (mathematics)Query languageImage resolutionCommutatorCore dumpSource codeComputer animation
05:41
Object-relational mappingQuery languageObject (grammar)Data modelElectronic mailing listLevel (video gaming)Group actionQuery languageState of matterComputer animation
06:15
Object-relational mappingBuildingEmailBuildingQuery languageInsertion lossCategory of beingTable (information)Form (programming)Stability theoryComputer animation
06:43
Lie groupLattice (order)Selectivity (electronic)Category of beingDialectStability theoryInformationScaling (geometry)BuildingQuery languageComputer animation
07:20
Local GroupLattice (order)Web pageCategory of beingLetterpress printingQuery languageObject (grammar)Digital filterOrder (biology)Limit (category theory)ASCIICodeMaxima and minimaTemplate (C++)BuildingOrder (biology)Total S.A.Query languageTable (information)Type theoryLimit (category theory)Functional (mathematics)Level (video gaming)Category of beingUniqueness quantificationGroup actionCondition numberRoutingInformationTemplate (C++)Adaptive behaviorCASE <Informatik>DatabaseSet (mathematics)Closed setLogic gateMaxima and minimaGoodness of fitComputer animationMeeting/Interview
10:31
Query languageTable (information)Endliche ModelltheorieInterior (topology)Right angleThermal expansionRecursionQuery languageLimit (category theory)Theory of relativityTable (information)Right angleRecursionLetterpress printingLattice (order)LogicCalculationInterior (topology)Library (computing)Row (database)Default (computer science)ExpressionInheritance (object-oriented programming)MathematicsForestOcean currentFiber (mathematics)Condition numberUsabilityPoint (geometry)Electric generatorOptical disc driveComplex (psychology)DataflowLecture/ConferenceMeeting/InterviewComputer animation
12:44
CountingQuery languageObject (grammar)Binary multiplierNeuroinformatikCountingResultantMultiplicationLattice (order)Data storage deviceMultiplication tableMoment (mathematics)NP-hardSheaf (mathematics)Computer animation
13:43
Query languageControl flowMereologyPersonal digital assistantData analysisComplex (psychology)BuildingDatabaseMultiplication signQuery languageProduct (business)Data analysisNumberCartesian coordinate systemWebsiteDisk read-and-write headMathematicsReading (process)MereologySoftware maintenanceProjective planeCASE <Informatik>Type theoryLevel (video gaming)Computer animation
14:41
Variable (mathematics)DatabaseDefault (computer science)Type theoryProjective planeINTEGRALDefault (computer science)Connected spaceCartesian coordinate systemPoint (geometry)Computer architectureMeeting/InterviewProgram flowchart
15:19
Thread (computing)Instance (computer science)Table (information)Endliche ModelltheorieMessage passingMeta elementRegulärer Ausdruck <Textverarbeitung>Endliche ModelltheorieConnected spaceInstance (computer science)Table (information)Cartesian coordinate systemComputer architecturePoint (geometry)ExpressionComputer configurationMultiplication signProcess (computing)Presentation of a groupReflection (mathematics)Thread (computing)Library (computing)Field (computer science)MappingRepresentation (politics)INTEGRALLecture/Conference
17:11
Table (information)QuicksortQuery languageRegulärer Ausdruck <Textverarbeitung>Data storage deviceComputer iconTable (information)ExpressionKey (cryptography)Group actionResultantCondition numberSystem callRow (database)Proxy serverTheory of relativityQuicksortBitQuery languageSlide ruleTupleAttribute grammarLattice (order)CodeData dictionaryComputer animation
18:25
Query languageSoftware testingUniform resource locatorSoftware testingRule of inferenceInterpreter (computing)Set (mathematics)Line (geometry)Function (mathematics)Variable (mathematics)OpticsSource codeFunctional (mathematics)DreizehnResultantProxy serverMathematicsComputer animation
19:12
Cursor (computers)Proxy serverException handlingIterationSoftware testingCursor (computers)Structural loadConnected spaceException handlingRow (database)Semiconductor memoryResultantWrapper (data mining)Process (computing)Functional (mathematics)SpeicherbereinigungSystem callRight angleCASE <Informatik>Parameter (computer programming)Computer animation
19:56
Process (computing)Wrapper (data mining)ResultantCASE <Informatik>Functional (mathematics)Exception handlingParameter (computer programming)Proxy serverComputer animation
20:21
Software testingPersonal digital assistantRollback (data management)Database transactionCodeTable (information)CASE <Informatik>Endliche ModelltheorieParameter (computer programming)Query languageDatabaseSet (mathematics)Control flowExpressionSet (mathematics)Theory of relativityType theoryDatabase transactionConnected spaceGame controllerSource codeInjektivitätOffice suiteParameter (computer programming)DatabaseTable (information)Factory (trading post)State of matterSoftware testingCASE <Informatik>Independence (probability theory)BitINTEGRALInformationEndliche ModelltheorieElectronic program guideCartesian coordinate systemMereologySquare numberLibrary (computing)1 (number)SoftwareLevel (video gaming)Scaling (geometry)Multiplication signPoint (geometry)Functional (mathematics)Content (media)Insertion lossCodeAbstractionBlock (periodic table)Query languageBoolean algebraBuildingHuman migrationData analysisRollback (data management)NP-hardData dictionaryFree variables and bound variablesTupleExpressionVideo game consoleXML
25:23
Dean numberSlide ruleLink (knot theory)DialectWhiteboardQuicksortProcedural programmingBitWordMereologyConnected spaceNumberProjective planeRule of inferenceView (database)Square numberMultiplication signProcess (computing)Data storage deviceSingle-precision floating-point formatMetropolitan area networkBit rateArithmetic mean40 (number)Point (geometry)SequelSoftware developerGoodness of fitDatabaseQuery languageBuildingObject-relational mappingStack (abstract data type)System administratorOnline helpCartesian coordinate systemFunctional (mathematics)Front and back endsOcean currentCodeComputer animation
Transcript: English(auto-generated)
00:02
Hello, my name is Gleb, I'm a software developer from Ukraine, Kyiv, working with Python Django for more than six years, currently employed at Django Stars. And today, I want to share with you actually a lot of information, so I have to be really fast to fit on the time. And I hope we will have some time for questions, but I don't promise it.
00:24
Actually I want to start with motivation. Why should we actually need to mix Django and SQL alchemy core, which benefits we can get, and in which cases that could be useful for us? So actually the main use case is data analysis application.
00:41
So your application works mostly with aggregations, so you don't work with single lines of table, you don't have REST API, or you don't modify single rows of your data. So you're only interested in aggregations and calculations. So you likely will have a lot of data, so you have your queries to be really performant
01:05
and precise. And maybe you could have some, if you have some pattern of your pages, you could build some wire which will generate such advanced queries for you. So you maybe have such wire, and it's really easy to build it with SQL alchemy.
01:22
And maybe you also want to, you design your queries in SQL in the console, you check performance, how it works, then you satisfy it and you want to move them to Python. It's really easy to do it with SQL alchemy also. And maybe your data source, which you work with, doesn't natively support it by Django,
01:42
and maybe third-party applications, which provides a support and integration, they are not fit your needs. So this is the main use case when you likely should check this tool. So I really like Django, I really like Django, it's a fantastic tool. You even kind of forgot how to SQL, but yeah, it's greatly evolved.
02:07
It allows you to focus on the implementing of business features instead of doing some low-level stuff, so you really can deliver your features much faster and more reliable
02:22
way. And in the last releases, Django got a lot of cool features, like subqueries and functions, so you already heard it yesterday from Sigurd. And so you have less reasons to switch to raw SQL. But if we build the data analysis application, Django RAM has its own specifics, so let's
02:45
talk about this. So, for example, if we want to, we have properties and we have owners, and we want to find, for example, five properties, which is the city, which starts with the letter key, and we want to get the user name of the owner. So we're doing select-related, which performs left outer join.
03:06
We can also limit the amount of fields which we want, so query is a bit simpler, but in Python it's less readable. So I want you to focus on these two lines. So as you see, we have left outer join and we have limit.
03:21
And if we see the explain how, in which order this operation performed, we see that first of all, we find the properties which we are interested in, it's huge amount of properties, and only then we join to each of the row of the property, we join owners, and only after that we're doing a limit.
03:42
So this is not performance, this is not the way that we want to execute the query. Likely we want to have a query like this, so we have nested select, it's like correlated subquery, and we select only fields we're interested in, so we're doing limit there. And then we join users to these five rows, so it's very performant, it's good, believe me.
04:07
You have not to read it. And with the scale alchemy core, such query will look like this. And as you see on the first line, we simply build this nested select, which doing a limit,
04:20
so this subquery, and we simply insert it in other select and perform join. So it's really straightforward, and you can really fast understand what's going on underneath. In Django REM, this query will look like this. That's why, because all your query we should build, it starts from model, model, like declare for you the from clause, which will be used in the query.
04:45
So the subqueries that we have, they could be used in the select we're having, but we can't put it into the from, so we can't make select from select. So we, of course, can build a similar feature, similar query.
05:02
We can get a primary case of the properties which we're interested in, and put it into subquery. So it's also performant, so it's okay. But this example showed us that actually Django REM and the scale alchemy core, they are on different wires, and they are designed to solve different needs and different problems.
05:24
So in Django stack, we have underlying non-public API, which is a query set dot query, but documentation doesn't recommend to use it. But in your scale alchemy core, it's like on top of the SQL, so it's very close to SQL,
05:42
and as you can see, it's very readable. It's really easy to understand, and you can build your query easily. On the other hand, as you see Django REM sample, the first filter, it would be actually the where clause, the values, it would be a group by.
06:00
Annotate would be a list of fields, a list of aggregations, which you want to perform on the query, and the last filter would be actually a heading clause, which you apply on the top of your query. So it's not directly mapped to the SQL level. And when you're building queries on ORM, such queries,
06:22
you really feel that you don't have enough freedom. And I want to show one more example. For example, we have two tables. We have properties and owners, and in properties table, we have apartments and we have buildings. So apartments place it in buildings, and they have this building ID,
06:41
so it's self-relation to the table. So let's start building our fancy query with scale alchemy. We're doing simple select from the properties table. We just select information which we want, like building ID, owner, and sale price. And we want to get the properties which are for sale and which has sale price.
07:02
So simple select. Then we can add more level, where we're doing select from the previous select. But here we want to replace the owner ID with user name. So we have to join users, and we simply replace it. Then we decide that we need to go deeper,
07:23
and we add one more level. So we built level three in which we have building ID, so we can group by our apartments and get the total price of all apartments which are for sale in this building. So we're doing group by. We apply the functions that we need, and it's very readable.
07:45
And on the last level, we have only unique building ID, so we can join building related information here, so we have only roles which we want to join. So we can add total apartment amount to the building.
08:05
So your query would look like this. So it's select from select from select from select. And on each level, you're doing join, group by, again join. So this is the freedom I'm talking about. So SQLAlchemy allows you to build any type of such queries,
08:23
which is really cool. Another example about Django limitations and readability. Imagine the case, we also work with properties. So we want to find the first price which is bigger than one million, and then we want to get all properties which has the same price which we just found.
08:45
So the query we want to build is like this. So we put like where sale price equals two, and aggregation over all the table by our condition. Let's try to do it in Django. Our first attempt would be to use aggregate,
09:02
but aggregate queries are evaluated immediately, so it doesn't work for us, because we have two queries to database, two trips which is not performant. So we want to use subqueries. Subqueries worked with query sets, and that's why we have to change our minimum aggregation,
09:25
minimal to the such trick like order by ascending by price, and get the first one. So it will get the minimum price, but as you see in our select, we have also order by limit one, and it's not readable.
09:40
And if you see order by or distinct account in your SQL query, they are not performant, and this is not the way that you want to build queries if you work with big amounts of data. So we can leverage underlying public API which I mentioned earlier. So as you see, we have a query set dot query add annotation.
10:02
So we manually add this annotation which we want. Its summary says that it should be applied over the old table, and we actually get the query which we want. But yeah, it's not recommended in the Django documentation, so it's up to you, do you want to use it or not. And one more way, we can subclass subqueries,
10:22
and we can override template. But yeah, we also get the SQL which we want, but this solution is to route as for me. So on the other hand, with SQL alchemy, this is pretty straightforward. As you see, on the first line, we build in also select,
10:43
and in the second line, in the where, we simply put this select into the condition, and it will do all the magic, and we will get the query which we want. More limitations in Django RM, joins. You can't join tables which are not related. If they don't have relation, you totally don't have ability to do joins.
11:03
You can't do right outer join, but yeah, it's very rare, but sometimes it happens, but I have to mention it. Also, Django decides for you when to make inner or left outer join, and you doesn't know what's happened underneath.
11:21
If you don't print query, or you can't change it from left outer to inner if you want to simply cut out some rows which has no values. And Django also generates for you this join condition, on condition, and you can barely customize it.
11:43
You can't get rid of it. You can't change this default. You can only use filter duration, which will add and, and that's actually all. So you can't provide or, or some more explicit logic, some more complex logic to do join.
12:03
Recursive common table expressions are also not supported by Django yet. It's useful when you work with the roles which has like parent ID, and it's pointing to another role which has parent ID, another role, or another role, and you want to take all of them
12:21
and make some aggregation or calculation over them. So currently you can do it with role SQL or SQL alchemy. Also, there is a library like Django city forest. But it has limitations and it implemented via extra, but extra would be deprecated, so it's not recommended to use.
12:41
So this library is not usable currently. One more example about how Django syntax is far from SQL. So it's actually from Django documentation. I like it very much. So as you see, we have book, we have authors, we have store, stores.
13:00
And if we're doing this annotation in one line, so aggregation of multiple tables, so we get the wrong aggregation. So in fact, instead of subqueries, it will be a join. So you simply multiply amount of roles and all the aggregation will produce wrong result.
13:22
Count could be fixed with distinct true, so it will kick out all roles which are redundant, but actually other aggregations which will produce the result which you don't actually want to get. So if you not read the computation precise enough,
13:41
you could fail in such issue. So to sum up, it's hard to read advanced queries in Django. It's hard to understand what's going on SQL level. And yeah, it takes time to convert a query from SQL to Python. If you design it, it's somewhere. And it's not so flexible, so some parts you can't change
14:03
or you have to change your original SQL query to make Django to be able to build it. But this is not a problem in 95% of the cases. 95 is the number from my head. So usually it's okay.
14:22
We have ability to switch to raw SQL. And if you have a few such places, it's not an issue and it doesn't hurt maintainability of our product. But if you build a data analysis application where you have only aggregations, it really could hurt. It's like the same site as you saw.
14:42
So it's okay to look at such tool like SQL alchemy for such type of project. So here's a brief tutorial for you how to start and what you have to expect with this integration. First of all, you have to create engine. It's a global variable which describe your connection to database.
15:02
And SQL alchemy comes by default with connection pooling functionality. So you get a queue pool. But if you want your connection to behave the same as a Django, like Django connection, you have to disable pooling with no pool. So from architecture point of view, your application would look like this.
15:25
So you have one instance. You have UGGI. You have four workers which are processes. Each of them configured to have eight threads. And each thread manage its own connection. So you have 32 Django connections and 32 SQL alchemy connections.
15:43
So likely later when you get the user, so maybe when your instances start scaling, so you would like to add some connection pooling wire like PGBouncer. For example, if you use Postgres because for Postgres, it forks a process for each connection.
16:02
So it's quite expensive to hold connections. It takes at least 10 megabytes of RAM. So if you have two instances, you just simply lost 1.2 gigabytes of RAM simply to hold these connections. So that's why connection pooling wire would be recommended for you if your application grow.
16:20
Then you need to understand how you will work with the tables. So you have few options. There are two libraries which allows you to integrate SQL alchemy with Django. First of them, Algemy, it has a mapping of the Django fields. And it's built SQL alchemy representation of this field of the tables
16:41
and attach it to the models. Another one, it's table reflection. It's used, it's SQL alchemy technique which allows you like here, you say, okay, I have this table. I want to get Python representation of this table. So it generates it for you, but it takes some time.
17:01
So you have to cache it maybe on application launch. Also, you can define your tables explicitly or define with inline expressions. If you define explicitly, it's look like Django models. But you can keep it more simpler because we only read the data.
17:20
So you can provide only names of the table, of the columns. But I also advise you to specify foreign keys because SQL alchemy automatically pick up this relation. So you have not to specify on condition for joins. So it would be a bit simpler in code. So how you can use such tables?
17:43
So you build a query and you have C attribute which stores columns. And you use it like a reference. And the result which you get, each row of result is a row proxy which actually behaves like named tuple or like tuple or like dictionary.
18:00
So it's very handy. And also, it's possible to define your tables and columns with inline expressions. They are lightweight. So you simply put the names in the query. But if you have to keep the reference for the column to the table, which it refers to, you can define it like this.
18:22
So the usage would be like on the previous slide. Actually, that's all what you need to know. But did somebody say test? Test is not so straightforward. It has some pitfalls and something which is worth to mention.
18:42
So first of all, if you remember, we created in Gen is a global variable. So once the interpreter will read this line, it will evaluate it. So we can't use override settings. We can't use pi test fixtures or hooks to change this setting because our line would be already evaluated.
19:01
So likely, you will have to introduce a function like this. So if it's test, it adds test prefix for you. Otherwise, you will lose your data. Another interesting feature, which I faced, it's how pi test work with cursors.
19:21
So if you have a result proxy, it's actually a wrapper around db.api cursor. And you have ability to iterate over the rows. So you don't load all of them into memory. You're just iterating across them. So you have a function like process rows. And maybe somewhere inside there is you write test
19:40
and there is exception and it fails. So pi test will mark this test as failed and it will hang on forever because it doesn't allow garbage collector to close the connection. Usually, it's performed automatically. So you have to do such tweaks like, for example,
20:01
write a decorator which applied to this function which iterate and produce exception, for example, in case of some error. So you simply run across all arguments. And if it's a result proxy, if it's your cursor, you close and erase exception. But it looks not so straightforward.
20:22
And the most important thing which you have to keep in mind when you develop such type of application is that you have two connections. And each of these connections have its own transaction isolation level. So if you use this case and you populate the database with model mommy or factory boy,
20:42
it will happen in Django connection. So for test case, in start of each test, Django creates transaction for you but it doesn't commit it in the end. It makes a rollback. So transaction is never committed and SQLAlchemy doesn't see the data which was produced in this test.
21:01
So this is some kind of issue. But still you can use test case in your application. But just keep in mind that when you write tests for the code which works with one connection and you populate the test database with the same connection.
21:21
But if you populated the data with SQLAlchemy connection, you have to clean your tables by yourself between the tests, maybe with hooks or fixtures. And also theoretically it's possible to share the data which was created and not committed. So you need to change the transaction isolation level on the SQLAlchemy connection to read uncommitted.
21:43
But yeah, it's not recommended but it's possible. And also you can use transaction test case and there is no issues with that. So it's fine but such tests work slower because in how this transaction test case works
22:04
they are doing a post migrate signal between each test. So they truncate all the tables which have models and then they launch post migrate signal which creates permissions and content types. So it takes some time. So such tests would be slower. Yeah, and if you work with tables which doesn't have relation to Django models
22:25
you also have to clean up them between tests by yourself. So drawbacks. It's a bit hard to start because it's not too much information about such integration and there are no such like tutorial like, okay, you have to do this.
22:42
But documentation of SQLAlchemy is quite good but sometimes it lacks some examples. But overall it's nice to work with. And yeah, you can find that it's not very easy to work with. If you want to debug your query which you made in Python
23:02
so you print it into the console and you will see that all parameters which you have maybe 20 or 30 of them if it's hard query it would be a placeholder instead of values which you wanted to be there. So it's a bit annoying to insert them manually
23:20
because this functionality is done by DBAP which doing this safe insert of the parameters into the query so SQLAlchemy doesn't have such functionality of escaping your values to omit this injection, SQL injections.
23:41
And yeah, there are some solutions but it's not so straightforward so it's a bit annoying. And you have slower tests because likely you will use more transaction test case which are slower but it also depends. And yeah, you have more connections to database but the most important thing to keep in mind
24:03
that you can't no longer use libraries like Django Filters or pagination because they designed it to work with query sets and now you have only like dictionaries or tuples or lists so you have to find some query set independent libraries or come with your own solutions to cover this gap.
24:24
But let's talk about benefits. So you actually get the full control over SQL level which is really cool and it gives you freedom to build whatever you want. Yeah, it's faster to express SQL in Python code so if you have some data analysis guy
24:41
which bring you tons of queries and say, okay, build these queries in Python, it's really easy to build them, modify. Yeah, and if you have some layer which creates such queries for you dynamically, you have in SQLAlchemy, you have all abstractions over SQL so you can use them like building blocks
25:01
and you can build whatever you want. It's very handy to do this. And yeah, of course, readability, mintability and performance because actually what you design it in SQL, you generate it from Python so if you know SQL well enough, so you will do correct queries and performance.
25:23
That's all. I don't know, do we have time for questions?
25:43
We do have a few minutes for questions. So if anyone wants to come to the front or if we have anyone online with DjangoCon QA hashtag, we can answer a few questions. Hello, thanks for the talk.
26:01
One question come into my mind. You said you are having two database connections which is obvious because you have Django and SQLAlchemy. Did you explore the possibility of writing, I think in SQLAlchemy it's a database dialect which would bridge over to the Django connection and reuse the underlying database API connection
26:24
instead of having the two? Sorry, could you repeat? So there would be the possibility to write a custom backend for SQLAlchemy which would then reuse the connection from Django. Did you explore this?
26:40
Actually there is a library. Currently it's under development. It's like Django SQLAlchemy and it tries to be inserted into the wire which generates these queries. So there are some attempts to merge them together but this is, as far as I know, they're currently under development and likely it would be not so easy and straightforward.
27:03
So I haven't tried this but I'm not sure that this good way to move the application. Okay, thanks for the talk. It was interesting. I probably can guess the answer
27:20
but I wanted to know from you. You haven't talked about the admin stuff in Django. Would it be possible to use the SQLAlchemy with the admin? Probably not. No, I believe there are some attempts also to bring this to the Django stack
27:43
but the solution which I'm currently talking about, it's mostly about designing your queries which would be displayed for the users. Maybe you can use it. If you start customizing a lot, your Django admin,
28:00
maybe you can insert there but it will be not easily integrated and Django admin, it's mostly ORM so it works with single roles but this is aggregation. So you can help build custom dashboard somewhere to show the numbers, overall numbers but if you want to work with single roles
28:21
so you have to stick with Django ORM. Okay, thank you. We have a little bit less than one minute so we can answer about one more question. Okay, I will try to be fast. So you mentioned in your talk that you start usually with a big SQL query that you already have.
28:41
So I was wondering if you explored to store that query as a stored procedure in the database and query and using crisis to launch the stored procedures and what are the pros and cons compared to SQLAlchemy in your project? I think it's mostly about how do you then manage
29:03
and maintain your stored procedures because if you have everything in Python code in Python project, it's easily to manage, to change and to influence in that. So I can say about pros and cons,
29:21
from my point of view, it's mostly about how it will be maintainable in the long run. So my opinion, it's better to have everything in Python and not move this functionality to database because it would be not so explicit and not transparent what will be going on. Thank you.