mito
mito copied to clipboard
Unbound slot error
Hi,
I have created 3 tables. users, courses and user-courses to implement a many-to-many relationship between the users and courses. When I try to access the user or courses object via the user-courses entry, which I got with find-dao from the database, I get an unbound slot error. Since I'm pretty new to Lisp (this is my first real application), I don't know if this behaviour is intended or actually a bug?
Simplified code to reproduce:
(defparameter *connection*
(mito:connect-toplevel :sqlite3 :database-name "mitotest.db"))
(mito:deftable users ()
((name :col-type (:varchar 64))
(email-address :col-type (:varchar 128))))
(mito:deftable courses ()
((name :col-type (:varchar 128))))
(mito:deftable user-courses ()
((user :col-type users)
(course :col-type courses)
(selected :col-type :bytea)))
(mapcar #'mito:ensure-table-exists '(users courses user-courses))
(let* ((user (make-instance 'users :name "Me" :email-address "[email protected]"))
(course (make-instance 'courses :name "Test Course"))
(user-course (make-instance 'user-courses :user user :course course :selected 0)))
(mapcar #'mito:insert-dao (list user course user-course)))
(defvar *my-course* (user-courses-course (mito:find-dao 'user-courses :id 1)))
Results in a unbound-slot error when trying to access the course from the user-courses object.
Hi, This simplified example works fine:
(let* ((user (make-instance 'users :name "Me" :email-address "[email protected]"))
(course (make-instance 'courses :name "Test Course"))
(user-course (make-instance 'user-courses :user user :course course :selected 0)))
(print (user-courses-course user-course)))
(your example lacks the lines to create the DB)
What does the find-dao return? You could inspect
the result.
You can ask on Stack Overflow.
ps: I'd use (list user …)
instead of backquote and comma.
Hi Vindarel, thanks for checking out the issue!
Of course I have created the DB in my actual application, this was just very simplified. :)
Yeah, I know that it works when I directly bind the objects returned from make-instance, but what is the point of saving it in a DB when I can't access them anymore afterwards? (type-of ...) says that a user-courses object is returned by find-dao, so that is correct. When I inspect the object, I see that course and user are unbound, but course-id and user-id are bound to the corresponding object ids. Therefore, I can't access the user and course slot.
In my opinion I should retrieve a user/course object when trying to access these slots (just like with the freshly created object from make-instance), correct?
And thanks for the tip with (list ...), you are right, definitely more readable. ;)
Okay, I just found out that I can retrieve the user id and the course id via user-courses-course-id
and user-courses-user-id.
Therefore I am able to retrieve the object like that and I suppose that user-courses-course doesn't work by design?
(defvar *my-course* (mito-find-dao 'courses :id (user-courses-course-id
(mito:find-dao 'user-courses :id 1))))
Works.
Of course I have created the DB in my actual application, this was just very simplified. :)
just saying your example isn't totally self-contained and ready to test for the lazy. I'll add
(defparameter *connection*
(mito:connect-toplevel :sqlite3 :database-name "mitotest.db"))
and
(loop for table in '(users courses user-courses)
do (mapc #'mito:execute-sql (mito:table-definition table)))
In my opinion I should retrieve a user/course object when trying to access these slots (just like with the freshly created object from make-instance), correct?
agreed, so
user-courses-course doesn't work by design?
AFAIK, it should. I don't understand either why the newly-created object is ok, and find-dao doesn't bind user
or course
:
;; after manual creation:
(mito:insert-dao *USER-COURSE*)
#<USER-COURSES {1003473FA3}>
CL-USER> (inspect *)
The object is a STANDARD-OBJECT of type USER-COURSES.
0. CREATED-AT: @2020-04-15T10:52:43.720762+02:00
1. UPDATED-AT: @2020-04-15T10:52:43.720762+02:00
2. SYNCED: T
3. ID: 2
4. USER: #<USERS {1003406DC3}>
5. USER-ID: "unbound"
6. COURSE: #<COURSES {1003423BD3}>
7. COURSE-ID: "unbound"
8. SELECTED: 0
CL-USER> (mito:find-dao 'user-courses :id 2)
#<USER-COURSES {10034AB1B3}>
CL-USER> (inspect *)
The object is a STANDARD-OBJECT of type USER-COURSES.
0. CREATED-AT: @2020-04-15T10:52:43.720762+02:00
1. UPDATED-AT: @2020-04-15T10:52:43.720762+02:00
2. SYNCED: T
3. ID: 2
4. USER: "unbound"
5. USER-ID: 2
6. COURSE: "unbound"
7. COURSE-ID: 2
8. SELECTED: 0
just saying your example isn't totally self-contained and ready to test for the lazy.
Alright, I think I self-contained it now.
AFAIK, it should. I don't understand either why the newly-created object is ok, and find-dao doesn't bind user or course.
Okay, thank you ... then I'll leave the issue open.
I have again been puzzled by that. As I inspect or describe an object, Mito says that a relation (user
or course
) is unbound. But: I can very well access it with its accessor. (I'd like to check this doesn't fire a DB query, just to be sure…)
So I tried again your example, and it works if you add an accessor.
Given this user-course
table:
(mito:deftable user-courses ()
((user :col-type users)
(course :col-type courses)
(selected :col-type :bytea)))
You tried to call user-courses-course
(supposedly automatically generated by Mito, and the symbol is indeed present with the autocompletion) on a user-courses
object ((mito:find-dao 'user-courses :id 1))
), but we got an error:
The slot BOOKSHOPS.MODELS::COURSE is unbound in the object #<USER-COURSES {10084B8CB3}>.
Now it works if you define an accessor:
(mito:deftable user-courses ()
((user :col-type users)
(course :col-type courses
:accessor course) ;; <= addition
(selected :col-type :bytea)))
and use it:
(course (mito:find-dao 'user-courses :id 1))
#<COURSES {1008427043}>
Come into similar issues, here is major part of my code:
(define-table file ()
;;omit since not important
)
(define-table test ()
((file-dao :col-type file
:initarg :file-dao
:accessor file-dao)))
(with-slots (file-dao) (find-dao 'test)
;;omit since not important
)
And, as you can see, the slot file-dao
is unbound.
So I looked into the source code and found the followings:
- When
select-dao
(which is behindfind-dao
andretrieve-dao
)make-dao-instance
callingselect-by-sql
for an object you get from db, it simply won't initialize slots of which col-type points to anohter class, leaving thevalue
of that slot unbound - Instead, I suppose that Mito create another method for
read
when youdeftable
, using something likefile-dao-id
to retrieve the correspondingfile
object, I believe it have something to do withadd-relational-readers
. Update: This method may depends on:accessor
, case
(define-table test123 ()
((file-dao :col-type file
:initarg :file-dao-1
:accessor file-dao-2)))
;;only file-dao-2 works
(file-dao-2 (find-dao 'test123))
(mapcar (lambda (slot)
(c2mop:slot-definition-readers slot))
(c2mop:class-direct-slots (class-of (find-dao 'test123))))
;; this can provide useful info too.
And according to comments in this issues:
- I suppose that the
user-courses-course
depends on thevalue
of slotcoures
. Sincecourse
work with:accessor
. I suppose we can confirm that once what is:conc-name
been known. Update:HyperSpec says that
:conc-name
... prefixing of names of reader (or access) functions fordefstrut
.
I suppose it do the same work for defclass
, and since the value of slot is unbound, then prefix-name is unbound too.
BTW,
(defclass te ()
((a)))
(a (make-instance 'te))
will give The function FOO::A is undefined.
(Since it has no :accessor
)
Since my goal is simply to use with-slots
, I did this silly work around:
(with-accessors
should fill my needed, but I'm lazy and don't want to type more.)
(defun find-unbound-slot (obj)
"Return unbound slots and workaround access read method to obj"
(let ((direct-slots (mapcar #'c2mop:slot-definition-name
(c2mop:class-direct-slots (class-of obj)))))
(mapc (lambda (slot)
(when (slot-boundp obj slot)
(alexandria:removef direct-slots slot)))
direct-slots)
(remove-duplicates direct-slots)))
(defmacro select-dao (class &body clauses)
(with-gensyms (sql clause results include-classes foreign-class dao unbound-slot value)
(once-only (class)
`(#+sb-package-locks locally #+sb-package-locks (declare (sb-ext:disable-package-locks sxql:where))
#-sb-package-locks cl-package-locks:with-packages-unlocked #-sb-package-locks (sxql)
(progn
(setf ,class (ensure-class ,class))
(let* ((sxql:*sql-symbol-conversion* #'unlispify)
(,sql
(sxql:select :*
(sxql:from (sxql:make-sql-symbol (table-name ,class)))))
(,include-classes '()))
(macrolet ((where (expression)
`(sxql:make-clause :where ,(expand-op expression ',class))))
;; ignore compiler-note when includes is not used
#+sbcl (declare (sb-ext:muffle-conditions sb-ext:code-deletion-note))
(flet ((includes (&rest classes)
(setf ,include-classes (mapcar #'ensure-class classes))
nil))
(dolist (,clause (list ,@clauses))
(when ,clause
(add-child ,sql ,clause)))
(let ((,results (select-by-sql ,class ,sql)))
(dolist (,foreign-class ,include-classes)
(include-foreign-objects ,foreign-class ,results))
(alexandria:when-let (,unbound-slot (find-unbound-slot (first ,results)))
(dolist (,dao ,results)
(dolist (,clause ,unbound-slot)
(alexandria:when-let* (,value (funcall ,clause ,dao))
(setf (,clause ,dao) ,value)))))
(values ,results ,sql))))))))))
There are multiple bugs in my work around.
My thought is to (setf unbound-slot obj)
before it returned from selec-dao.
Of course, this wll not handle some case like slot that has no :accessor
There should be better solution for this issues.
(mito:deftable user-courses () ((user :col-type users) (course :col-type courses) (selected :col-type :bytea)))
You tried to call user-courses-course (supposedly automatically generated by Mito, and the symbol is indeed present with the autocompletion) on a user-courses object ((mito:find-dao 'user-courses :id 1))), but we got an error:
Seems auto-generated accessors won't work as intended? Need to look into it.
This seems to happen on Arch, but not MacOS (both with SBCL 2.2.6). Very peculiar.