django和sqlalchemy集成
django-sorcer的Python项目详细描述
- 自由软件:麻省理工学院许可证
- github:https://github.com/shosca/django-sorcery
sqlalchemy是一个优秀的orm。django是一个很好的框架,直到您决定不使用django orm。这个图书馆 提供实用程序、帮助程序和配置,以减轻在django中使用sqlalchemy的痛苦。它旨在提供 与使用django orm构建django应用程序类似的开发经验,使用sqlalchemy除外。
安装
pip install django-sorcery
快速启动
让我们从创建站点开始:
$ django-admin startproject mysite
让我们创建一个应用程序:
$cd mysite $ python manage.py startapp polls
这将创建一个带有标准django应用程序布局的投票应用程序:
$ tree . ├── manage.py ├── polls │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py └── mysite ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py 3 directories, 12 files
让我们把pollsapp和django_sorceryin INSTALLED_APPSin mysite/settings.py:
INSTALLED_APPS=['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','django_sorcery','polls.apps.PollsConfig',]
现在我们要做一个改变,开始用sqlalchemy构建我们的应用程序。让我们定义我们的模型 polls/models.py:
fromdjango_sorcery.dbimportdatabasesdb=databases.get("default")classQuestion(db.Model):pk=db.Column(db.Integer(),autoincrement=True,primary_key=True)question_text=db.Column(db.String(length=200))pub_date=db.Column(db.DateTime())classChoice(db.Model):pk=db.Column(db.Integer(),autoincrement=True,primary_key=True)choice_text=db.Column(db.String(length=200))votes=db.Column(db.Integer(),default=0)question=db.ManyToOne(Question,backref=db.backref("choices",cascade="all, delete-orphan"))
现在我们有了一些模型,让我们使用alembicintegration:
$ python manage.py sorcery revision -m "Add question and poll models" polls Generating ./polls/migrations/3983fc419e10_add_question_and_poll_models.py ... done
让我们看看生成的迁移文件./polls/migrations/3983fc419e10_add_question_and_poll_models.py:
""" Add question and poll models Revision ID: 3983fc419e10 Revises: Create Date: 2019-04-16 20:57:48.154179 """fromalembicimportopimportsqlalchemyassa# revision identifiers, used by Alembic.revision='3983fc419e10'down_revision=Nonebranch_labels=Nonedepends_on=Nonedefupgrade():# ### commands auto generated by Alembic - please adjust! ###op.create_table('question',sa.Column('pk',sa.Integer(),autoincrement=True,nullable=False),sa.Column('question_text',sa.String(length=200),nullable=True),sa.Column('pub_date',sa.DateTime(),nullable=True),sa.PrimaryKeyConstraint('pk'))op.create_table('choice',sa.Column('pk',sa.Integer(),autoincrement=True,nullable=False),sa.Column('choice_text',sa.String(length=200),nullable=True),sa.Column('votes',sa.Integer(),nullable=True),sa.Column('question_pk',sa.Integer(),nullable=True),sa.ForeignKeyConstraint(['question_pk'],['question.pk'],),sa.PrimaryKeyConstraint('pk'))# ### end Alembic commands ###defdowngrade():# ### commands auto generated by Alembic - please adjust! ###op.drop_table('choice')op.drop_table('question')# ### end Alembic commands ###
让我们看看生成的sql:
$ python manage.py sorcery upgrade --sql polls CREATE TABLE alembic_version_polls ( version_num VARCHAR(32) NOT NULL, CONSTRAINT alembic_version_polls_pkc PRIMARY KEY (version_num) ); -- Running upgrade -> d7d86e07cc8e CREATE TABLE question ( pk INTEGER NOT NULL, question_text VARCHAR(200), pub_date DATETIME, PRIMARY KEY (pk) ); CREATE TABLE choice ( pk INTEGER NOT NULL, choice_text VARCHAR(200), votes INTEGER, question_pk INTEGER, PRIMARY KEY (pk), FOREIGN KEY(question_pk) REFERENCES question (pk) ); INSERT INTO alembic_version_polls (version_num) VALUES ('d7d86e07cc8e');
让我们更新数据库:
$ python manage.py sorcery upgrade Running migrations for polls on database default
现在,我们有足够的资源进入Django Shell:
$ python manage.py shell >>> from polls.models import Choice, Question, db # Import the model classes and the db # we have no choices or questions in db yet >>> Choice.query.all()[] >>> Question.query.all()[] # Lets create a new question >>> from django.utils import timezone >>> q= Question(question_text="What's new?", pub_date=timezone.now())>>> q Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text="What's new?") # lets save our question, we need to add our question to the db >>> db.add(q)# at this point the question is in pending state >>> db.new IdentitySet([Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text="What's new?")]) # lets flush to the database >>> db.flush()# at this point our question is in persistent state and will receive a primary key >>> q.pk 1 # lets change the question text >>> q.question_text ="What's up?">>> db.flush()# Question.objects and Question.query are both query properties that return a query object bound to db >>> Question.objects <django_sorcery.db.query.Query at 0x7feb1c7899e8> >>> Question.query <django_sorcery.db.query.Query at 0x7feb1c9377f0> # and lets see all the questions >>> Question.objects.all()[Question(pk=1, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text="What's up?")] >>> exit()
让我们在polls/views.py中添加几个视图,从列表视图开始:
fromdjango.shortcutsimportrenderfromdjango.templateimportloaderfromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreversefromdjango_sorcery.shortcutsimportget_object_or_404from.modelsimportQuestion,Choice,dbdefindex(request):latest_question_list=Question.objects.order_by(Question.pub_date.desc())[:5]context={'latest_question_list':latest_question_list}returnrender(request,'polls/index.html',context)defdetail(request,question_id):question=get_object_or_404(Question,pk=question_id)returnrender(request,'polls/detail.html',{'question':question})defresults(request,question_id):question=get_object_or_404(Question,pk=question_id)returnrender(request,'polls/results.html',{'question':question})defvote(request,question_id):question=get_object_or_404(Question,pk=question_id)selected_choice=Choice.query.filter(Choice.question==question,Choice.pk==request.POST['choice'],).one_or_none()ifnotselected_choice:returnrender(request,'polls/detail.html',{'question':question,'error_message':"You didn't select a choice.",})selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse('polls:results',args=(question.pk,)))
并在polls/urls.py:
中注册视图fromdjango.urlsimportpathfrom.importviewsapp_name='polls'urlpatterns=[path('',views.index,name='index'),path('<int:question_id>/',views.detail,name='detail'),path('<int:question_id>/results',views.results,name='results'),path('<int:question_id>/vote',views.vote,name='vote'),]
并注册SQLAlchemyMiddleware,为每个请求模式提供工作单元:
MIDDLEWARE=['django_sorcery.db.middleware.SQLAlchemyMiddleware',# ...]
并添加一些模板:
polls/templates/polls/index.html:
{% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><ahref="{% url 'polls:detail' question.pk %}">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %}
polls/templates/polls/detail.html:
<h1>{{ question.question_text }}</h1> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} <formaction="{% url 'polls:vote' question.pk %}"method="post"> {% csrf_token %} {% for choice in question.choices %} <inputtype="radio"name="choice"id="choice{{ forloop.counter }}"value="{{ choice.pk }}"/><labelfor="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br/> {% endfor %} <inputtype="submit"value="Vote"/></form>
polls/templates/polls/results.html:
<h1>{{ question.question_text }}</h1><ul> {% for choice in question.choices %} <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> {% endfor %} </ul><ahref="{% url 'polls:detail' question.pk %}">Vote again?</a>
这很好,但是我们可以使用泛型视图做得更好。让我们在polls/views.py:
中调整视图fromdjango.shortcutsimportrenderfromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreversefromdjango_sorcery.shortcutsimportget_object_or_404fromdjango_sorceryimportviewsfrom.modelsimportQuestion,Choice,dbclassIndexView(views.ListView):template_name='polls/index.html'context_object_name='latest_question_list'defget_queryset(self):returnQuestion.objects.order_by(Question.pub_date.desc())[:5]classDetailView(views.DetailView):model=Questionsession=dbtemplate_name='polls/detail.html'classResultsView(DetailView):template_name='polls/results.html'defvote(request,question_id):question=get_object_or_404(Question,pk=question_id)selected_choice=Choice.query.filter(Choice.question==question,Choice.pk==request.POST['choice'],).one_or_none()ifnotselected_choice:returnrender(request,'polls/detail.html',{'question':question,'error_message':"You didn't select a choice.",})selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse('polls:results',args=(question.pk,)))
然后调整polls/urls.py如下:
fromdjango.urlsimportpathfrom.importviewsapp_name='polls'urlpatterns=[path('',views.IndexView.as_view(),name='index'),path('<int:pk>/',views.DetailView.as_view(),name='detail'),path('<int:pk>/results',views.ResultsView.as_view(),name='results'),path('<int:question_id>/vote',views.vote,name='vote'),]
template_name和context_object_name的默认值与django的泛型视图类似。如果我们 handn没有定义那些模板名的默认值应该是polls/question_detail.html和 polls/question_list.html用于详细信息和列表模板名称,而question和question_list用于上下文 详细视图和列表视图的名称。
这一切都很好,但我们甚至可以用一个视图集做得更好。让我们在polls/views.py:
中调整视图fromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreverse,reverse_lazyfromdjango_sorcery.routersimportactionfromdjango_sorcery.viewsetsimportModelViewSetfrom.modelsimportQuestion,Choice,dbclassPollsViewSet(ModelViewSet):model=Questionfields="__all__"destroy_success_url=reverse_lazy("polls:question-list")defget_success_url(self):returnreverse("polls:question-detail",kwargs={"pk":self.object.pk})@action(detail=True)defresults(self,request,*args,**kwargs):returnself.retrieve(request,*args,**kwargs)@action(detail=True,methods=["POST"])defvote(self,request,*args,**kwargs):self.object=self.get_object()selected_choice=Choice.query.filter(Choice.question==self.object,Choice.pk==request.POST.get("choice")).one_or_none()ifnotselected_choice:context=self.get_detail_context_data(object=self.object)context["error_message"]="You didn't select a choice."self.action="retrieve"returnself.render_to_response(context)selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse("polls:question-results",args=(self.object.pk,)))
调整我们的polls/urls.py,就像:
fromdjango.urlsimportpath,includefromdjango_sorcery.routersimportSimpleRouterfrom.importviewsrouter=SimpleRouter()router.register("",views.PollsViewSet)app_name="polls"urlpatterns=[path("",include(router.urls))]
通过这些更改,我们将拥有以下URL:
$ ./manage.py run show_urls /polls/ polls.views.PollsViewSet polls:question-list /polls/<pk>/ polls.views.PollsViewSet polls:question-detail /polls/<pk>/delete/ polls.views.PollsViewSet polls:question-destroy /polls/<pk>/edit/ polls.views.PollsViewSet polls:question-edit /polls/<pk>/results/ polls.views.PollsViewSet polls:question-results /polls/<pk>/vote/ polls.views.PollsViewSet polls:question-vote /polls/new/ polls.views.PollsViewSet polls:question-new
这会将以下操作映射到视图集上的以下操作:
Method | Path | Action | Route Name |
---|---|---|---|
GET | /polls/ | list | question-list |
POST | /polls/ | create | question-list |
GET | /polls/new/ | new | question-new |
GET | /polls/1/ | retrieve | question-detail |
POST | /polls/1/ | update | question-detail |
PUT | /polls/1/ | update | question-detail |
PATCH | /polls/1/ | update | question-detail |
DELETE | /polls/1/ | destroy | question-detail |
GET | /polls/1/edit/ | edit | question-edit |
GET | /polls/1/delete/ | confirm_destoy | question-delete |
POST | /polls/1/delete/ | destroy | question-delete |
现在,让我们添加一个内联表单集,以便能够向问题添加选项,调整polls/views.py:
fromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreverse,reverse_lazyfromdjango_sorcery.routersimportactionfromdjango_sorcery.viewsetsimportModelViewSetfromdjango_sorcery.formsetsimportinlineformset_factoryfrom.modelsimportQuestion,Choice,dbChoiceFormSet=inlineformset_factory(relation=Question.choices,fields=(Choice.choice_text.key,),session=db)classPollsViewSet(ModelViewSet):model=Questionfields=(Question.question_text.key,Question.pub_date.key)destroy_success_url=reverse_lazy("polls:question-list")defget_success_url(self):returnreverse("polls:question-detail",kwargs={"pk":self.object.pk})defget_form_context_data(self,**kwargs):kwargs["choice_formset"]=self.get_choice_formset()returnsuper().get_form_context_data(**kwargs)defget_choice_formset(self,instance=None):ifnothasattr(self,"_choice_formset"):instance=instanceorself.objectself._choice_formset=ChoiceFormSet(instance=instance,data=self.request.POSTifself.request.POSTelseNone)returnself._choice_formsetdefprocess_form(self,form):ifform.is_valid()andself.get_choice_formset(instance=form.instance).is_valid():returnself.form_valid(form)returnform.invalid(self,form)defform_valid(self,form):self.object=form.save()self.object.choices=self.get_choice_formset().save()db.flush()returnHttpResponseRedirect(self.get_success_url())@action(detail=True)defresults(self,request,*args,**kwargs):returnself.retrieve(request,*args,**kwargs)@action(detail=True,methods=["POST"])defvote(self,request,*args,**kwargs):self.object=self.get_object()selected_choice=Choice.query.filter(Choice.question==self.object,Choice.pk==request.POST.get("choice")).one_or_none()ifnotselected_choice:context=self.get_detail_context_data(object=self.object)context["error_message"]="You didn't select a choice."self.action="retrieve"returnself.render_to_response(context)selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse("polls:question-results",args=(self.object.pk,)))
在polls/templates/question_edit.html和polls/templates/question_edit.html中添加choice_formset
<form...> ... {{ choice_formset }} ... </form>