Django是目前最常用的Python全栈Web框架。它已经存在了整整15年,在Python已经成熟的时期脱颖而出,成为赢家,但它的Web开发工具相对来说要不成熟和零散得多。
Django允许使用Model基类将程序中的对象定义为Models。它们的行为在很大程度上类似于普通的Python类,并增加了对保存到应用程序的关系数据库中以及从关系数据库中检索应用程序的支持。如果您不需要这样的数据库支持,那么您首先就不需要一个全栈Web框架。
Django尝试独立于您选择的数据库。这听起来是个好主意,但只是纸上谈兵。在使用Django系统几年后,无论是从头开始编写,还是继承和维护,我都觉得使用数据库和Django的方式是有福的,Django会让您以次优的方式使用数据库,并且不必要地使项目的开发和维护周期变得复杂。
我认为一厢情愿的想法和现实之间的分歧源于你和姜戈之间的根本误解,这并没有写在你还没有和他们签订的合同中:
Django之所以需要它,是因为没有绑定到单个数据库供应商的Web框架比绑定到特定数据库供应商的Web框架更有价值--这已经足够公平了。但是你不需要:你的网络程序很可能不需要从一个数据库切换到另一个数据库。那么,你也不需要它了,™。不惜一切代价的便携性至少会导致两个问题:
2)对您的模型或数据库模式的每一次更改都将比应该的更复杂。
您在一个项目上工作了多少次,经过1-2年的开发,您更改到了不同的数据库?
我可以告诉你这种事在我身上发生了多少次,我数了一下:完全没有。
更换您的数据库供应商是一件重大的创伤性事件,几乎与用不同的语言重写您的程序一样。如果您更换数据库,很可能是因为您对新数据库的功能感兴趣。您需要使用它们,所以使用新旧功能之间的共性不会解决您的任何问题。
您是不是从SQLite开始您的项目,现在您的项目已经发展到需要更大的数据库了?如果是这样,那么您仍然处于项目是玩具的阶段:您还没有做任何需要考虑并发性的事情。即使你不得不重写一些东西,也不会很多。
你有一个大的MySQL项目,现在你必须迁移到PostgreSQL吗?这是不可能的:你可能已经调整了MySQL,拥有MySQL方面的专业知识。也许PostgreSQL在某些方面可能是一个更好的数据库,但也不至于让你想要迁移所有的数据并从头开始,而不需要修改、摆弄、调整数据库配置。您是说您配置了高可用性和灾难恢复功能吗?当然,这也是一种变态。
在上面的段落中,将数据库供应商替换为MySQL、PostgreSQL、MS SQL、Oracle的所有排列。那是不可能的。不过,也许甲骨文的推销员在你的项目中找到了某个人,只是做了一点点决策,并说服他们购买了某种昂贵的许可证,但这不是一个技术问题,而是一个政治问题,你对此是否满意由你来决定。
您是否已将PostgreSQL投入生产,但您想使用SQLite进行测试,因为它更容易设置?如果是这样的话,您的测试只是一个勾选框练习:您没有测试任何与您的活动系统有丝毫相似之处的东西。
选择数据库是在项目的最初几天进行的,当项目成熟时就不会选择数据库了。您也可以使用数据库的所有可用特性,而不仅仅是那些足够常见以至于Django为其创建了Python包装器的特性。
扫描我编写并维护了几年的Django程序的模式,我发现:
如果使用了这些功能,那是因为它们允许以比语言中可能的方式更简单的方式实现程序所需的某些功能(如果可能的话)。审核实例:Django没有审核功能,除了在管理中所做的更改。即使您向每个save()方法添加了某种形式的手动审计,它也不会捕获在Django之外所做的更改。它也不会非常安全:Django使用单个用户访问数据库,所以如果有人试图劫持该用户,他们将能够更改数据库中的数据,并更改审计表以隐藏其踪迹。
创建一个AUDIT&34;用户:它将拥有与Django应用程序的用户不同的权限。
创建一个AUDIT&34;架构:撤销Django用户对它将包含的所有对象的写权限。
创建一个函数,将记录附加到由AUDIT&34;用户拥有但可由Django用户调用的审核表。
此设置需要特定于Postgres的知识,这对于必须监视数据库数据的功能来说是公平的,无论更改的来源是什么。但是由于PostgreSQL本身是可扩展的,您可以使用扩展来自动创建和维护审计触发器和函数。
这样做的好处是,如果你做事情足够仔细,Djangowy将根本不会注意到任何东西。
以上面的审计示例为例:Django并不是要与它交互:一切都会在它的眼皮底下发生。对于只读模型,您可以使用视图而不是表格,您可以使用域而不是字段的更基本数据类型:Django不会看到您的触发器触发,您的约束约束,您的权限允许-除非出现问题,这将导致错误500和Python回溯。这至少比数据库中的坏数据要好。而且,它也不会看到你的程序在进行,你的领域占主导地位……你明白了。
使用psql将模式导入到空数据库中意味着您可以模块化代码并使用\i导入";子模块";。对我来说,一个典型的模式是让database.sql创建全局对象(用户、扩展、模式),设置基本权限,并将详细信息导入目标模式。
\i用户。SQL REVOKE CREATE ON SCHEMA PUBLIC FROM PUBLIC;--SAFE--为Django app表创建一个模式CREATE SCHEMA myapp;将schema myapp上的用法授予myapp,backup;-为将在其中创建的所有对象设置默认权限将schema中的默认权限更改为myapp授予myapp对表的SELECT、INSERT、UPDATE、DELETE;更改schema myapp中的默认权限授予要查看、备份的表的SELECT;更改schema myapp中的默认权限将序列上的ALL授予myapp;更改schema myapp中的默认权限。-将表导入模式集合search_path to myapp,public;\i Django。SQL--Django对象--用户、组、权限表\i myapp。SQL--您的应用程序模型重置了Search_PATH;
Database.sql本身不会创建数据库,因此您可以在需要的任何位置创建一个空数据库,然后您可以使用psql-f database ase.sql";postgres://connection/url&34;来填充它。Myapp.sql文件不包含对模式myapp的任何引用,其中创建了对象,因此可以很容易地更改Schama。Postgres没有CREATE USER...。如果不存在:在users.sql中,您可以使用以下命令模拟它:
执行$$BEGIN从pg_user where usename=';myapp';;执行$$BEGIN 1。如果未找到,则创建用户myapp;end if;end$$language plpgsql;
Django有一个极其复杂的系统来执行模型迁移。除了其他原因外,这是很复杂的,因为:
它实际上迁移的是Python模型,而不是数据库模式。即使您更改字段的帮助文本,它也会生成迁移。
这对数据库一点用处都没有,但是Django会为您创建它,如果您删除它,它会重新添加它。类似地,更改选项列表(显示标签)会导致迁移,不需要数据库操作,只需要Python操作,实际上没有SQL用途。
它允许在迁移之间使用get_model(appname,model name)和返回的模型中的一些Python代码访问模型的状态。但是,如果该代码碰巧使用了应用程序中的任何代码,使用普通的Python导入来导入模型,则会因为数据库中的模型定义和模式不匹配而崩溃。但它们不会立即崩溃:只有当您应用一些无关的迁移时才会崩溃。而不是在需要迁移的时候:只有在它已经被应用之后,并且在它的生命周期中没有更多的事情要做:它是作为Django将反复导入的模型来实现的。在我目前正在进行的一个项目中,我不得不将此函数添加到我们的代码库中:
Def Can_Run(*Models):Try:with Transaction。ATOM():对于MODEL中的MODEL:MODEL。对象。全部()。First()Exception:Return False Exception:Return True。
并将其用作打击鼹鼠的工具,以避免已经申请的移民激增,增加了早期救助措施,如:
Def data_Migration(app,schema_Editor):如果没有,则Can_Run(Model1,Model2,Model3):return#...这是我想要做的类迁移(迁移。迁移):操作=[迁移。RunPython(Data_Migration),]。
它检测模型的更改并推断如何更改模式,这几乎不可能以自动方式完成,除非是在最简单的情况下。所以这个功能是不完整的,当它认为它知道自己在做什么时,通常它并不知道,而且很难让它停止。
在我经历过的一个案例中,我的模型foo在其属性栏中需要一些额外的胡椒。因此,我们考虑将数据从数据库提取到HIDDED_BAR属性中,然后使用Python属性来公开它:
Class foo(Models.Model):-bar=Models.CharField(+_bar=Models.CharField(max_length=255,+db_column=';bar';,)++@property+def bar(Self):+Return Something_More()。
不需要对数据库进行任何更改,但是Django坚持要创建包含以下大部分内容的迁移:
那么,再见吧,您的数据!不过,反正谁还需要酒吧呢。虽然我认为如果那次迁移影响了生产,我可以否决一家酒吧和几杯饮料。
这个系统的大部分复杂性都是为了给您提供一个特定的功能:从数据库中抽象出来的迁移。在我看来,如果有比编写复杂的应用程序在可互换的数据库上运行更不可能发生的事情,那就是需要重复相同的迁移历史。你为YAGNI功能付出的代价是一个笨重而脆弱的系统,试图在与你战斗的同时拥有自己的生命。这是杰里米·米勒(Jeremy Miller)观察到的一个直接应用,即任何从未使用过的可扩展性都不只是徒劳无功,它也很可能会阻碍你的工作。
我可能有点控制狂,但我认为这里有太多的魔力,太少的钩子来干预来纠正它,所有这些都与我的数据玩得太近了,让人感觉不舒服。这样做的好处是将来可以在不同的数据库供应商上重放您的迁移,但是我很难找到一个不涉及并行宇宙和时间机器的有效用例。
您可以使用SQL文件!是的,和以前的分机一样。这需要一些纪律,但它的回报是对数据库行为和数据安全的极大控制。涉及的工作包括:
了解您的数据库及其数据定义和操作语言:了解如何更改模式和相关数据。
如果您在源代码管理中有数据库架构,当您更改架构时,通常会希望关联一个类似的架构补丁。我的一位同事用正确的名称编写了一个迁移脚本和一个包含模式差异的注释,以给出一个要做什么的想法。
您可以让数据库表记录已应用到数据库的方案补丁程序,并使用脚本查找未应用的补丁程序:运行它们,并将它们记录到表中,以便在部署时运行。
我有一个patch_db.py脚本,我已经在几个项目中使用过这个脚本,只是有一些小的变化。它是以数据库超级用户身份执行的,因此应用程序用户只能获得有限的权限:足够读取和更改它需要的数据。无需成为超级用户或拥有表即可更改其模式。这个简单脚本的几个功能包括:
如果数据库从未打过补丁,它会创建SCHEMA_PATCH表,并按照已经应用的方式注册可用的补丁。
如果表存在,它会将补丁程序目录的内容与表内容进行比较,并按字母顺序应用和注册缺少的补丁程序。您可以使用日期作为前缀,以确保它们以正确的顺序应用。
它在整个运行过程中保持一个建议锁,以确保不会错误地同时运行两个修补进程。
在PostgreSQL中,可以通过其appname标识修补进程,因此可以监视和处理修补进程,以防出现意外行为。
补丁程序可以通过交互式用户确认逐个应用,也可以无人值守自动部署。
您可以将可执行脚本与修补程序相关联,以便在修补程序之前或之后运行,以实现难以在SQL中实现的过程:您可以让my_patch.pre.sh和my_patch.post.py分别在应用my_patch.sql之前和之后运行,并接收Patch_DSN环境变量以了解连接到哪里。
该脚本独立于Django;如果在Django项目中使用,它可能会作为管理命令实现,但我从来没有觉得有必要这么做。
将此脚本与您的部署过程集成后,可以生成以下补丁程序示例,扫描我使用过的项目的补丁程序目录:
将一个字段移动到另一个表,将一个表拆分为两个联接的表,将关系从一对多更改为多对多,而不会丢失当前关系。
通过创建分区、将数据移动到其中并清空基表来对以前未分区的数据进行分区。
在货币表中添加新货币,将该巴尔干国家的名称更改为北马其顿共和国,将该岛屿从欧盟国家列表中删除,或对某些配置表进行任何其他更改。
添加新的Django组并设置组权限或向现有组添加新对象权限。
为以前以非结构化方式存储到JSON字段中的数据创建一个普通列,从JSON中删除数据并将其移动到新字段。
实际上,Django迁移只自动创建最简单的操作,如添加新(空)列或删除列。重命名列已经是一项充满风险的操作,而在更改模型本身的同时使用Python模型处理数据则是一项极其复杂的任务。
在大多数情况下,在Django迁移系统中编写SQL补丁比编写apatch简单。后者要想成功实现,不需要太多的数据库知识,而需要移民系统本身的知识。
在Django迁移中添加不可为空的字段涉及创建正常迁移,即创建字段=model。UUIDField(默认值=uuid.uuid4,UNIQUE=True),创建两个空迁移(建议使用makemMigrations myapp--Empty,因为涉及一定数量的样板),以及相当多的内容具有以下顺序:
在应用迁移的同时,在对象创建之间也存在一些争用情况。
BEGIN;ALTER TABLE myapp_mymodel add uuid uuid;update myapp_mymodel set uuid=uuid_GENERATE_v4();ALTER TABLE myapp_mymodel ALTER UUID SET NOT NULL;COMMIT;
或者上面的变体(如果您想要由数据库生成缺省值、大表上的非锁定操作、唯一约束……)
编写非原子迁移既可以使用PL/pgSQL do语句在一个SQL脚本中全部处理,也可以使用另一个脚本替换上一个示例中的第二步(未经测试):
#Migration add_uuid_nullable.post.py cur=conn。CURSOR()WHILE 1:CUR。如果不是cur,则执行(";";";update myapp_mymodel set uuid=uuid_Generate_v4()from(select id from myapp_mymodel,其中UUID为NULL ORDER BY ID LIMIT 1000)x Using(Id)返回id";";";)。FetchAll():中断。
在第三方应用程序之间迁移数据会阻碍整个get_model()机制,您必须与其错误作斗争。
将ManyToManyField更改为使用直通模型是另一个例子,Django认为您的数据是可消耗性的,如果您想浪漫地保留它们,就可以跳过复杂的圈套。
整个示例由64行代码组成,只有当您知道模式更改机制的内部模型以及如何将Schemachanges和Django模型状态操作分开时,这些代码才有意义(在我看来,这两行代码一开始就没有理由放在一起,第二行也没有存在的理由)。
顺便说一下,第一个语句在RunSQL示例中是逐字的,我认为这个示例不一定是可移植的,所以我真的很难理解这一切的意义。
一旦您介绍了您的迁移过程,并激励您的团队使用它们,您可以使用以下几个操作来取代涉及Django迁移的官方方式:
Py迁移:第一次(例如,新的开发设置、单元测试)就可以运行psql-f database ase.sql。如果您在测试套件运行中执行此操作,您也将测试您的模式。如果初始模式已经就位(部署在试运行和生产阶段,将更改分发给其他开发人员),您可以运行类似于建议的patdb.py.的内容,并测试补丁。
Py makemigations:运行git diff schema/*.sql并计算出您必须做的事情,就像您想要使用的Django命令一样,该命令不能完成您需要的一切。
Manage.py南瓜迁移:……我对南瓜迁移的复杂性感到敬畏。它将您的大量迁移减少到不确定的较少迁移数量,这取决于您在其中创建并使用SQL或Python的时间。如果您想删除操作,可以将操作标记为";elidable";,但是要小心CircularDependencyError,尽管您可以使用--no-Optimize...。
一旦迁移应用于您的所有测试和生产系统,以及您的所有开发人员数据库,只需将其删除即可。或者把它们留在原地:谁在乎呢。即使它们留在那里,即使是数千韩元的模块也不会对您的程序的启动时间造成任何损害:它们不是自动导入的模块。如果您希望保留它们,而不是淹没在补丁中,可以将它们划分为月或年目录,并更改patdb脚本的一行以查找它们。
编写复杂的迁移包括尝试在验证数据库中执行的操作,这通常是一个反复尝试的过程,其中会出错,需要返回到迁移前的状态,并且不要忘记添加分号!
编写此处描述的类型的迁移补丁的方法大致由以下几部分组成:
在schema/patches/目录中创建名为YYYY-MM-DD_SOME_MENTIN的文件。
.