1

Hello This is my first question so please forgive the formatting:

Current lab setup

1 project:

library

1 app

catalog

2 databases

library_admin_db (admin-mysql)

catalog_db (mysql1

I'm trying to add a database object on "catalog_db" database using a "CreateView" Class based view

I already set up the Databases connection:

DATABASES = {
# Must use Default
'default': {
    'ENGINE': 'django.db.backends.mysql',
    'NAME': 'library_admin_db',
    'USER': 'root',
    'PASSWORD': 'password',
    'HOST': '192.168.164.128',
    'PORT': '8002'
},
'catalog': {
    'ENGINE': 'django.db.backends.mysql',
    'NAME': 'catalog_db',
    'USER': 'root',
    'PASSWORD': 'password',
    'HOST': '192.168.164.128',
    'PORT': '8000',
}

}

I set up the DATABASE_ROUTERS:

DATABASE_ROUTERS = [
    BASE_DIR / 'routers.db_routers.LibraryRouter'# the "MyApp1Router is the class inside the db_routers file"
]

Here is the Routers class:

    class LibraryRouter:
    
    route_app_labels = {'catalog'}

    def db_for_read(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return 'catalog_db'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return 'catalog_db'
        return None
    
    def allow_relation(self, obj1, obj2, **hints):
        if (
            obj1._meta.app_label in self.route_app_labels or
            obj2._meta.app_label in self.route_app_labels
        ):
           return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label in self.route_app_labels:
            return db == 'catalog_db'
        return None

here is my model with the foreign keys:

from django.db import models
from django.urls import reverse
import uuid

# Create your models here.
class Genre(models.Model):
    name = models.CharField(max_length=150)

    def __str__(self):
        return self.name

class Book(models.Model):
    
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    summary = models.TextField(max_length=600)
    isbn = models.CharField('ISBN', max_length=13, unique=True)
    genre = models.ManyToManyField(Genre)
    language = models.ForeignKey('language', on_delete=models.SET_NULL, null=True)


    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return reverse('book_detail', kwargs={"pk":self.pk})

class Language(models.Model):
    name = models.CharField(max_length=200)
    def __str__(self):
        return self.name

class Author(models.Model):
    
    first_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)
    date_of_birth = models.DateField(null=True,blank=True)

    class Meta:
        ordering = ['last_name','first_name']
    
    def get_absolute_url(self):
        return reverse('author_detail', kwargs={"pk":self.pk})
    
    def __str__(self):
        return f"{self.last_name} {self.first_name}"

class BookInstance(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)
    book = models.ForeignKey('Book', on_delete=models.RESTRICT, null=True)
    imprint = models.CharField(max_length=200)
    due_back = models.DateField(null=True, blank=True)

    LOAN_STATUS = (
        ('m', "Maintenance"),
        ('o', 'On Loan'),
        ('a', 'Available'),
        ('r', 'Reserved')
    )
    status = models.CharField(max_length=1, choices=LOAN_STATUS, blank=True, default='m')

    class Meta:
        ordering = ['due_back']

    def __str__(self):
        return f'{self.id} ({self.book.title})'

Here is the View:

from django.shortcuts import render
from catalog.models import Book, BookInstance, Author, Genre, Language
from django.views.generic import CreateView
from django.urls import reverse_lazy


# Create your views here.

class BookCreateView(CreateView):
    model = Book
    fields = ['title', 'author', 'summary', 'isbn', 'genre', 'language']
    # queryset = Book.objects.using('catalog') # Must use this if using a secondary database! if not using secondary database then this is automated!
    success_url = reverse_lazy('catalog:home')

    

    def form_valid(self, form):
        temp = form.save(commit=False)
        temp.save(using='catalog')
        print('Hello')
        return super().form_valid(form)

    def form_invalid(self, form):
        print('form_invalid') 
        return super().form_invalid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'].fields['author'].queryset = Author.objects.using('catalog')
        context['form'].fields['language'].queryset = Language.objects.using('catalog').all()
        context['form'].fields['genre'].queryset = Genre.objects.using('catalog').all()
        return context

Here is the Jinja template with the form:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Teacher Form</h1>
    <form method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Submit">
    </form>
</body>
</html>

The Issue comes here on the validation:

[enter image description here][1]

[1]: https://i.stack.imgur.com/oKD1n.png <- Image of issue

I'm not sure how to validate the information for the create form to the "create_db" I'm almost certain it's checking the "admin_db" but none of the records are stored in the "admin_db" only the "create_db".

I've been searching for hours with no luck as it seems everyone uses a single database and as such there isn't much documentation on support for multiple databases.

bekevin
  • 13
  • 4
  • Can you add your router class, too? – vinkomlacic Jun 10 '22 at 17:59
  • Sorry wrong routers class! Just fixed it and added the correct routers class! – bekevin Jun 11 '22 at 16:07
  • I don't think `DATABASE_ROUTERS` is supposed to have a list of _paths_. It should instead have a dotted path (How you would write an import for the class) – Abdul Aziz Barkat Jun 13 '22 at 16:10
  • @AbdulAzizBarkat it can have a list of paths according to Django documentation however the way I did the path was through a few copy past from other resources seeing how the full path needs to be dynamic depending on which OS I place the Django project onto. "BASE_DIR" seemed like a good way for 4.0 based on the built in comments, however, I also see importing os and using it to append the path is another way people do it. Could you give an example of another way you see as a best practice to do this! – bekevin Jun 13 '22 at 20:00
  • `DATABASE_ROUTERS` is supposed to take a list of import strings you could say, **not** a filesystem path, you mistake that with other settings like `STATICFILES_DIRS`. – Abdul Aziz Barkat Jun 24 '22 at 17:39

2 Answers2

0

For anyone who ran into this same issue, It looks like the CreateView is broken with foreign keys and many to many on Django 4.0.5.

The simple work around is to not use Create view and use FormView instead and just create your "Form and add the second database to the query like this:

from django import forms
from .models import Book, Author, Language, Genre
from django.forms import ModelForm

class BookForm(ModelForm):


    def __init__(self, *args, **kwargs):

        super().__init__(*args, **kwargs)
        # here I force the model's foreign key to the second database
        self.fields['author']=forms.ModelChoiceField(queryset=Author.objects.using('catalog').all())
        self.fields['genre']=forms.ModelMultipleChoiceField(queryset=Genre.objects.using('catalog').all())
        self.fields['language']=forms.ModelChoiceField(queryset=Language.objects.using('catalog').all())

        

    class Meta:
        model = Book
        fields = "__all__"

Then I just use the FormView:

class BookFormView(FormView):
    #basic info
    form_class = BookForm # Connect your form to your view here!
    template_name = 'catalog/book_form.html' # Connect your template!

    def get_success_url(self):
        return reverse_lazy('catalog:book_detail', kwargs={'pk': self.temp.id} ) # Make sure to from django.urls import reverse_lazy


    def form_valid(self, form):
        self.temp = form.save(commit=False) # have to do this first if using a second database
        self.temp.save(using='catalog') # using self to grab the id to redirect on the reverse_lazy
        return super().form_valid(form)

If anyone can get "CreateView" to work with multiple databases and foreign keys please share!

bekevin
  • 13
  • 4
  • How does having a custom form class prevent you from using `CreateView`? You can specify a form class for create view same way you can do it for form view. – Abdul Aziz Barkat Jun 13 '22 at 16:11
  • right, but the point of CreateView would be to avoid creating the form all together. if you need to create the form from scratch then you might as well use FormView. but that does bring up a good point as my question should then be, "how to create a Class Based View to add objects without creating a form using Multiple databases and foreign keys". CreateView normally handles the creation of the form but in this case Create View can't build a form with the correct validation when foreign keys are involved... Thus I believe it to be broken with foreign keys and multiple databases. – bekevin Jun 13 '22 at 19:48
  • That's not the point of `CreateView`, that's just one feature. The main point of that view is that when the form is valid it will _create_ a new object (assuming the form is a model form) – Abdul Aziz Barkat Jun 14 '22 at 04:12
  • Validation is normally done in the Form... So again CreateView just gets read of the Form creation and reduces the code you have to write. – bekevin Jun 24 '22 at 17:32
0

The problem is that you are specifying the DATABASE_ROUTERS incorrectly. The DATABASE_ROUTERS setting is supposed to be a list of import strings or the database router instance itself, whereas you are passing a list of pathlib.Path objects:

DATABASE_ROUTERS = [
    BASE_DIR / 'routers.db_routers.LibraryRouter'# the "MyApp1Router is the class inside the db_routers file"
]

You coincidentally don't get any error for this because of a few reasons, firstly Django's implementation [GitHub] is just assuming that you are passing a router instance to it:

for r in self._routers:
    if isinstance(r, str):
        router = import_string(r)()
    else:
        router = r

Next when it comes to actually using the instance it just assumes that the router passed doesn't implement the specific method as seen from the code [GitHub]:

for router in self.routers:
    try:
        method = getattr(router, action)
    except AttributeError:
        # If the router doesn't have a method, skip to the next one.
        pass

So in simpler words your router isn't actually being used and you should update your settings to pass a proper import string:

DATABASE_ROUTERS = [
    'routers.db_routers.LibraryRouter' # the "MyApp1Router is the class inside the db_routers file"
]
Abdul Aziz Barkat
  • 19,475
  • 3
  • 20
  • 33
  • 1
    Would like to add one additional detail here... this is the fix!... it was so simple! but something really dumb on my part is that I had already done this and it was still not working... you have to **RESTART** your Django app for the Router to change! hope this helps someone else! – bekevin Jun 29 '22 at 17:23